Changes asked one by one, 2026-09-16, outside the GSD cycle ("no need to GSD for these only if you
think it's necessary"). Each is recorded here verbatim before it is made.

## 1. Apply to ZONA goes; Store stays; every Store clears first

> first: we dont need the apply to ZONA, only Store stays. also every Store should send a Clear
> before Storing

Reading, to be confirmed by the user if wrong: the RAM audition (Apply to ZONA, Phase 7's TRY ON
DEVICE) leaves the interface everywhere - the workspace's bar, the Sandbox's bar, the probe pages
keep their own controls. Store on ZONA is the one write, and it becomes: the five firmware defaults
(the same five Clear writes), then the configuration's five strings, then the page store, then the
read-back proof - one flash write per click, the page defaulted underneath it. The inline
confirmation on Store (SAFE-05) stays. The browser preview is the audition. PROJECT.md's "flash
writes stay separate from RAM auditions" and SAFE-02 / SAFE-05's RAM-before-flash wording are
retired by the user's word; the gate that amends them is whichever phase next runs a gate.

### The strings change 1 wrote, verbatim (D-05's register), and the ones it retired

New or rewritten, each in `src/lib/device/install-copy.ts` unless named otherwise; the sample page
is wire 1, read as `Page 2`, as the batch writes it:

- `keepLineEnabled` (Store on ZONA's description while connected, I.6.1 rewritten):
  `Returns Page 2 to its firmware default, then writes this and stores it, so it stays after power-off.`
  ~~`Stores this on Page 2 so it stays after power-off.`~~
- `KEEP_REASONS.already-kept`: `Already stored on ZONA. Change something to store it again.`
  ~~`Already stored on ZONA. Turn a knob and apply it again to store a new one.`~~
- `KEEP_REASONS.no-session`: `Needs your ZONA connected.` (NEEDS_ZONA, referenced, not retyped)
- `confirmReplaces` (the confirmation's second sentence, I.5.9 rewritten):
  `This returns Page 2 to its firmware default, then writes this configuration — its touch element’s Setup and Timer and the page’s own init, timer and utility scripts — and stores it, so it stays after power-off.`
  ~~`This replaces what Page 2 holds on your ZONA — its touch element’s Setup and Timer and the page’s own init, timer and utility scripts — and it stays after power-off.`~~
- `nothingLandedBlock("store")` (the form a Store's RAM leg takes, I.5.5):
  detail `Nothing of this reached Page 2, and nothing was stored.`
  step `Click Store on ZONA to send it again`
  ~~`Nothing got through. Page 2 is unchanged, so your own configuration is still playing.`~~
  ~~`Click Apply to ZONA to send it again`~~
- `keptMismatchBlock.steps[0]`: `Click Store on ZONA again`
  ~~`Click Apply to ZONA, then Store on ZONA again`~~
- `partialBlock.steps[0]`: `Click Store on ZONA to send all five again`
  ~~`Click Apply to ZONA to send all five again`~~
- `snapshotFailedBlock.steps[0]`: `Click Store on ZONA to read it again`
  ~~`Click Apply to ZONA to read it again`~~
- `src/lib/tune/copy.ts` `tryOnBudgetReason` (the name keeps its history):
  `Over the 908-character budget on Setup. Turn something down to store it.`
  ~~`Over the 908-character budget on Setup. Turn something down to apply it.`~~
- the bar's busy clause (`device-clause.ts`, `writing`) is `keepingLabel`: `Storing on Page 2…`
  ~~`writingLabel`: `Applying to Page 2…`~~

Retired by name (the ledger block in `install-copy.ts`, "APPLY TO ZONA'S STRINGS ARE RETIRED BY NAME,
2026-09-16"): `TRY_ON_LABEL` (`Apply to ZONA`), `writingLabel`, `HONESTY_NO_SESSION` (`Connects to your
ZONA and applies this to its active page. About a second.`), `honestyReady` (`Applies this to Page 2 in
about a second. It stays until power-off unless you store it.`), `KEEP_REASONS.never-tried` (`Apply to
ZONA first, then store it.`), `KEEP_REASONS.knobs-moved` (`The knobs moved since it was applied. Apply
to ZONA again first.`), `KEEP_REASONS.after-partial` (`Not after a partial apply. Apply to ZONA again,
or click Clear.`), `KEEP_REASONS.after-mismatch` (`Apply to ZONA again first, then store it again.`),
`nothingLandedBlock`'s `try` form; and `page-target.ts`'s `APPLY_LABEL` (`Apply to ZONA`), its own
ledger line. Kept by name, the probe's alone: `settledCaption`, `liveSettled`.

### Done, 2026-09-16

Three commits, no push, no device, no deploy: `58eb603` feat(install) - the store, the copy, the
zone, KeepConfirm, the bar's clause, the probe's Keep, the local README, the five node specs and the
string ledger above (the copy spec reads this file as its fifth document); `b43b0b0` test(e2e) - the
six e2e files; then this paragraph with `docs/TESTING.md`'s "2026-09-16 change 1", the runbook's
row O and dated label-map line, and the gate records `gate/change-1.*` / `gate/change-1-after.*`.

**The interpretation taken.** Every Store is one click and one action (`keep`) of three legs - the
five firmware defaults through `#ramLeg` (the same five writes Clear sends; run under the action
`keep`, not `clear`, so a failure inside a Store is a Store's failure in the zone's block and the
probe's readout, and so the click is one capture), the configuration's five through `#ramLeg`, the
page store through `#storeLeg`, the D-12 proof - landing `kept`; a RAM leg that fails lands
`partial` / `nothing-landed` / `lost` as any RAM leg does, with the classifier reading that leg's
steps alone, and stores nothing; a store leg that fails lands `kept-mismatch` / `unconfirmed` as
today. `armed` is Store's readiness (the old Z-05 meaning is `keepReason`'s already-kept row);
`keepReason` is three rows and the store is the retry from every failure, so the four rows that
pointed at Apply are retired rather than renamed; the confirmation stays on Store and closes when a
change would disable the control (over budget, the pair withdrawn, or back to the pair already
stored) but stays open across a change inside the budget - the click writes what the screen shows.
`tryOnDevice` and `settled` stay for the probe by name. The bar's `writing` clause is
`keepingLabel` ("Storing on Page N…"), since every write from a route is a store or a clear;
`settledCaption` / `liveSettled` are kept by name (the probe's, and the bar's clause for a phase the
anti-collapse test requires). The nothing-landed block has a `store` form for a Store's leg; the
header's Clear keeps the put-back form it had. `tryOnBudgetReason` says "store" and keeps its name.

**The frame sequence per Store click, observed on the fake:** 5 `CONFIG/EXECUTE` (the defaults,
verbatim, SLOTS order) · 1 `HEARTBEAT/EXECUTE` · 5 `CONFIG/EXECUTE` (the configuration) · 1
`HEARTBEAT/EXECUTE` · 1 `PAGESTORE/EXECUTE` · 5 `CONFIG/FETCH` = **18 frames, 18 steps** - the
brief's 17 omits the second leg's restore heartbeat, which `sequence.ts` sends on every RAM leg's
every path. On the shim: 10 `CONFIG/EXECUTE`, 1 `PAGESTORE/EXECUTE` per click.

**The zone as shipped:** `Target` · `Store on ZONA` (bordered, as before; nothing took Apply's
fill); `store-honesty` is Store's sr-only description (`keepLineEnabled(page)` while connected,
`HONESTY_SNAPSHOTTING` during the snapshot, `HONESTY_INCAPABLE`, else `NEEDS_ZONA`);
`store-on-zona-line` the record's three reasons; `store-refusal` the over-budget line, which
disables Store (TUNE-05: zero frames, `install.spec.ts`); the confirmation in Store's place
(SAFE-05), handed the config and the name. `keepReason`'s union: `"no-session" | "already-kept" |
"incapable"`.

**Retitled tests** (none deleted; the count term is `+0 / +0`): see `docs/TESTING.md`
"2026-09-16 change 1" for the full old -> new list - one in `install-copy.spec.ts`, seven in
`install.spec.ts`, one in `device-ui.spec.ts`, four in `e2e/install.e2e.ts`, one each in
`e2e/sandbox.e2e.ts` and `e2e/tuning.e2e.ts`.

**The census diff, by string** (`hash-strings.mjs --diff`, `gate/change-1-after.txt`): out -
`Apply to ZONA` (2 -> 0), `Applying to ${}…`, `Applies this to ${} in about a second. It stays
until power-off unless you store it.`, `Connects to your ZONA and applies this to its active page.
About a second.`, `${} first, then store it.`, `The knobs moved since it was applied. ${} again
first.`, `Not after a partial apply. ${} again, or click ${}.`, `${} again first, then store it
again.`, `Already stored on ZONA. Turn a knob and apply it again to store a new one.`, `Nothing got
through. ${} is unchanged, so your own configuration is still playing.`, `Click ${}, then ${}
again`, `Stores this on ${} so it stays after power-off.`, `This replaces what ${} holds on your
ZONA — its touch element’s Setup and Timer and the page’s own init, timer and utility scripts — and
it stays after power-off.`, `Over the 908-character budget on ${}. Turn something down to apply
it.`, the keys `never-tried` (4 -> 0), `knobs-moved` (3 -> 0), `after-partial` (3 -> 0),
`after-mismatch` (3 -> 0), the testids `apply-to-zona`, `apply-honesty`, `apply-refusal`, the class
`destination-apply`, the specifier `./install-copy` (try-on.ts's re-export); in - `Returns ${} to its
firmware default, then writes this and stores it, so it stays after power-off.`, `Already stored on
ZONA. Change something to store it again.`, `This returns ${} to its firmware default, then writes
this configuration — … — and stores it, so it stays after power-off.`, `Nothing of this reached ${},
and nothing was stored.`, `Over the 908-character budget on ${}. Turn something down to store it.`,
`no reason` (the probe's readout), the testids `store-honesty`, `store-refusal`; counts moved -
`Click ${} again` 5 -> 6, `no-session` 5 -> 8, `store` 8 -> 13, `keep` 2 -> 6, `try` 9 -> 2,
`settled` 12 -> 10, `snapshot-failed` 9 -> 11, `unconfirmed` 13 -> 10, `kept-mismatch` 9 -> 8,
`partial` 8 -> 7, `idle` 23 -> 24, `snapshotting` 6 -> 5, `clear` 8 -> 7, `button` 98 -> 97,
`${} ${} ${}` 3 -> 4. 2728 -> 2713 distinct literals.

**The gate's terms:** equal - the wire set, full and sandbox set, the fixtures, the OG, the
utilities (44 -> 44), check 655 / 0 / 0, lint, quick 94 / 966, the build, `src/` 18 modified / 0
added / 0 deleted / 0 renamed, the refuse-list empty; moved - the census, the copy exports (five
left, none joined), the testids (320 -> 319, the five named), the titles, the normalised JS, and the
SCOPED CSS - which the brief asked to hold and a control's departure cannot: proved to be exactly
`.destination-apply` and `.destination-apply:disabled` leaving `DestinationZone.svelte`, every other
selector and scope hash unchanged (`svelte/compiler` at `f304891` and HEAD under one filename;
`KeepConfirm.svelte`'s CSS byte-identical). The script exits 1 at the census by design; the later
terms were compared from the two records by hand.

**Chunks:** c1 32 (third run - the first two red on two fixes then made: `keepReason` reading
`phase` first so a `$derived` tracks the queue's arrival, and a RAM leg publishing `writing` before
any await so the zone's focus rule reads a disabled Store), c3 21, c4 15, c5 11, c2 22 (fourth run
at `--workers 1`; runs one to three red only on the known `browse:299` / `browse:343` hydration
races). Counts: quick 94 / 966 (+0 / +0), check 655 (+0), e2e 86 titles / 101 runs (+0 / +0).

**Questions for the user.** (a) Should `settled` (and `restored`) leave the union now that no route
reaches them, at the cost of the anti-collapse test's fifteen and two bar clauses? Kept by name
here. (b) Does the confirmation stay on Store? Kept (SAFE-05); it now says the default first.
(c) Should Store take Apply's filled style now that it is the bar's one write? Kept bordered.
(d) `already-kept` disables Store after a store until something changes - keep that row, or let a
second identical store go through? (e) `nothingLandedBlock`'s put-back form still renders for the
header's Clear ("Send the restore again"), as before this change; a `clear` form is one string
away. (f) `partialBlock`'s detail ("part of this configuration and part of your own") is imprecise
on a Store's second leg (the other part is the firmware default) and on a Clear, as it was before;
reworded on request. (g) The runbook's rows B-M name Apply; row O and the label-map line read them
across rather than rewriting them (append-only) - say if you want the rows rewritten.

**Not supported by the tree / departures from the brief:** 17 frames is 18 (above); the scoped CSS
cannot hold when a control with its own rules leaves (above); the defaults leg runs under the action
`keep`, not `clear` (above); `WRITABLE_PHASES`'s comment still says "TRY ON DEVICE works from here"
because `install.spec.ts` pins that text byte-for-byte and TRY does still work from there on the
probe. Next gate: amend SAFE-02 and SAFE-05 (RAM-before-flash) and PROJECT.md's "flash writes stay
separate from RAM auditions"; CAT-04 stays `[ ]`.

## 2. Store on ZONA stores on one click - no confirmation

> next: no opting when pressing ZONA, nothing opens down under storing, it just stores it with one
> click.

Reading: the inline confirmation under Store on ZONA (KeepConfirm, Phase 7's SAFE-05 - "the only
confirmation on the site") goes. One click on Store on ZONA runs the whole write: the defaults, the
configuration, the store, the proof. Clear is already one click (13.1 D-04). No control on the site
asks first; "nothing is written without a click" remains the rule.

### Done, 2026-09-16

Four commits, no push, no device, no deploy: `8e211d0` feat(install) - the store, the copy, the
zone, `KeepConfirm.svelte` deleted (`git rm`), the workspace route's Escape handler, the probe's
one Keep, the clause's comment and the four node specs; `459deed` test(e2e) - the two e2e files;
`ad3f72e` fix(install) - the focus rule re-aimed at the one click (below); then this paragraph with
`docs/TESTING.md`'s "2026-09-16 change 2", the runbook's dated label-map line, and the gate records
`gate/change-2.*` / `gate/change-2-after.*`.

**The interpretation taken.** `keepOnDevice(config, name)` runs from the click and carries the
refusal `openConfirm()` used to make: `if (!this.armed) return; if (this.keepReason(this.#capable())
!== undefined) return;` - so a click while disarmed or while the record names a reason writes
nothing and moves no phase, and from `snapshot-failed` the click still re-reads first. `confirmOpen`,
`openConfirm()`, `dismissConfirm()` are gone with the three branches that closed the confirmation
(the detach, `observeConfig`, `requestPage` - which returns the target's answer straight). The probe
does not use them: its `Keep on device` calls `keepOnDevice(pair(), NAME)` and `Keep, yes` / `Not
now` / the `confirm open` readout left. The write itself is unchanged: the same three legs under
one action `keep`, **18 frames per Store on the fake** (5 `CONFIG/EXECUTE` · 1 `HEARTBEAT/EXECUTE` ·
5 `CONFIG/EXECUTE` · 1 `HEARTBEAT/EXECUTE` · 1 `PAGESTORE/EXECUTE` · 5 `CONFIG/FETCH`), 10
`CONFIG/EXECUTE` and 1 `PAGESTORE/EXECUTE` per click on the shim; `WRITE_CLICKS` is still three
(the affirmative was never in the list - verified). "Nothing is written without a click" holds.

**The zone as shipped:** `Target` · `Store on ZONA` - a plain `<button type="button">`, bordered
as before, `onclick` -> `install.keepOnDevice(config, name)`, disabled on `!armed`, a reason, the
refusal, measuring or `writing`; nothing renders in its place in any phase (`device-ui.spec.ts`
renders every phase and finds Store itself, no `role="group"`, no dialog). **The line under Store:**
`store-honesty`, Store's sr-only description, is `keepLineEnabled(page)` while connected -
`Returns Page 2 to its firmware default, then writes this and stores it, so it stays after
power-off.` - which already carried the one-click fact from change 1, so no new sentence was written
and `confirmReplaces`'s sentence was not reused (it named the touch element, which SAFE-05 alone
required); `store-on-zona-line` is the record's three reasons, `store-refusal` the over-budget line.
**The focus rule moved rather than left:** the brief read 13.1-07's rule as moot and its trigger (a
confirmation leaving) is, but the first shim run of the rewritten e2e title logged `focus after the
click sits on BODY` - Store disables in the click's flush and Chromium drops a disabled button's
focus on the body, the keyboard regression 13.1-07 fixed under Rule 2. So `store()` reads whether
Store held focus, calls the write, and focuses the zone (`tabindex="-1"` back on the root) when the
store published `writing` in that flush; synchronous, because an `$effect` over `writing` ran after
the browser had moved focus (measured red on the shim). From `snapshot-failed` the click awaits the
re-read first and that path's focus is not held - named, not fixed.

**Retired by name** (`install-copy.ts`'s ledger, "THE STORE CONFIRMATION'S STRINGS ARE RETIRED BY
NAME, 2026-09-16"): `NOT_NOW_LABEL` (`Not now`), `confirmCaption` (`Store this on ZONA · Page N?`),
`confirmReplaces` (`This returns Page 2 to its firmware default, then writes this configuration —
its touch element’s Setup and Timer and the page’s own init, timer and utility scripts — and stores
it, so it stays after power-off.`), `CONFIRM_WAY_BACK` (`Clear still returns the page to its
firmware default.`), `confirmRig` (`Your EN16 and BU16 are on the same cable. Their current pages
are stored too, because the store reaches every module at once.` and its singular) with its
formatter `moduleList`. Kept by name: `KEEP_LABEL` (Store's own). No string written. SAFE-05 ("the
only confirmation on the site") and SAFE-06 (the confirmation naming the other modules) are retired
by the user's word; the next gate amends them, with change 1's SAFE-02 and PROJECT.md line.

**Retitled and deleted tests** (`docs/TESTING.md` "2026-09-16 change 2" for the full old -> new
list): two in `install.spec.ts` (the rig title; the flash-only-what-you-have-heard title), one in
`install-copy.spec.ts` (the formatters), one in `device-ui.spec.ts` (the zone), one in
`e2e/install.e2e.ts` (test 8, rewritten under a new title: the confirmation's absence, Enter as the
write, the zone holding focus, and a rig's one-click store), one in `e2e/sandbox.e2e.ts`. **No test
deleted:** `device-ui.spec.ts` has no standalone KeepConfirm test - the brief's "test 8" is the zone
test (the file's eighth `it`), which held the no-trap assertions and is retitled with their
absence asserted; the count term is `+0 / +0` for vitest (94 / 966) and `+0 / +0` for e2e (86
titles / 101 runs); check is 655 -> 654, `-1` for the deleted component.

**The census diff, by string** (`hash-strings.mjs --diff`, `gate/change-2-after.txt`): out - `Store
this on ZONA · ${}?`, `This returns ${} to its firmware default, then writes this configuration — …
— and stores it, so it stays after power-off.`, `${} still returns the page to its firmware
default.`, `Your ${} is on the same cable. Its current page is stored too, …`, `Your ${} are on the
same cable. Their current pages are stored too, …`, `${} and ${}` (moduleList's join), `Not now`
(2 -> 0), `Keep, yes`, `confirm open`, the ids `${}-caption` / `${}-replaces` / `${}-way-back` /
`${}-rig`, the classes `confirm`, `quiet-control`, `secondary pill`, `body quiet` 4 -> 2, the
testids `keep-confirm` (2 -> 0), `keep-confirm-yes`, `keep-confirm-no`, `store-confirm`,
`install-confirm`, `install-keep-yes`, `install-keep-no`; counts moved - `button` 97 -> 93,
`Escape` 10 -> 8, `keydown` 4 -> 2, `group` 7 -> 6, `module` 2 -> 1, `body` 14 -> 13, `caption`
6 -> 5, `actions` 6 -> 5, `, ` 7 -> 6, `${} ${} ${}` 4 -> 3; in - nothing. 2713 -> 2690 distinct
literals, 192 -> 191 files; the testid set 319 -> 312 (the seven named); the copy exports: six
left, none joined.

**The gate's terms** (`--before change-2` at `93ddc6b`, `--after change-2 --against change-2
--check 654` at `ad3f72e`): equal - the wire set `a24b256f…`, full `514cb2c7…`, the sandbox set
`40b44316…`, the fixtures' four hash-objects, the OG (26 files, 154136 B, `2a9ccf80…`), the
utilities (44 -> 44, the five markup-named intact), lint, quick 94 / 966, the build, the
refuse-list `--stat` empty; moved - check 655 -> 654, the census, the copy exports, the testids, the
titles (967 vitest and 101 playwright either side), the normalised JS, the SCOPED CSS `e8841c5f…` ->
`661eab32…` (a `.svelte` file leaving must move it: `KeepConfirm.svelte`'s stylesheet is gone, and
`DestinationZone.svelte` compiled with `svelte/compiler` at `93ddc6b` and at HEAD under one filename
differs by exactly `.confirm.svelte-6l3wua { min-inline-size: 0; }` leaving, the scope hash and
every other selector unchanged), the name-status of `src/` 10 modified / 0 added / 1 deleted
(`KeepConfirm.svelte`) / 0 renamed. The script exits 1 at the census by design; the later terms were
compared from the two records by hand. The first `--after` run read the quick suite against the
build the tree had before the change (the script runs the suite before it builds; `radius.spec.ts`
layer B refuses a stale build by design) and was rerun on the fresh build.

**Chunks** (`scripts/gate/e2e-chunks.sh`, fresh detached servers, stopped through PowerShell, HTTP
000 after each): c1 32 (first run at `ad3f72e`; `install.e2e.ts` alone ran twice before that
commit - red once on the `$effect` form of the focus rule, green on the handler form), c2 22 on the
third run at `--workers 1` (run one red on `browse:343`; run two on `browse:299` / `:343` / `:1186`
/ `:1404` - the known hydration races and one more of their shape at `:1186`; the browse files are
untouched by this change), c3 21, c4 15, c5 11. Counts: quick 94 / 966 (+0 / +0), check 654 (-1),
e2e 86 titles / 101 runs (+0 / +0).

**Questions for the user.** (a) On a rig, the fact that a store reaches every module on the cable
(SAFE-06's sentence) is said nowhere now - should it join Store's sr-only description when the
identity lists other modules, or stay retired? Retired here. (b) The focus rule: the zone takes
focus after a keyboard commit (as before, when the confirmation left) - keep, or let the browser
drop it? Kept. (c) `Store on ZONA`'s description is sr-only; with the confirmation gone no visible
sentence says what the click does before it is clicked - is a visible line under Store wanted?
None added. (d) Change 1's questions (a) to (g) still stand.

**Not supported by the tree / departures from the brief:** `device-ui.spec.ts` has no standalone
KeepConfirm test to delete (above), so the vitest delta is `+0`, not `-1`; the focus rule was
re-aimed rather than removed (above, measured); `confirmRig` and `moduleList` were retired beyond
the brief's `CONFIRM_*` and two labels because they were the confirmation's alone (SAFE-06 named);
the workspace route's window Escape handler left whole because nothing remained for it to do. Next
gate: amend SAFE-05 and SAFE-06 (this change), SAFE-02 and PROJECT.md's line (change 1); CAT-04
stays `[ ]`.

## 3. The "Your ZONA didn't confirm the store" block goes

Screenshot (attached): after a Store on ZONA that worked on the bench, the bar showed the
`unconfirmed` block - "Your ZONA didn't confirm the store / Arc is still running on Page 4 in
memory. No confirmation of the store came back, so HANGAR can't say whether it survives power-off.
/ 1. Click Store on ZONA to send the store again / 2. Or click Clear to return Page 4 to its
firmware default".

> remove this, this is not a t rue bug report, it works fine

Reading: the store's acknowledgement gate reports a failure the hardware does not have - the page
stored and survives power-off. The `unconfirmed` outcome for a store leaves the interface: a Store
whose writes were acknowledged lands `kept` when the read-back matches or when the store's
acknowledgement is simply late or absent; only a read-back that returns DIFFERENT bytes still says
so (kept-mismatch). SAFE-07's "acknowledged before done" is retired for the store leg by the user's
word; the next gate amends it.

### The strings change 3 wrote and retired, verbatim (D-05's register)

Rewritten, in `src/lib/device/install-copy.ts`; the sample page is wire 1, read as `Page 2`:

- `keptMismatchBlock.detail` (I.5.2 rewritten - the leg no longer waits on the acknowledgement,
  so the sentence names only what HANGAR read):
  `Reading Page 2 back after the store gave something different. HANGAR won’t call that stored.`
  ~~`Your ZONA acknowledged the store, but reading Page 2 back gave something different. HANGAR won’t call that stored.`~~

Retired by name (the ledger block in `install-copy.ts`, "THE UNCONFIRMED STORE'S STRINGS ARE
RETIRED BY NAME, 2026-09-16"): `UNCONFIRMED_TITLE` (`Your ZONA didn’t confirm the store`),
`unconfirmedBlock` (detail `Arc is still running on Page 2 in memory. No confirmation of the store
came back, so HANGAR can’t say whether it survives power-off.`; steps `Click Store on ZONA to send
the store again`, `Or click Clear to return Page 2 to its firmware default` - the shared step stays
as `stepOrClear`), `FIRMWARE_DEFAULT_NAME` (`The firmware default`, the name a clear's unconfirmed
row read). Kept by name: `restoredUnconfirmedBlock` and `RESTORED_UNCONFIRMED_TITLE` (the probe's
put-back on a read-back that never matches, and the discard).

### Done, 2026-09-16

Four commits, no push, no device, no deploy: `42892f8` feat(install) - the store, the copy, the
zone, the bar's clause, the probe's comment, the four node specs and the string ledger above (the
copy spec reads this file as its fifth document); `15dcdff` test(e2e) - the probe's sixth title
re-aimed; `1e78f4b` refactor(install) - the clause's header back at ten lines; then this paragraph
with `docs/TESTING.md`'s "2026-09-16 change 3", the runbook's dated label-map line, and the gate
records `gate/change-3.*` / `gate/change-3-after.*`.

**What `grid_decode.c` says** (`:940-1000`, read only). The `PAGESTORE/ACKNOWLEDGE` is built and
broadcast in one place - `grid_protocol_nvm_store_success_callback` (`:939-960`), the completion
callback of the bulk store, so it is sent only once the NVM write has finished; the callback then
starts a page reload (`:958`), itself a bulk operation. `grid_decode_pagestore_to_ui` (`:962-1000`)
drops a `PAGESTORE/EXECUTE` that arrives while any bulk operation is in progress with `return 1` and
no reply at all (`:979-981`) - no NACK exists for the store. So the acknowledgement is conditional
(an idle module, a write that finishes inside 3000 ms), it is matched per attempt by request id
(a late ACK for attempt 1 matches nothing once attempt 2 minted a new id), and attempts 2 and 3
land during the reload the store itself started and are dropped - which lands `unconfirmed` with
the page genuinely in flash. One full explanation of the screenshot; not verified on hardware here.

**The interpretation taken.** The acknowledgement is not waited on for the outcome: `#storeLeg`
runs the request to its bound (three attempts at `pagestoreMs`, about 9.4 s when nothing answers -
the still-writing line at 2 s), catches its timeout, and proceeds to the proof as after an
acknowledgement - the heartbeat, then up to three rounds of the five fetches, a round whose fetch
is unanswered or refused skipped for the next. **Sub-cases:** (A) acknowledged, read-back matches -
`kept` (unchanged); (B) acknowledged, different bytes in every completed round - `kept-mismatch`
(unchanged); (C) acknowledgement late or missing, read-back matches - **`kept`** (was unconfirmed);
(D) late or missing, different bytes - **`kept-mismatch`** (was unconfirmed); (E) no round completed
at all - **`kept`** by the user's word, the comment in `#storeLeg` citing this section and
`grid_decode.c:939-960` / `:979-981` (was unconfirmed). `clearToDefault()` runs the same leg:
(A)/(C)/(E) `cleared`, (B)/(D) `kept-mismatch`; `FIRMWARE_DEFAULT_NAME` had no other reader and is
retired. `putBack()` (probe-only) keeps `restored-unconfirmed` for a read-back that never matches
(cause `mismatch`), and the discard still lands it on its own timeout or NACK - kept by name.

**`unconfirmed` left the union.** Nothing lands it, so the fifteen-phase union is fourteen,
`WRITABLE_PHASES` is 13-12's list less one, `UNCERTAIN_PHASES` three, the zone's mapping six cases,
the bar's clause and tone lose their case; the anti-collapse test reads fourteen / six / three and
asserts the name absent, its comment naming the retirement as the user's (13-18 kept six on purpose;
this retires one by the user's word). **Copy:** `UNCONFIRMED_TITLE`, `unconfirmedBlock` and
`FIRMWARE_DEFAULT_NAME` retired by name (the ledger above); **one string rewritten beyond the
brief** - `keptMismatchBlock.detail` claimed "Your ZONA acknowledged the store", false in sub-case
(D), and says what HANGAR read now (the ledger above, old form struck). `restoredUnconfirmedBlock`
kept by name. The testid set is unmoved (312). The frame sequence per Store is unchanged (eighteen
on the fake when the ACK comes; 12 + 3 attempts + 5 fetches when it never does).

**Retitled tests** (none deleted; the count term is `+0 / +0`): five in `install.spec.ts` (the
late-acknowledgement title; the clear's title in three parts; the put-back's; the enablement table;
the phase-list pin), one in `device-ui.spec.ts`, one in `install-copy.spec.ts`, one in
`e2e/install.e2e.ts` - the full old -> new list in `docs/TESTING.md` "2026-09-16 change 3". The
brief's second title ("different bytes land kept-mismatch") is the clear title's second part, not a
standalone test: the acknowledged-and-different case already had one.

**The census diff, by string** (`hash-strings.mjs --diff`, `gate/change-3-after.txt`): out - `${} is
still running on ${} in memory. No confirmation of the store came back, so HANGAR can’t say whether
it survives power-off.`, `Click ${} to send the store again`, `The firmware default`, `Your ZONA
acknowledged the store, but reading ${} back gave something different. HANGAR won’t call that
stored.`, `Your ZONA didn’t confirm the store`, the key `unconfirmed` 10 -> 0; in - `Reading ${}
back after the store gave something different. HANGAR won’t call that stored.`; counts moved -
`kept` 23 -> 24, `mismatch` 9 -> 6, `timeout` 11 -> 8, `fetchAll` 1 -> 2. 2690 -> 2685 distinct
literals; the copy exports two left, none joined.

**The gate's terms** (`--before change-3` at `227aba1`, `--after change-3 --against change-3
--check 654` at `1e78f4b`): equal - the wire set `a24b256f…`, full `514cb2c7…`, the sandbox set
`40b44316…`, the fixtures' four hash-objects, the OG (26 files, 154136 B, `2a9ccf80…`), the
testids `cebf17b5…` (312), **the SCOPED CSS `661eab32…` and the raw CSS `fc20dd55…`** (no component
left), the utilities 44 -> 44, check 654 / 0 / 0, lint, quick 94 / 966, the build, the refuse-list
`--stat` empty; moved - the census `e27b7442…` -> `40660d18…`, the copy exports `18e53279…` ->
`25468f7e…`, the titles `61c1f546…` -> `5b5a92bb…` (967 vitest and 101 playwright either side), the
normalised JS `23e3bfa5…` -> `d315d6dc…`, `src/` 8 modified / 0 added / 0 deleted / 0 renamed. The
script exits 1 at the census by design; the later terms were compared from the two records by hand.

**Chunks** (`scripts/gate/e2e-chunks.sh`, fresh detached servers, stopped through PowerShell, HTTP
000 after each): `install.e2e.ts` alone first, 16 passed (36 heartbeats per leg on the re-aimed
title); c1 **32** (first run; `install:1957`, A.21's CLEAR title, green on both engines at 4 and 5
heartbeats - this change leaves its CONFIG acknowledgements alone and it did not flake), c2 **22** =
21 in the chunk at `--workers 1` twice (`browse:343` the one red both times, the known hydration
race; run one at three workers red on `:299` / `:343` / `:1404`; the browse files untouched) plus
`:343` green alone on a fresh server, c3 **21**, c4 **15**, c5 **11** on its second run (the first
run's one red was `artifacts.e2e.ts:63`'s stamp check against a HEAD that had moved under the two
source commits made after the build - a harness fact; green on the gate's fresh build). Counts:
quick 94 / 966 (+0 / +0), check 654 (+0), e2e 86 titles / 101 runs (+0 / +0).

**Questions for the user.** (a) Sub-case (E) - the store acknowledged or not, and then no fetch
answered inside three rounds - lands `kept` by your word as the brief read it; `kept-mismatch` would
be the other honest landing, and its sentence would be false there. Confirm `kept`, or say which.
(b) The wait: when the acknowledgement never comes, the bar reads `Storing on Page N…` for about 9.4 s
(three attempts at 3000 ms) before the proof begins and the store lands. Cutting the attempts to one
would land in about 3 s but is a queue-wide setting (`RETRY_ATTEMPTS`) or a descriptor change
(`descriptors.ts`, untouched by the rules); say if the wait matters. (c) `keptMismatchBlock.detail`
was reworded because it claimed an acknowledgement (above) - keep the new sentence, or supply one.
(d) Changes 1 and 2's questions still stand.

**Not supported by the tree / departures from the brief:** the hardware cause is argued from
`grid_decode.c`, not measured (no device); the brief's "different bytes land kept-mismatch" title is
a part, not a test (above); `keptMismatchBlock.detail` rewritten beyond item 2 (above);
`FIRMWARE_DEFAULT_NAME` retired beyond the two names the brief listed because nothing read it once
the block left; a fourth commit (`1e78f4b`) for the clause's header, which the first cut had at
eleven lines. Next gate: amend SAFE-07 ("acknowledged before done", the store leg) - this change;
SAFE-05 and SAFE-06 (change 2); SAFE-02 and PROJECT.md's line (change 1); CAT-04 stays `[ ]`.

## 4. A Trackpad variation with a comet trail (2026-09-17)

> next: make a variation of the Trackpad where everything stays the same but for the visuals/LED
> animations we use a cometlike trail that follows your finger and fades

Reading: a new catalog card beside Trackpad - the same recipe (pointer through gmms, two-finger
scroll, tap-to-click, right-click on two) with the edge flash replaced by a comet: the LEDs the
finger passes light under it and fade behind it. Painted from the Timer like the flash, through
the library's decaying stamp K (12.1-08b - the presets' own comet), so the Setup stays at its 903.
Provisional id `trackpad-comet`, name "Trackpad comet" - the user may rename.

### Done, 2026-09-17

Three source commits, no push, no device, no deploy: `3e7e780` feat(catalog) - the card, the
catalog's wiring, the demo path, `frames.json`, the six specs whose counts moved and audition row
29; `7e24c78` test(sim) - the VM proof; `f2f9a16` test(share) - the stamp sweep's colour exemption
28 -> 30; then this paragraph with `docs/TESTING.md`'s "2026-09-17 change 4" and the gate record
`gate/change-4-after.*`. **Id `trackpad-comet`, name "Trackpad comet"** - provisional, the user's to
rename; appended after TRACKPAD, 27 = 8 + 19.

**The two costs at the RGB444 picker corner**, measured under the pinned `compressScript` after
`initLuaFormatter()` over all 96 knob states: **Setup 903** - TRACKPAD's string byte for byte, proved
by `toBe` in `lua-smoke.spec.ts`, no knob token in it, 5 free, `s.u,s.v=f,h` carried unread; **Timer
427** at the corner (both colours 255,255,255, tail 42, scroll `false` - the worst of the 96), 424 at
the defaults, 481 free; every state a fixed point passing `checkSyntax`. Both Phase 11 gates: the
recipe's three guards declared for the new id in `touch-guard.spec.ts` (nothing on code 9); no `Q`,
so the Q-then-G order does not arise. Shape `9`, format `w`.

**The shape, and where it departs from the brief.** The brief proposed the library's `K` (the
presets' comet) stamped from the Timer. Measured first at 306 at the corner and driven in the VM: a
stamp re-written every 20 ms at the cell's falling bilinear weight re-stamps a cell the finger is
leaving DOWN to its last small start (6 or 12, gone in a tick or two), so a slow finger left almost
no trail. The shipped Timer is GHOST's comet shape instead: `G(s,i,1,x,y,0,@H)` draws the bilinear
finger on layer 0 and `D(N(x,y),1,@T*6)` re-arms the nearest calibrated cell on layer 1 at full,
for every contact in `s.p` while `s.q<26 and(@S or s.n<2)`; blocks of contacts not drawn are cleared
through `V` by the Timer's own sweep over `B`. A left cell decays from 252 at any speed; the
head's softness is `G`'s; the layers add in the firmware's mix (one layer capping at 254/512), so the
head reads brighter than the trail. The recipe's idle window (`s.q<26`) is the comet's: a lost lift
fades inside 500 ms + the tail rather than glowing forever.

**The knobs (four, all in the Timer; TUNE-01's three to six):** `colour` Trail colour `@C` (four
swatches, the picker reaches the lattice); `head` Head colour `@H` (the same four, the same lime by
default - the "head" the brief floated as the stamp's peak is impossible with `K` or `D`, whose peak
IS the tail length, so it is the head's colour); `tail` Tail length `@T` 42 / 31 / 21 ticks (`*6` in
the Lua, every start a multiple of six inside `D`'s ceiling); `scroll` Comet on scroll `@S` `true` /
`false` (two comets under a scroll, or none, as TRACKPAD's flash). No token a prefix of another.

**The VM proof** (`lua-smoke.spec.ts`, the 39th test): the Setup compared to TRACKPAD's; one gesture
script (drag, lift, tap, fast tap, two-finger tap, two-finger scroll) run on both entries, the HID
logs equal call for call (32 calls: gmms 25, gmbs 7); a landed finger drawn as `G`'s twin from KX /
KY over the raw pair with its nearest cell armed at 252; a drag's trail rising toward the head
(162, 198, 246); the head cleared inside one Timer call of the lift and the trail black after 38 more
ticks with every cell on 0 on both layers; a still finger held at 47 ticks and gone past the 25-call
idle window with the finger down; tail 21 black after 16; scroll `true` lights both fingers' cells
and two heads, `false` lights nothing, the notches equal either way.

**Counts, carried + delta:** catalog 26 -> 27 (8 + 18 -> 8 + 19); quick 94 / 966 -> **94 / 967**
(+0 / +1), green twice at `--maxWorkers=2`; check 654 -> **655** (+1 file), 0 / 0; lint clean; sweep
`4 19` -> **`4 19`** green (`lua-entries` 1,140 -> 1,201 combinations; the member list unmoved -
the brief's `4 20` does not happen, no sweep test is per entry); audition rows 28 -> **29**; e2e 86
titles / 101 runs -> **86 / 101** (+0 / +0); OG 26 -> **27** files (`trackpad-comet.png` 4,506 B,
sha256 `a79a6387…`, 5 of 81 lit; 154136 -> 158642 B); `frames.json` `a581ef4c` -> `5166ff6c`, only
the new block added, byte-identical on a second regeneration. Specs moved: `touch-guard` 4 -> 7
rows, `colour-picker` `two` 6 -> 7, `filter.spec` entries 27 / `pointing` 3 / `precise` 8 / `still`
7 (with `facets.ts`'s parentheticals), `demo.spec` four paths, `audition.spec` 29,
`stamp-roundtrip.sweep` `exempted` 30; `surprise`, `wire-pin`, `reachability.sweep`,
`catalog.spec`, `frames.spec`, `aesthetic.spec` pin nothing this moves and stand as they were.

**The gate's terms** (`--after change-4 --against change-3-after --check 655` at `f2f9a16`; no
`--before change-4` - change 3's after-record at `1e78f4b` IS the baseline, `git diff --stat
1e78f4b e57e126 -- src scripts` empty, and the script accepted it by name): equal - the sandbox set
`40b44316…`, the three other fixtures, **the SCOPED CSS `661eab32…` and the raw `fc20dd55…`**, the
utilities 44 -> 44, the copy exports `25468f7e…`, the testids `cebf17b5…` (312), check 655, lint,
the build, the refuse-list `--stat` empty; moved - the wire set `a24b256f…` -> `3f2531f7…` and full
`514cb2c7…` -> `df9345d9…` with **1,761 records byte-identical, 0 moved, 0 removed, 31 added**
(`hash-wire.mjs` per-string, the JSON diffed), the census `40660d18…` -> `b251cbba…` (2,685 ->
2,699 literals, additions and count rises only), the titles `5b5a92bb…` -> `a0e65d2d…` (one added,
one retitled: "twenty-eight" -> "twenty-nine"), the JS `d315d6dc…` -> `b4a972a3…`, `src/` 13
modified / 1 added / 0 deleted / 0 renamed. The script exits 1 at the wire by design; the later
terms compared from the two records. Its own quick run had one red - `install.spec.ts`'s snapshot
test timing out on identification at 2.95 GB free straight after the sweep, the known load flake -
against the two green counted runs.

**Chunks** (fresh detached servers on 4173, stopped through PowerShell, HTTP 000 after each; the
user's 5173 untouched): c4 **15 passed** (catalog, fidelity, first-experience's hero, library,
sandbox); c2 18 / 4 at three workers (`browse:299`, `:343`, `:1263`, `:1404` - the grid read before
hydration; the count line reads 27), **22 passed** at `--workers 1` on a fresh server, the split
change 3 recorded. c1, c3, c5 not run - no file in them reads the catalog's length as a literal.

**Questions for the user.** (a) The name and id - "Trackpad comet" / `trackpad-comet` are
provisional. (b) The fourth knob, the head's colour - keep (white head over a lime trail is the
classic comet), or three knobs with one colour. (c) The comet under a two-finger scroll defaults ON -
say if the scroll should stay dark like TRACKPAD's flash. (d) `pointing` now carries three and sits
last in the FOR row after `shortcuts` at two; the row's own rule is "descending by carriers" - move it
before `shortcuts` (one line, a toolbar change) or leave it. (e) In the VM a still finger's comet
goes dark after the recipe's 25 quiet Timer calls (500 ms), because a still finger sends nothing;
on the module a resting finger wobbles every sample (Q1) so the head should stay - row 29(b) asks.
(f) Changes 1 to 3's questions still stand.

**Not supported by the tree / departures from the brief:** `K` alone does not make a comet at a slow
finger's speed (above), so the Timer is `G` + `D(N)`, TRACKPAD's edge-flash `D` and GHOST's shape,
not the presets' `K`; the `head` knob is a colour, not a peak; no `--before change-4` record (above);
the sweep's member list stays `4 19`, not `4 20`; the audition's cost table was re-padded by prettier
(twenty lines, no text moved) beside the append; `docs/entries/trackpad-comet.md` not written (no
history yet); ROADMAP / REQUIREMENTS / STATE untouched; CAT-04 stays `[ ]`.

## 5. A brightness setting, Playground and Sandbox, 1 to 255 (2026-09-17)

> next change: add brightness setting to all playground mode and sandbox mode. lowest brightness 1
> highest 255

Reading: one LED brightness setting for the module's whole light output - a control in the
workspace's inspector for every Playground card and in the Sandbox's inspector for a surface -
integer 1..255, 255 the full output the entries have today. It is a setting of what gets stored,
not of the preview only: the brightness reaches the ZONA. How it reaches the wire is research
first (a firmware LED intensity call, or one library global the painters scale by, or the colours
scaled at landing) - the cheapest mechanism that does not move any entry past 908; measured, not
sketched.

### Done, 2026-09-17

Seven source commits, no push, no device, no deploy: `df8d386` feat(catalog) - the scaler and its
gate; `5b52611` feat(tune) - the tuner lands at a brightness on both routes, the preview dimmed;
`dbd6b87` feat(sandbox) - the region rows scaled, the records carry the field; `6bcc627` feat(ui) -
the field under Appearance on both routes; `e8fb3bc` test(e2e) - EUCLID at 128 on the fake ZONA,
the surface's field; `b9ef5e8` docs(audition) - row 30; `164df23` fix(sim) - the gate's harness;
then this paragraph with `docs/TESTING.md`'s "2026-09-17 change 5" and the gate records
`gate/change-5.*` and `gate/change-5-after.*`.

**The mechanism, measured.** The firmware has no brightness: `grid_protocol.h:236-287` lists the
whole LED API (`glc glx gld gln glp glt glf gls glpfs glag`, `glr glg glb`) and `grid_led.c:408-463`
sums `colour * alpha` per layer over 512 with nothing scaling the sum. Two candidates: **(b) a
library global** - `LB=255` costs 7 of 255/0's 66 free, scaling inside `G` is +24 and inside `K`
+24, 48 against 255/6's 35 free (G alone fits, K does not), and it cannot reach the `glc` calls the
entries make themselves or the compiler's output; a wrapper of `glc` in 255/0 measures 69-75 (74
made safe against the page-load re-run) against 66 free, before `gld/glx/gln`. **(a) at landing,
chosen**: `src/lib/catalog/brightness.ts` rewrites every colour argument of every painter call
(`glc gld glx gln`, the library's `G` and `K`, five entries' own painters and palettes declared by
id - CULL, LUMEN, QUADRANT's tables, SNAKE's `P`, POMODORO's `I`) and every palette table: a channel
`max(1, floor(v*b/255))` (0 stays 0, a dim colour never vanishes), a linear form's coefficient
(`255-j*85`, `f*80`, `lo+d*x//8`) a plain floor, so no form goes negative at any brightness. One
scanner over every producer: a Lua entry's rendered pair, a preset's compiled pair (the compiler at
its own Full), the Sandbox's region rows through `regionRow`. A scaled number never gains a digit,
so no string grows; the picker corner at 255 stays the worst case. **Coverage**: 19 Lua entries at
hash-wire's sampling (519 states, 7,119 colour arguments) and 8 presets (345 states, 2,271), none
unreachable; the whole Lua cross-product (232,824 states, 2.57 million arguments) measured once,
the same.

**The two tightest at the picker corner, 255 / 128 / 1:** Trackpad Setup 903 / 903 / 903 (no
colour in it), Timer 510 / 510 / 504; Chorus 822 / 820 / 803, Timer 29; Console 788 / 788 / 759.
Every scaled string passes `checkSyntax`.

**Where it lives**: `Surface.brightness?` and `PlaygroundRecord.brightness?` (1..255, absent = 255;
older records unchanged; 255 written as absence; the export file carries it; 0 / 256 unreadable);
the stamp does not (a shared link lands at 255; `y` / `z` reserved); a dimmed Playground copy
opens through `?from=<record id>` beside its stamp, the Sandbox's shape. **The control**: `Brightness`
under Appearance on both routes (the workspace's section is unconditional now; the Sandbox's with or
without a selection, `One brightness for the whole surface, every element included.` beneath,
read-only in Play), a typed field 1..255 refused inline with `Brightness is 1 to 255.` or `Type a
whole number.`, the last good value kept, the changed marker and `Reset Brightness`, 44px, square;
Reset settings puts it back with the knobs; Randomize draws knob indices only and cannot reach it.
**The preview**: a Lua entry and a surface run the scaled bytes in a fresh VM (exact); a preset's
PadSim frame is scaled by the same rule, within one unit of the wire on 11,664 bytes across the
eight (lua-parity 7); EUCLID's brightest channel 135 -> 70 in the browser.

**The wire, above all: `hash-wire --full --sandbox` at 255 byte-identical to HEAD's** - the set
`3f2531f7…`, the full `df9345d9…`, the sandbox set `40b44316…` - and on the fake, Store at 128 lands
`scaleLua`'s string byte for byte and the read-back proof compares what was sent.

**Counts, carried + delta:** quick 94 / 967 -> **95 / 978** (+1 / +11), green three times at
`--maxWorkers=2`; check 655 -> **658** (+3), 0 / 0; lint clean; sweep `4 19` green (1,201
combinations; 44,846 states, worst 906); e2e 86 / 101 -> **88 / 103** (+2 chromium titles); audition
rows 29 -> **30**; OG 27 files unmoved; fixtures unmoved. Specs: `brightness.spec` new (5),
`model.spec` 14, `lua-parity` 7, `emit.spec` 6, `transfer.spec` 5, `tune-ui.spec` 13 (components 11,
the census gains the field at 0, the error-ink carriers gain it), `sandbox-ui.spec` 9,
`audition.spec` thirty, `config-shape` a third permitted catalog path.

**The gate's terms** (`--before change-5` at `b5432db`, `--after change-5 --against change-5 --check
658` at `164df23`): equal - **the wire set, the wire full, the sandbox set**, the four fixtures, the
OG (27 / 158,642 B / `f60a6363…`), the utilities 44 -> 44, check 658, lint, the build, the refuse-list
`--stat` empty, no rename, no deletion; moved as a feature moves them - the SCOPED CSS `661eab32…`
-> `7f88b4f4…` (BrightnessField.svelte gained rules), the census `b251cbba…` -> `e4f23375…` (2,699 ->
2,721), the copy exports (three), the testids 312 -> 317 (the field's five), the titles 968 -> 979 /
101 -> 103, the JS 71 -> 72 files, `src/` 24 modified / 3 added. The script exits 1 at the census by
design; the first `--after` run's wire term was empty because Node's type stripping refused a
parameter property in `engine.ts` - `164df23` fixed it and the record above is the second run.

**Chunks** (fresh detached servers on 4173, stopped through PowerShell, HTTP 000 after each; the
user's 5173 untouched): c1 **33 passed** (+1), c4 **16 passed** (+1), c3 **21 passed**; no rerun.

**Questions for the user.** (a) THE PRESET KNOB: the eight preset cards already carried the
compiler's five-step Brightness rail under Behavior (15 / 30 / 50 / 75 / 100, in the stamp, rolled by
Randomize); the new field composes with it (a quarter at Half and 128). Retire the knob for one
1..255 setting everywhere - the wire set moves by its 64 records, D-01's three-knob floor drops
STARFIELD and FOUR FADERS to two, `c`-format links land at Full - or keep both. (b) Reset settings
resets the brightness with the knobs - say if it should leave it. (c) LUMEN's sysex reports the
dimmed colour at a brightness under 255 - say if it should report the palette's. (d) The Sandbox's
field sits under Appearance with or without a selection - say if you want a surface section in the
rail instead. (e) Changes 1 to 4's questions still stand.

**Not supported by the tree / departures from the brief:** "every colour triplet" does not describe
six entries (palettes, local painters, morph's arithmetic), so the scaler has a linear-form rule and
five declarations with a coverage gate; a preset is scaled on the compiler's output, not the PadState
(the per-finger hues and the fader palette are coefficients inside `src/vendor/`, untouched), and its
preview is the dimmed frame; Reset settings resets it; the quick spec samples the cross-product; the
preset knob above.

### 5b. The presets' own brightness knob goes (2026-09-17)

Asked "one 1..255 Brightness everywhere (retire the compiler's five-step knob on the eight presets)
or both", the user answered:

> one

The compiler's `brightness` knob (15 / 30 / 50 / 75 / 100 %, in the BOTOR stamp, rolled by
Randomize) leaves the eight preset cards; the field is the one brightness. The wire set moves by
those cards' records; a shared `c`-format link whose vector carried the knob lands at full; D-01's
three-knob floor on STARFIELD and FOUR FADERS is amended by the user's word at the next gate.

#### Done, 2026-09-17

One source commit, no push, no device, no deploy: `cade81b` feat(tune) - the knob retired, the pin,
the nine specs that moved; then this paragraph with `docs/TESTING.md`'s "5b, the same day" under
the change-5 section and the gate record `gate/change-5b-after.*` (taken against
`gate/change-5-after.*`).

**Where the pin lives.** `presetKnobs` (`src/lib/tune/knobs.preset.ts`) appends nothing now, and
`BRIGHTNESS_KNOB_ID`, `brightnessKnob`, `BRIGHTNESS_OPTIONS` and `stepForBrightnessIndex` leave by
name under a dated `RETIRED BY NAME` block. **The pin is `baseStateFor` in
`src/lib/tune/state.ts`** - the one door HANGAR opens a preset's `PadState` through (`model.ts`'s
`stateOf` and `resetAll`, `stamp.ts`'s `stateFor` and `decodeCompiler` all start there) - and it
writes the vendored table's last step, derived (`BRIGHTNESS_TABLE[length-1].step`) and not typed.
Every shelf card already shipped at Full, so the pin moves no byte; `src/vendor/` is untouched and
the compiler still has the field. Racks after it: aurora 4, pinwheel 3, **starfield 2**, radar 3,
joystick 4, ninepads 5 (`grid` last again), **faders 2**, dial 4, tpad 3 (never had it).

**The wire, removals only.** The SET record 1,765 -> 1,701 strings: **64 removed, 0 added, 0
moved**, and every removed key is a `P/<card>/knob brightness=N/{setup,timer}` - 8 lit cards x the
knob's 4 non-default positions x 2 events. Under `--full`, the same 64 plus the 8 per-card
cross-product keys re-named by their smaller counts (126,920 -> 25,384 preset states): 1,792 ->
1,728 records with **1,720 byte-identical**. The sandbox set `40b44316…` unmoved; no Lua entry's
record moved.

**An old share link.** The vendored writer emits format `c` ONLY when brightness is not Full
(`_pad.ts:2604-2623`). A pre-5b link at Full (`a`, `b`, `d`, or the `p<presetId>` base row) is
byte-identical to what HANGAR mints today and still lands **restored**; one whose brightness was
moved lands **unreadable** - `decodeCompiler` rebuilds from the pinned base, re-encodes and
compares, and `older` is reachable only through the Lua formats' shape character - so the
workspace shows the unreadable notice and opens the card as it ships, at Full. The codec, its spec
and the round-trip sweep are untouched; `y` and `z` stay reserved. Both landings are asserted in
`brightness.spec.ts` test 6, minting the same vector both ways.

**TUNE-01's floor.** STARFIELD and FOUR FADERS keep two real knobs each.
`knobs.preset.spec.ts`'s **"gives every shelf card three to six real knobs"** carries the dated
exemption, by those two ids only, plus the positive proof that no rack offers a `brightness` knob;
REQUIREMENTS.md is untouched and the next gate amends TUNE-01 by this word.

**The specs that moved, none renamed:** `knobs.preset.spec.ts` (the floor exemption, the kinds
carve-out unconditional again, the pin asserted where the knob's default was, NINE PADS at five
with `grid` last); `ladder.spec.ts`'s pinned test from `dial` to `pinwheel` at a measured
`{ setup: 600 }` (1013 of 908, 105 over, four `look` steps and two `touch`) - the dial's only
trimmable sheet was `look` and its `look` knob WAS the retired one; `surprise.spec.ts` (the dial
rolls `mode` + `sensitivity`; starfield holds one knob); `tune-ui.spec.ts` (the coexistence note
becomes the retirement's proof over every rack); `brightness.spec.ts` **+1 (6)**. The two
non-vacuity sweep floors go 40,000 -> **25,000** with their arithmetic re-derived (Pass A 20,270
-> 4,054, Pass B unchanged 24,576, total 44,846 -> 28,630; 25,000 is still above either pass
alone, and the derived `expectedA` / `expectedB` equalities are the real guard).

**Counts, carried + delta:** quick 95 / 978 -> **95 / 979** (+0 / +1), green twice plus the
gate's; check **658** (+0); lint clean; sweep `4 19` green (`lua-entries` 1,201 unmoved); e2e 88
titles / 103 runs -> **88 / 103** (+0 / +0); OG, fixtures, testids and copy exports all unmoved.

**The gate** (`--after change-5b --against change-5-after --check 658` at `4488eb6`): equal - the
sandbox set, the four fixtures, the OG (27 / 158,642 B / `f60a6363…`), **the scoped CSS
`7f88b4f4…` and the raw `56d81122…`**, the utilities 44 -> 44, **the copy exports `7ec85d4a…` and
the testids `70dfe98a…` (317)**, check, lint, the build, the refuse-list `--stat` empty, `src/` 9
modified / 0 added / 0 deleted / 0 renamed; moved - the wire (above, removals only), the census
`e4f23375…` -> `e12326a3…` (2,721 distinct either side; four counts down, nothing added:
`"Brightness"` 2 -> 1, `"brightness"` 3 -> 2, `"amount"` 29 -> 28, `"look"` 11 -> 10), the titles
`82938fa6…` -> `a2f9cfa8…` (979 -> 980, none renamed), the JS `870762eb…` -> `ae3b7779…`.

**Chunks** (fresh detached servers on 4173, stopped through PowerShell, HTTP 000 after each; 5173
untouched): **c3 21 passed** (tuning + tuning-webkit, AURORA's rack), **c1 33 passed** (install +
session, the rails by index), **c5 1 failed / 10 passed** then **11 passed** on the rerun
`c5-rerun-5b` - `artifacts.e2e.ts`'s staleness check compares the build's source archive against
HEAD and HEAD moved mid-run when changes 6 and 7 were recorded (documentation only); a rebuild at
the new HEAD is green. c2 and c4 not run: no title in them reads a preset's rack length.

**Questions for the user.** (a) Change 5's (b), (c) and (d) stand. (b) STARFIELD and FOUR FADERS
show two knobs each under Behavior now - if that reads thin, the honest fix is a third REAL knob
from the card's own compiler fields (the faders' `layout`, starfield's `speed`), never a
brightness. (c) TUNE-01 still says "three to six": the amendment waits for the next gate.

**Not supported by the tree:** nothing in 5b; the one departure from the brief's wording is the
landing - a pre-5b dim link is `unreadable`, not `older`, because `older` is unreachable for a
BOTOR format by the codec's own design (the brief asked which fires; this is it, measured).

## 6. ARC as an LFO - a wave-shape choice and an offset fader (2026-09-17, on Fable 5.1)

> ARC: this is basically an LFO config, we need an offset fader on the eight side of the module
> you should be able to pick the type of the wave: just like in Ableton.: sine, sotus up, down,
> triangle, square, random
> the offset fader should be able to manipulate all the time

Reading: ARC's swirl is an LFO - its rate is set by the drag, its value goes out as a CC. Two
additions on the card. (1) A wave-shape knob in the inspector, Ableton's six: sine, saw up, saw
down, triangle, square, random ("sotus up" read as "saw up"; the user may correct). The shape
changes the CC's curve over the cycle; the swirl's light may follow the shape or stay as it is -
research decides what fits 908. (2) An offset fader on the right-hand column of the pad
(column 8, nine cells): the finger's height on it adds a constant offset to the LFO's CC value,
live at every moment - while the swirl runs, while it is stopped, while another finger drags the
rate. Column 8 leaves the swirl's playing area; the offset fader lights where the finger is.
Everything else about ARC stays.

### Done, 2026-09-17

One source commit, no push, no device, no deploy: `119f4b0` feat(catalog) - the entry, the word
table, the listing's sentence, `frames.json`, the VM proof and the two specs that moved with it,
audition row 31, the entry's history; then this paragraph with `docs/TESTING.md`'s "2026-09-17
change 6" and the gate records `gate/change-6.*` (before, at `98cb427`) and `gate/change-6-after.*`.
**"sotus up" is read as "saw up"** - the user may correct.

**The reading built.** ARC's Timer is the oscillator; `v` is now the chosen wave over the phase
`p` (0..255), and the wave is the sixth knob, **Wave shape**, a six-option select worded `Sine /
Saw up / Saw down / Triangle / Square / Random` (Ableton's six, in that order; **Triangle is the
default, index 3** - the wave ARC had, byte for byte). The definitions, each an integer expression
landing in 0..255: Sine a parabola per half-wave, `128+(1-p//128*2)*(p%128*(128-p%128)*127//4096)`
(128 at p 0, 255 at 64, 128 at 128, 1 at 192); Saw up `p`; Saw down `255-p`; Triangle
`255-math.abs(p*2-255)` (the old `p<128 and p*2 or 510-p*2` at every p); Square `255-p//128*255`
(high for the first half); Random `s.n%256`, a sample-and-hold: one new value on each phase wrap,
held for the cycle - never a value per tick. **Neither `math.sin` nor `math.random` is in D-08's
restricted subset** (`lua-entries.sweep.spec.ts`: `atan sqrt abs max min floor tointeger`, and
`math.random` forbidden by name as weakly seeded on ESP-IDF), so the sine is the parabola and the
random an arithmetic LCG, `s.n=(s.n*75+74)%65537`, advanced on the wrap, seeded 1 - the same
sequence after every power cycle. **The offset fader is column 8** (cells 8, 17, ..., 80, the
right-hand column), read through the calibrated `N(x,y)%9==8` on the finger's onset and `U(y,KY)`
(0..512) on every sample; the offset is **bipolar, -64..+63** (`63-s.u*127//512`: +63 at the top
cell, 0 at the centre LED, -64 at the bottom; Ableton's centre = no offset), **held after the lift**
and added to every CC on every 20 ms tick - while running, while stopped (the frozen value moves
with the fader), while another finger drags the rate. Column 8's layer 2 is painted black; the
fader lights **one cell** at the finger's height on layer 1 in the heart's colour (cell 44 at rest).
The heart's 3x3 and the stop tap on cell 40 are as they were.

**The two-contact rule.** The old `if i>0 then return end` is gone: the fader's contact is
whichever contact LANDED in column 8 (`s.z`, its id), the swirl's is the first contact that landed
anywhere else (`s.w`); each role is forgotten on its own end code and released by a coalesced
code 9. Either order of landing works, and a fader that lifts first does not drop the rate finger.
Both fingers in the playing area: the second is ignored, its centre taps included (as today). Both
on the fader: **the later wins**; the first is ignored until it lifts and lands again. The stop tap
toggles only from the swirl's contact, so the stop finger stays the swirl's and a wobble after the
tap goes on tracking the rate (12-05). A swirl finger dragged into column 8 keeps driving the rate
(x there is the right edge, as it always was). A lost lift holds a role until the same id lands
again - the same stale as every entry without the library's `X` sweep.

**The knobs, six:** `@SWIRLC` Swirl colour, `@ARMS` Arms, `@HEARTC` Heart colour (now the fader's
marker too), `@CC` CC number, `@CH` MIDI channel, **`@SHAPE` Wave shape** (kind `mode`, worded by a
`SHAPE_WORDS` table in `view.ts` keyed by the six literals, appended LAST so a stored five-index
record still lands its first five). No `@FADERC`: a seventh knob would pass TUNE-01's six.

**The costs at the RGB444 picker corner**, measured under the pinned `compressScript` after
`initLuaFormatter()`: **Setup 528 -> 806** (380 -> 102 free; 803 at the defaults), **Timer 275 ->
437** (633 -> 471 free; 410 at the defaults) - every wave a fixed point passing `checkSyntax`. The
brief's two forms for the wave, costed: an integer and an `and/or` chain in the Timer 447 (the card
carrying all six); six closures in the Setup **1,191, 283 over**. The chosen form is the wave as
the knob's literal (CHORUS's `@SCALE` idiom) - 437 with the sine, 392 with `p`. The contact rule's
cheaper form (the swirl is "the other id", column-tested per sample) measured 770 and was rejected:
it drops the rate finger when the fader lifts first. The first draft painted the marker from the
handler and measured **953** (45 over); the Timer paints it.

**Counts, carried + delta:** quick 95 / 979 -> **95 / 980** (+0 / +1), green twice at
`--maxWorkers=2`; check 658 -> **658**, 0 / 0; lint clean; sweep `4 19` green (`lua-entries`
1,201 -> 1,212 combinations); e2e 88 / 103 -> **88 / 103**; audition rows 30 -> **31**; OG
27 files, 158,642 -> **158,745 B** (`arc.png` 6,786 -> 6,889 B, 70 of 81 lit; built, not
tracked); `frames.json` `5166ff6c` -> `79698b0d`, ARC's block alone, byte-identical on a second
regeneration. `brightness.ts` gains no declaration: the one new colour site is three literal zeros.
Specs moved: `lua-smoke` 39 -> 40 (the VM proof: each wave's v over a cycle, Random held per cycle,
the fader in all three states and held after the lift, both role orders, the fader lifting first,
the later fader finger winning, the code-9 release, the stop tap on cell 40 and not on the column;
the wobble test reads the swirl's 72 cells and keeps its stop finger down through the wobble),
`audition.spec` thirty-one, `stamp.spec` declares ARC's captured five-knob wild stamp
`unreadable` (the stamp's length rule for an ADDED knob, beside POMODORO's `older` for a resized
one; the fixture untouched).

**The gate's terms** (`--before change-6` at `98cb427`, `--after change-6 --against change-6
--check 658` at `119f4b0`): equal - the sandbox set `40b44316…`, the three other fixtures (`golden-frames`, `preset-baseline`, `synthetic-zona`), **the SCOPED CSS `7f88b4f4…`** (the raw CSS too), the utilities 44 -> 44 (0 appeared, 0 disappeared), the copy exports `7ec85d4a…`, the testids `70dfe98a…` (317), check 658, lint, the build (stamp `119f4b0`), the refuse-list `--stat` empty, no rename, no deletion; moved as a feature moves them - the wire set `63e93f57…` -> `97a42874…` and full `c1a61f4c…` -> `5e357a90…` in the tree at the run, with **1,618 records byte-identical, 108 moved, 2 removed, 24 added** by `hash-wire.mjs` per string - and 48 of those movers are MORPH's (36 moved, 1 removed, 11 added): the other executor's change 9 sat uncommitted in `morph.ts` when the gate read the tree. Against a CLEAN worktree at `119f4b0` (`git worktree add`, `hash-wire --full --sandbox`, dirty false): 1,728 -> 1,740 records, **1,655 byte-identical, 72 moved, 1 removed, 13 added, every one of them `E/arc/`** (the defaults, the corner, every single-knob position on both events, the six `shape=` pairs added, the cross-product key 6,000 -> 36,000 states), the full set `94189b0c…`, the sandbox set `40b44316…` equal - every non-ARC record byte-identical; the census `e12326a3…` -> `6b8c5116…` (2,721 -> 2,738 literals: ARC's two literals and its sentence replaced, the six wave expressions, the six words, `Wave shape`, `shape`, `@SHAPE`, `mode` 24 -> 26 - and MORPH's in-flight `@CENTRE` / `Centre` / `96` beside them); the titles `a2f9cfa8…` -> `1111bd8f…` (980 -> 981 vitest titles, one added, one retitled: "thirty" -> "thirty-one"; 103 playwright runs unmoved); the JS `ae3b7779…` -> `9aaab2bd…` (72 files); `frames.json` `5166ff6c` -> `79698b0d`; the OG 27 files, 158,642 -> 158,745 B, `f60a6363…` -> `a12c2393…`; `src/` 7 modified / 0 added / 0 deleted / 0 renamed. The script exits 1 at the wire by design; the later terms are compared from the two records.

**Chunks** (fresh detached servers on 4173, stopped through PowerShell, HTTP 000 after each; the
user's 5173 untouched): c3 **21 passed** (tuning, tuning-webkit; +0 - no e2e names ARC's select, the CC-field title walks ARC's cc knob as before); c2 **2 failed / 20 passed** at three workers (`browse:299`, `:343` - the grid read before hydration, the flake changes 3 and 4 recorded), then **22 passed** at `--workers 1` on a fresh server (rerun `c2-w1-change6`, the script's body with one flag changed, run from the scratchpad). c1, c4, c5 not run: no install, session, catalog, fidelity, first-experience, library, sandbox, artifact, radius, skeleton or smoke title reads ARC's knobs (library.e2e's stored ARC draft carries five indices, which `my-configs` lands on the first five by position). e2e 88 titles / 103 runs unmoved.

**Questions for the user.** (a) "sotus up" = saw up. (b) The offset is bipolar -64..+63 with the
centre LED at 0; say if it should be unipolar 0..127. (c) The fader's light is one cell in the
heart's colour; a level bar from the bottom (Strip's), a bar from the centre, or its own colour
knob (a seventh - one would have to go) are the alternatives. (d) Should the swirl's light follow
the wave, or stay the rate's picture as built? (e) Square is high-first and Random is the same
sequence after every power cycle (D-08); say if either should be otherwise. (f) A shared ARC link
minted before today lands `unreadable` (the stamp's rule for an added knob; the card opens at its
defaults with the notice); say if the stamp should learn to land a shorter vector - that is stamp
code, not touched here. (g) Changes 1 to 5b's questions still stand.

**Not supported by the tree / departures from the brief:** `math.sin`, `math.pi` and `math.random`
are outside D-08 (above); the wave is the knob's literal, not an integer (both brief forms costed;
the chain fits, the closures do not); the marker and the offset arithmetic are the Timer's, not the
handler's (the handler form did not fit); no `@FADERC`; the swirl does not follow the wave; the
wobble test's gesture changed with the roles; older shared ARC links land `unreadable`; `listing.ts`'s
sentence changed (ledgered in D-05's register: "Draw an LFO with your finger: rate, depth and its
wave, an offset fader down the right, and it keeps sending."); ROADMAP / REQUIREMENTS / STATE
untouched; CAT-04 stays `[ ]`.

## 7. CHORUS - a chromatic key, the lowest chord bottom-left, octave pads, smart inversion (2026-09-18, queued for Fable 5.1 after ARC lands)

> Chorus: remove the randomization adn lock options. left bottom corner should be the
> legmélyebb hang.Keys options should have sharp keys. Key options should be from C to B
> including sharp keys. Two of the keys on the module should always be octave up and octave
> down because we only need 7 keys. Also research smart inversion and put a toggle option for it.
> if you have any questions ask

("legmélyebb hang" - the lowest note.) Questions put to the user before the brief is written;
answers recorded below when they arrive.

Answers, 2026-09-18:

> 1) chorus only for now
> 2) ok
> 3) you decide but make it clear using colors
> 4) approved
> 5) yes
>
> on the code length: also keep in mind that System has much more characters to work with, so
> thats always an option for last resort. only use that though if necessary so we can keep
> everything as compact as possible

Recorded: (1) Randomize and Lock leave the CHORUS card only. (2) The twelve roots are C3..B3
(MIDI 48..59). (3) The layout is the executor's, made legible by colour - the seven chord pads one
colour family, the two octave pads another, the current octave shift shown on them. (4) Octave
up / down pads shift the whole set by 12, +-2 octaves, lit to show the shift. (5) Smart inversion
= the inversion that moves the voices least from the previous chord; toggle Off / Smart, default
Off. Budget order: the entry's Setup, then its Timer slot (879 free), then the system slots
(255/4 has room) only if nothing else fits - everything as compact as possible.

### Done, 2026-09-18

One source commit, no push, no device, no deploy: `7c13cc3` feat(catalog), on `b322fd4` (change 9
had landed first) - the entry, the entry type's `rollable` field, the word table and the
`TuneView` field, the model's two lines, the inspector and the rack, the listing's sentence,
`frames.json`, the VM proof and the two specs that moved with it, audition row 33, the entry's
history; then this paragraph with `docs/TESTING.md`'s "2026-09-18 change 7" and the gate records
`gate/change-7.*` (before, at `b322fd4`, in a clean worktree with its own install and build) and
`gate/change-7-after.*`.

**The layout built.** The pad of cell n is `n%9//3+6-n//27*3`, pad 0 at the BOTTOM-LEFT: the
seven diatonic chords rise I ii iii along the bottom row, IV V vi along the middle, vii top-left,
and the top row's middle and right-hand pads are **Octave down** and **Octave up** - beside the vii
pad, down on the left and up on the right, where a hand on the chords reaches them. The chord pads
keep the blue / violet chessboard; the two octave pads are GREEN (`0,180,60`, a literal), at phase
40 with no shift, 140 at one octave and 240 at two on the pad that took the shift. Each press moves
the set by 12, two octaves either way; a press past the end is refused, not clamped; an octave pad
sends nothing; a chord held through an octave press keeps its notes, and its note-offs are the notes
it sent. **Key** is the twelve chromatic roots C3..B3 (48..59), C3 the default, a twelve-detent
rail whose readout is the note's name. **Smart inversion** is a toggle Off / Smart, Off the default:
Smart tries each inversion in the octave nearest the previous chord (five candidates - root, each
inversion above and below) and keeps the one whose three voices move least in sum, a tie going to
root position; from C E G it voices G as B2 D3 G3 (the G held, the other two a step down), C back
in root position, A minor as C3 E3 A3. **Randomize, its Undo and the row locks are gone from the
CHORUS card only** (`rollable: false` on the entry; the inspector reads it); the colour block's own
lock stays because `ColourPicker.svelte` is on the untouched list.

**The knobs, six:** `@KEY` Key (12), `@SCALE` Scale (6, as it was), **`@INV` Smart inversion**
(Off / Smart, in the old spread knob's slot), `@BLOOMC` Bloom colour, `@VEL` Velocity, `@CH` MIDI
channel. **`@SPREAD` Bloom spread is retired** (the literal 12, its old default): a seventh knob
would pass TUNE-01's six and the user named neither Scale nor the bloom - say if Scale should have
gone instead.

**The costs at the RGB444 picker corner**, measured under the pinned `compressScript` after
`initLuaFormatter()`: **Setup 822 -> 779** (86 -> 129 free; 777 at the defaults), **Timer 29 ->
602** (879 -> 306 free; 601 at the defaults) - every knob state a fixed point passing
`checkSyntax`. Four forms costed: everything in the Setup **1,243**, 335 over; the handler defined
from the Timer's first call - rejected: a press before the first call (the VM's, or a module's
inside the first period after a Store) would find no handler; the split - the Setup keeps the
table, the state, `R` and the whole handler (the notes are on the press), the Timer at `gtt(0,20)`
with `X(self,100)` (one hundred calls at 20 ms: the same two-second window) paints the picture on
its first call, the octave pads' brightness when the shift moves and the bloom the handler asks
for - **759 / 602** with three inversion candidates; and the chosen five-candidate voicing over it,
**779 / 602** (+20): the three-candidate form voiced G after C in root position because the
closest G lives an octave below the root. **No system slot was needed.** The picture, the
indicator and the bloom can be one 20 ms call behind the press; the notes cannot.

**Counts, carried + delta:** quick 95 / 981 -> **95 / 982** (+0 / +1; the brief's 980 was before
change 9's +1), green twice at `--maxWorkers=2`; check 658 -> **658**, 0 / 0; lint clean; sweep `4
19` green (`lua-entries` 1,212 -> 1,213 combinations); e2e 88 / 103 -> **88 / 103**; audition rows
32 -> **33** (the brief's 31 was before change 9's row); OG 27 files, 158,745 -> **158,644 B**
(`chorus.png` 6,508 -> 6,407 B, 81 of 81 lit; built, not tracked); `frames.json` `79698b0d` ->
`c153c746`, CHORUS's block alone (dark at tick 0, lit from the Timer's first call), byte-identical
on a second regeneration. `brightness.ts` gains no declaration. Specs moved: `lua-smoke` 40 -> 41
(the VM proof: every key's I chord at the bottom-left, the seven degrees rising, the octave pads
and their picture, the refusals at both ends, no MIDI from an octave pad, a held chord's notes
across a shift, Smart against Off on I V I vi, the range at both ends observed 26..95 inside
16..109; the older one-chord test's window re-spelled for one hundred calls at 20 ms), `stamp.spec`
two declared CHORUS exceptions (the captured default vector is no longer the defaults; the wild
stamp lands `unreadable` at the replaced knob), `audition.spec` thirty-three. The VM proof caught
the octave step written as two per press (`z*4-30`, now `z*2-15`) before the tree did.

**The gate's terms** (`--before change-7` at `b322fd4`, `--after change-7 --against change-7
--check 658` at `7c13cc3`): equal - the sandbox set `40b44316…`, the three other fixtures, **the
SCOPED CSS `7f88b4f4…`** (the raw CSS too), the utilities 44 -> 44 (0 appeared, 0 disappeared), the
copy exports `7ec85d4a…`, the testids `70dfe98a…` (317), check 658, lint, the build (stamp
`7c13cc3`), the refuse-list `--stat` empty, no rename, no deletion; moved as a feature moves them -
the wire set `97a42874…` -> `25aada35…` and full `5e357a90…` -> `14ef80ae…`, 1,723 -> 1,725
records, **1,630 byte-identical, 82 moved, 11 removed, 13 added, every one of them `E/chorus/`** -
no other executor's work sat in the tree at either run (change 9 was committed before both); the
census `6b8c5116…` -> `290eeeee…` (2,738 -> 2,747 literals: CHORUS's two literals and its sentence
replaced, the roots and the knob words); the titles `3f2be85d…` -> `b5de55e2…` (982 -> 983 vitest
titles; 103 playwright runs unmoved); the JS `9aaab2bd…` -> `f57bbcf0…` (72 files); the OG
`a12c2393…` -> `67a7beec…`; `src/` 11 modified / 0 added / 0 deleted / 0 renamed. The script exits
1 at the wire by design.

**Chunks** (a fresh detached server on 4173, stopped through PowerShell, HTTP 000 after; the
user's 5173 untouched): c3 **21 passed** (tuning, tuning-webkit; +0 - the titles that read the
Randomize controls open AURORA, where they are drawn as before). c1, c2, c4, c5 not run: no title
reads CHORUS's knobs or its picture. e2e 88 titles / 103 runs unmoved.

**Deviations, stated:** files beyond the brief's list moved for change 1 (`types.ts`, `model.ts`,
`view.ts`, `TuningRegion.svelte`, `KnobRack.svelte` - the Randomize and lock controls are the
inspector's, not the entry's); the Timer runs at 20 ms and paints the picture, so tick 0 is dark;
a note rail's readout is the note's name; the card sentence changed ("Seven chord pads, the lowest
at the bottom-left, two octave pads, and a warm bloom from the pad you hit."); the before record
was first taken at `4ebb3d2`, discarded when change 9 landed, and re-recorded at `b322fd4` - a
first worktree with a `node_modules` junction was abandoned after `git worktree remove` followed
the junction into the real `node_modules` (`node_modules/.bin` and what it reached went; restored
with `npm ci` from the untouched lockfile, `package.json` and `package-lock.json` unmoved), and the
worktree then got its own `npm ci` and build; change 9 committed before this change's source
commit, so `git commit --only` swept nothing of another executor's. `docs/TESTING.md` carries the
full record.

**Questions for the user.** (a) The layout: the two octave pads on the top row beside the vii pad
(down left, up right); the two top corners, or the bottom row's right, are the alternatives. (b)
The octave pads are green; a knob would be a seventh. (c) Smart's metric is the least sum of voice
movement over five candidates, ties to root; say if it should search upward only, or keep a chord
inside a range. (d) The bloom and the octave indicator are up to 20 ms behind the press (the
Timer's); say if the bloom must be the handler's again. (e) The Key knob is a twelve-detent rail
reading "C#3"; a twelve-option select would move `WORD_ROW_MAX`, which two specs pin by name. (f)
Bloom spread retired rather than Scale. (g) The picker's lock on the Bloom colour block stays
unless `ColourPicker.svelte` may be touched. (h) Older shared CHORUS links land `older` or
`unreadable`. (i) Changes 1 to 6 and 9's questions still stand.

## 8. EUCLID renamed, a fourth ring, tempo direction, ring colours, MIDI clock sync, a note per ring (2026-09-18, queued for Fable 5.1 after CHORUS)

> Euclid: rename it to smth crfeative. Add one more ring the farest one from the center. Tempo
> slider is in the wrong direction the bigger tempo should be on right side.  each ring should
> have their own color. It should be able to get sync from a software or daw, its in the Editor,
> implement that for the sequencer profiles. its under function called MIDI rtm callback
> handler. Remove base not and you should be able to select a note for each ring from C -2 to G 8
> so full range.

Facts read before asking: the 9x9's concentric rings hold 8, 16, 24 and 32 cells; EUCLID uses the
inner three, so "the farthest from the centre" is the outer 32-step ring - it fits the pattern
with no rounding. The firmware's MIDI RX: `rx_mode(2, ...)` routes MIDIRTM (the realtime
messages: clock 0xF8 at 24 per quarter, start 0xFA, continue 0xFB, stop 0xFC) and
`midi_rx_register` binds a Lua callback (`grid_protocol.h:389-395`, `:882-883`, `:920-924`) -
the Editor's "MIDI rtm callback handler". "Sequencer profiles" read as every card that steps on
the Timer: EUCLID (this change), STEPS, RADAR POINTS, SONAR - the others follow once the shape is
proved here. Questions put to the user; answers recorded below.

Answers, 2026-09-18:

> 1) Orbit
> 2) yes
> 3) yes, step division knob
> 4) sure
> 5) typed names and numbers both
> 6) not right after, we want to test it but save it as sequencer config if we'd decide to do
>    this for the others as well

Recorded: (1) EUCLID becomes ORBIT (id `orbit`; the address, the OG name and every literal move;
the old id is the fourteenth dead link). (2) TUNE-01's six-knob rule is lifted for this card by
the user's word; the next gate amends it. (3) `Sync: Internal / External` plus a `Division` knob
(8th / 16th / 32nd - 12 / 6 / 3 clocks a step); under External the DAW's Start / Stop run the
rings and the tempo knob is ignored. (4) Default notes inner to outer: 36, 38, 42, 46 (GM kick,
snare, closed hat, open hat). (5) The note picker is a typed field taking `C#3` and `49` alike,
C-2 (0) to G8 (127). (6) The sync is built on ORBIT alone and benched; the clock-handling shape
is saved as a reusable sequencer piece (a library function or a documented idiom, whichever the
measurement favours) so STEPS, RADAR POINTS and SONAR can take it later on the user's word.

### Done, 2026-09-18

One source commit, no push, no device, no deploy: `c504753` feat(catalog), on `fd5e1bf` - the entry
moved with `git mv` (`euclid.ts` -> `orbit.ts`, `euclid.md` -> `orbit.md`, the e2e fixture), the
catalog's index, listing and front door, the knob type's `previewIndex`, the preview renderer, the
word tables and the note parser, the typed field, the inspector's held-preview line, the stamp's
wide field, the host's `rtm`, the brightness palette, `hash-wire.mjs`'s sampled product,
`frames.json`, the VM proof and every spec that moved with the rename, audition row 34, the
entry's history and a pointer in the library's; then this paragraph with `docs/TESTING.md`'s
"2026-09-18 change 8" and the gate records `gate/change-8.*` (before, at `fd5e1bf`, in a clean
worktree with its own `npm ci` and build) and `gate/change-8-after.*`. **The old address is the
fourteenth dead one** (`local.spec.ts`'s `REMOVED`).

**The card built.** Four rings - 8, 16, 24 and the outermost square's 32 steps, beating on a
96-step cycle - each with its own colour and its own note, on the Timer (Internal) or on the
DAW's MIDI clock (External, every Division clocks: an 8th, a 16th or a 32nd, 12 / 6 / 3 of the 24
per quarter). The ring walk is one rotation (`a,b=-b,a`) instead of four cases, the same cell
order for every ring. **The clock idiom, in the firmware's own spelling:** `self.rtmrx_cb=
function(s,h,b)` - `decode.lua:42-44` calls `el:rtmrx_cb({x[1],x[2],x[3]}, x[4])`, a header
triple and ONE byte (the Editor's "MIDI Real-Time RX callback handler" offers exactly
`self.rtmrx_cb(self, header, rtm)`); `grid_usb_midi.c:200-210` puts the raw byte on the wire,
`grid_decode.c:388` gates the class on `rx_mode` and `:420` pushes it; `init.lua:15` has MIDIRTM
off by default and `l_grid_rx_mode` needs a NUMBER, so the Setup routes with `grxm(2,@SYNC and 3
or 0)` - the package's usage line: "2=MIDIRTM ... 0x02=handle_external". 250 Start resets the
step and the count and runs, 251 Continue runs, 252 Stop halts, 248 while running steps every
`@DIV` clocks through the routine the Timer publishes as `s.f` (a field READ; a field call is
refused by host-surface.spec.ts), 254 does nothing. Under External the Timer steps nothing and
still sweeps the fingers; the tempo knob is ignored. Saved as a reusable idiom in
`docs/entries/orbit.md` ("The clock idiom"), with a pointer in `docs/entries/library.md`; a
library function would save nothing (255/0 has 66 free, 255/6 35, the callback is ~170 and its
routine the entry's own). STEPS, RADAR POINTS and SONAR untouched.

**The knobs, fourteen** (TUNE-01's six lifted by answer 2; the next gate amends the rule):
`@TEMPO` Tempo `70 90 110 140 180 240` (reversed as the brief spelled it, 110 still the default),
`@PULSES` `3,5,7,11` and five more quadruples, **`@R1C`..`@R4C` Ring 1..4 colour** (one palette:
cyan, spring green, violet, white at the defaults), `@TRAIL`, **`@SYNC` Sync** Internal / External,
**`@DIV` Division** 8th / 16th / 32nd (16th the default), **`@N1`..`@N4` Ring 1..4 MIDI note** (0..127
typed, 36 38 42 46), `@CH`. **`@RINGC` and `@NOTE` are retired.** The note field takes `C#3` and
`49` alike (`view.ts`'s `noteNumber`, `noteName`'s spelling: C4 = 60, so 0 is C-1 and 127 is G9 -
Live's C-2..G8 is the same 0..127), refuses `H3` and `128` with "A note here is C-1 to G9, or 0 to
127.", and shows the name; the four ring notes sit under MIDI output by their label's word and are
never rolled. The stamp carries the index: a knob past 32 options rides two base-32 characters
(`stamp.ts`'s `fieldChars`), every other rack byte-identical; ORBIT's payload is 28 characters.
**The preview holds Sync at Internal** (`previewIndex`, read by `createLuaPadSim` alone) and the
inspector says so under Behavior; the wire, the meters and the stamp carry the choice.

**The costs at the RGB444 picker corner**, measured under the pinned `compressScript` after
`initLuaFormatter()`: **Setup 726 -> 846** (182 -> 62 free; 843 at the defaults), **Timer 237 ->
404** (671 -> 504 free; 383 at the defaults) - every knob state a fixed point passing `checkSyntax`.
Two forms costed: everything in the Setup with the step routine there **976, 68 over**; the chosen
split - the routine, the twelve-channel colour table and the note table in the Timer (the head
colours its own cell as it lights it), the callback in the Setup. **No system slot was needed.**
The one caveat: the routine is published by the Timer's first call, at most one `@TEMPO` period
after the Setup, so a clock inside that period is counted, not stepped.

**Counts, carried + delta:** quick 95 / 982 + 1 todo -> **95 / 983 + 1 todo** (+0 / +1), green twice
at `--maxWorkers=2`; check 658 -> **658**, 0 / 0; lint clean; sweep `4 19` green (`lua-entries`
1,213 -> 1,804 combinations); e2e 88 / 103 -> **88 / 103**; audition rows 33 -> **34**; OG 27
files, 158,644 -> **159,169 B** (`euclid.png` 6,029 gone, `orbit.png` 6,554, 34 of 81 lit; built,
not tracked); `frames.json` `c153c746` -> `ccb860ca`, ORBIT's block alone (30 / 45 / 50 / 51 / 45
-> 52 / 74 / 82 / 81 / 74 lit bytes), byte-identical on a second regeneration; `brightness.ts`
gains `orbit: { palettes: ["c"] }`; `utilities` 44 -> 44; catalog 27. Specs moved: `lua-smoke` 41
-> 42 (the VM proof: four rings on the Timer with note-ons 36 / 30 / 28 / 33 over 96 steps - the
outer ring's 32 by count - each ring's colour on its cells, External on every Division clocks
from Start with step 0 on the first clock and step 1 on the seventh, 200 Timer ticks moving
nothing, Stop, Continue from clock 7, Start again, active sensing, the 8th and the 32nd, the
words, the preview, the note field), `stamp.spec` (EUCLID's two captured records under ORBIT:
the default vector encodes now - `tempo: 3` is 140 ms on the reversed list - and the wild `x` stamp
lands `unreadable`), `knobs.lua.spec` (the four wide knobs named), `stamp-roundtrip.sweep` (Pass
C), `surprise.spec` (thirty-three), `local.spec` (thirteen), `audition.spec` (thirty-four),
`colour-picker.spec` (12 / 7 / 4 / 4), `catalog.spec`, and the renames across nine more.

**The gate's terms** (`--before change-8` at `fd5e1bf`, `--after change-8 --against change-8
--check 658` at `c504753`): equal - the sandbox set `40b44316…`, the three other fixtures, **the
SCOPED CSS `7f88b4f4…`** (the raw CSS too), the utilities 44 -> 44 (0 appeared, 0 disappeared),
check 658, lint, the build (stamp `c504753`), the refuse-list `--stat` empty; moved as a feature
moves them - the wire set `25aada35…` -> `75d0c1eb…` and full `14ef80ae…` -> `5c2e7227…`, 1,752 ->
2,802 records, **1,657 byte-identical, 0 moved, 95 removed - every one `E/euclid/` - and 1,145
added - every one `E/orbit/`**; the census `290eeeee…` -> `991aab0d…` (2,747 -> 2,783 literals); the
copy exports `7ec85d4a…` -> `071be34f…` (two new lines); the testids `70dfe98a…` -> `74e0cfd6…` (317
-> 318, `preview-held`); the titles `b5de55e2…` -> `39b22413…` (983 -> 984 vitest titles: one added,
six retitled; 103 playwright runs, one retitled); the JS; the OG `67a7beec…` -> `9becd682…`; `src/`
34 modified / 1 added / 1 deleted / 0 renamed (git reads the entry as a delete and an add). The
script exits 1 at the wire by design. **The gate script's stale `QUICK_FILES=94` /
`QUICK_TESTS=966`** (stale since change 5, `quick exit 1` in every record since) read **95 / 983**
now, the figures these runs prove - the only edit to that script, in the docs commit.

**Chunks** (fresh detached servers on 4173, stopped through PowerShell, HTTP 000 after each; the
user's 5173 untouched): c1 **1 failed / 32 passed** at three workers (the install title's ±6
picture tolerance: 221 -> 118, 8 off half - ORBIT's white outer head over an orange marker sums two
layers, so the brightest channel moves with the head's phase at the sample; a ratio band
0.42..0.58 now, the wire bytes asserted exactly as before), then **33 passed**; c2 **2 failed / 20
passed** at three workers (`browse:299`, `:343` - the flake changes 3, 4 and 6 recorded), then **22
passed** at `--workers 1` on a fresh server; c3 **21 passed**; c4 **16 passed**; c5 **11 passed**.

**Deviations, stated:** files beyond the brief's list moved for the typed field, the preview and
the stamp (`types.ts`, `lua-pad-sim.ts`, `model.ts`, `view.ts`, `inspector-copy.ts`,
`MidiField.svelte`, `TuningRegion.svelte`, `stamp.ts`, `knobs.lua.ts`, `lua-host.ts`,
`hash-wire.mjs`); the copy exports and the testids moved beyond the rename (two copy lines, one
testid - what "says so" costs); `hash-wire.mjs` samples a product past a million states (ORBIT's
full product is 10^17; every other record byte-identical); the note range is spelled C-1..G9
(`noteName`'s C4 = 60) for the brief's C-2..G8, the same 0..127, so the field round-trips its own
readout; the Tempo list is reversed as the brief spelled it; the install e2e's picture check is a
ratio band; the colour-picker split re-recorded; `library.md`'s section 5 still names EUCLID
where it means ORBIT, mirroring the untouched `library.ts`; the card sentence changed (D-05's
register: "Four Euclidean rings, a colour and a note each, on their tempo or your DAW’s clock; tap
a step to change it."). `docs/TESTING.md` carries the full record.

**Questions for the user.** (a) Tempo reads 70..240 left to right, so the bigger number on the
right is the SLOWER step; a BPM readout (`gtt(0,15000//@BPM)`, +7 on each event, the same six
periods: 214 167 136 107 83 62) would put the bigger number and the faster tempo on the right
together - say which. (b) The note field spells 0..127 as C-1..G9 (the readout's rule, C4 = 60);
Live writes the same range C-2..G8 - say if the readout should move to Live's spelling everywhere
(CHORUS's Key would then read "C2"). (c) Under External the rings wait for Start or Continue; clock
alone runs nothing - say if clock alone should. (d) A clock inside the first `@TEMPO` period after
a Store is counted and not stepped. (e) The preview holds Internal and says so; say if it should
stand still under External instead. (f) Ring 4's default colour is white over the orange markers.
(g) The four-ring pulse sets are the executor's. (h) Every EUCLID link shared before today lands
`unreadable`; the card opens at its defaults. (i) TUNE-01's six needs its amendment at the next
gate. (j) Changes 1 to 7 and 9's questions still stand.

### Done, 2026-09-18, change 8b - the Tempo rail reads BPM

Question (a) answered by the coordinator: "bigger tempo on the right side" means FASTER on the
right. One source commit, no push, no device, no deploy: `3d91f2b` feat(catalog), on `a86acdc` -
`@TEMPO` (a period in ms) is **`@BPM`**, both events `gtt(0,15000//@BPM)` (a 16th a step), the
knob's id still `tempo` (the stamp, the fixture rack and the specs read the id; only the token
moved, at no cost to the plumbing), the label "Tempo (BPM)" (no unit path in `view.ts`; the
readout is the bare number). **The ladder `60 90 110 136 160 200`**, ascending - 250 166 136 110
93 75 ms - **136 the default**: `15000//136` is exactly the 110 ms step, so `frames.json` and
the OG are unmoved (a "clean" 140 default would be 107 ms and move the rest frame). **Costs at
the RGB444 picker corner:** Setup 846 -> **853** (55 free; 850 at the defaults), Timer 404 ->
**411** (497 free; 390 at the defaults). **Counts:** quick **95 / 983 + 1 todo** (twice), check
**658**, lint clean, sweep `4 19`, e2e **88 / 103**, audition rows **34**, utilities **44**, catalog
**27**. **The gate** (`--before change-8b` at `a86acdc`, `--after change-8b --against change-8b
--check 658` at `3d91f2b`): everything equal but the wire - 2,802 records, **1,657
byte-identical, 1,145 moved, every one `E/orbit/`, 0 removed, 0 added, 0 outside**; the census
moved by ORBIT's two strings, the token, the label and the six values; titles, copy exports,
testids, scoped CSS, utilities, OG, fixtures unmoved. **Chunk c3 21 passed** on a fresh detached
server (HTTP 000 after; 5173 untouched); c1 not run - no title reads the tempo. The captured
EUCLID default vector is the defaults again under ORBIT (`stamp.spec.ts`). Row 34 (f) and its
dated 8b paragraph in `docs/HARDWARE-AUDITION.md`, the cost row 850 / 390; `orbit.md`'s 8b
section; `docs/TESTING.md` "2026-09-18 change 8b". Questions (b)-(j) of change 8 still stand.


## 9. MORPH - the centre's value settable (2026-09-18, queued for Opus)

> queue for Opus:
> Morph: should be able to setup the value of the center.

Today the finger at dead centre gives each corner's CC a quarter of the range (about 32 of 127,
the four weights summing to 127). A question put to the user on what "the value of the center"
sets; answer recorded below.

> a

Recorded: a `Centre` knob - the CC value each corner sends when the finger is dead centre (today
about 32, a quarter of 127); the blend reshapes around it so a corner still reaches 127 under the
finger and the others fall toward 0, with the centre landing on the chosen value. Range and steps
are the executor's to measure inside 908 (MORPH's Setup is at 814).

### Done, 2026-09-18

One source commit, no push, no device, no deploy: `54acb09` feat(catalog) - the entry, the VM
proof, the two specs that moved with it, audition row 32, the entry's history; then this paragraph
with `docs/TESTING.md`'s "2026-09-18 change 9" and the gate records `gate/change-9.*` (before, on
an isolated worktree at `8d4364e`) and `gate/change-9-after.*`.

**The reading built.** A sixth knob, **Centre**, a five-dot rail with a readout under Behavior:
`0, 16, 32, 64, 96`, **default the 32 - today's behaviour**. THE FOUR WEIGHTS ARE UNTOUCHED and
still sum to the full range (`w = {u*v//127, x*v//127, u*y//127, x*y//127}`); the knob is a map
applied to each weight on its way OUT, which is the only reading a settable centre admits - a blend
that put an arbitrary value in the middle could not sum to 127 at all, because dead centre all four
weights are the same number. The map is two linear segments with the breakpoint at 32, the weight a
finger reads dead centre: `z=z<32 and z*@CENTRE//32 or @CENTRE+(z-32)*(127-@CENTRE)//95`, one
insertion between reading the weight and testing it, and nothing else in the Setup moved. So 0
stays 0 (the opposite corner is still silent), 127 stays 127 (a corner is still full under the
finger), 32 lands on the knob's value, and the map is monotone at every position. The corner blocks
are painted at the value SENT, not the raw weight - the picture is what the DAW hears, 11-08's own
rule. Everything else survives by construction, because nothing reads a weight after that line: the
one-message corner tap, the per-corner send-on-change, the dead margin, the comet, the
single-contact rule.

**AT THE DEFAULT THE MAP IS THE EXACT INTEGER IDENTITY** (`z*32//32` and `32+(z-32)*95//95` are
both `z` at every one of the 128 weights, asserted rather than sampled), so an untouched MORPH is
byte-identical in behaviour: the pinned 120-message centre-to-corner stroke captured at plan 12-09
is UNMOVED, and so are `frames.json`, the golden frames and the OG image. The Setup STRING is 46
characters longer, which is the one thing that could not be avoided - `renderLua` substitutes a
value, not an expression - so the brief's "at the default the two MORPH records must be
byte-identical" holds for the Timer (both `""`) and for the behaviour, but not for the Setup
record. What replaces it is the per-string wire diff: **36 records moved, 1 removed, 11 added, and
not one of them outside `E/morph/`**.

**The cost, and no second slot.** Setup **814 -> 860** at the RGB444 picker corner (94 -> **48
free**, 30 under the 890 error line), 810 -> **856** at the defaults, 853 at Centre 0. The Timer is
still the empty string with 908 free, so the `self:tim()` pattern was not needed and the system
slots - the last resort of section 7 - were not approached. The cheaper form was costed and
rejected: `z=glim(z*@CENTRE//32,0,127)` is **+23** against **+46**, but it cannot express a centre
below 32 at all (at 16 a corner reaches only 63, so "127 under the finger" fails at every position
that makes the middle quieter) and above 32 it saturates early (at 96 the macro reads 127 from
weight 43 onward). A branchless equivalent of the chosen map costs +51.

**Measured dead centre, in the real Lua host** (the four raw weights there are 31, 31, 31, 32 -
integer division's own split, the card's arithmetic since 11-08): silent at Centre 0,
15/15/15/16 at 16, 31/31/31/32 at 32, 62/62/62/64 at 64, 93/93/93/96 at 96; a press on either
extreme corner sends exactly one message, 127, at every position; a corner-block tap still sends
exactly one, 77 / 83 / 90 / 102 / 114. **127 IS NOT OFFERED** and the rejection is arithmetic: its
second segment is `127+(z-32)*0//95`, so every weight at or above 32 reads 127 - 33 distinct values
over the whole travel against 128 at the default, each macro pinned full across the quadrant
nearest its corner. **A CENTRE OF 0 IS KEPT**: the middle is silent and its four blocks are black,
and nothing is stranded, because a corner the finger leaves still sends its single 0 on the way out.
It is a question for you below.

**Counts, carried + delta:** quick 95 / 980 + 1 todo -> **95 / 981 + 1 todo** (+0 / +1), green twice
at `--maxWorkers=2` plus the gate's; check 658 -> **658** (+0), 0 / 0; lint clean; sweep `4 19`
green (`lua-entries` 1,207 -> **1,212** combinations); e2e 88 titles / 103 runs -> **88 / 103**
(+0 / +0), chunk c3 **21 passed** on a fresh detached server on 4173, stopped through PowerShell,
HTTP 000 after (your 5173 untouched); audition rows 31 -> **32**; OG, `frames.json`, the golden
frames and the preset baseline all byte-identical. Gate terms equal: the sandbox set, all four
fixtures, the OG, the raw and scoped CSS, the utilities 44 -> 44 (0 appeared, 0 disappeared), the
copy exports, the 317 testids, check, lint, the build, the refuse-list `--stat` empty, no rename,
no deletion, no addition. Moved: the wire (above), the census `e12326a3…` -> `7ef5b807…` (2,721 ->
2,724 literals; the diff is eight lines, all MORPH's), the titles 980 -> 981 (one added, none
renamed), the JS. Chained onto change 6's record at `4ebb3d2`: the wire set `792c5de4…` ->
`97a42874…`, full `94189b0c…` -> `5e357a90…`, 1,703 of 1,740 records byte-identical and **0 movers
outside `/morph/`**.

**Not supported by the tree / departures from the brief:** the default's Setup RECORD cannot be
byte-identical (above; the behaviour is, and the pinned stroke proves it); the knob's top value is
**96, not 127** (measured); the values are the brief's set with 127 replaced by its own suggested
floor alternative, 16; `stamp.spec.ts` and `audition.spec.ts` were edited though the brief's file
list did not name them (the first declares the older-link landing, without which the suite is red;
the second carries the row count); the gate ran on an isolated worktree because a second executor's
change 6 was in flight in the same working tree when this began, and only an isolated tree can show
a MORPH-only wire diff. A finding out of scope, stated rather than fixed: `scripts/13.2-gate.sh`'s
`QUICK_FILES=94` / `QUICK_TESTS=966` have been stale since change 5, so the gate's own quick term
prints `quick exit 1` in every record since (`change-5b-after`, `change-6`, both `change-9`); it has
never blocked anything and the suite is green.

**Questions for the user.** (a) **THE 0 FLOOR, above all:** at Centre 0 the middle of the pad is
silent and its four corner blocks black, each macro opening only as the finger moves into its half -
four gated quadrant macros. Nothing is stuck. Say if the floor should be 16 instead, or if 0 should
go. (b) 127 is not offered, for the reason above; say if you want it anyway as a deliberate
"everything full" position. (c) The five positions are `0, 16, 32, 64, 96`; say if you would rather
have an even ladder or a finer one - extra positions cost nothing on the wire. (d) Dead centre three
corners land `C//32` under the knob's value and the fourth on it exactly (62/62/62/64 at 64), because
the weights there are 31, 31, 31, 32; a breakpoint of 31 would put all four on the value exactly and
would cost the byte-identical default. Say which you would rather have. (e) Every MORPH link shared
before today lands **unreadable** and the card opens at its defaults - the stamp's payload-length
rule for an added knob, the same price ARC paid at change 6. (f) The corner blocks are painted at
the value SENT rather than the raw weight; say if you would rather the picture stayed the blend.
(g) Changes 1 to 6's questions still stand.

10. Sandbox feature set (queued for Fable, LAST - after ARC, Chorus, Orbit, Morph)
--------------------------------------------------------------------------------

> going to sleep now. queue this extra sandbox feature set for last, use fable for this. right now
> you can ask questions then after I answer use your best discretion if any new question would
> arise.
>
> In sandbox move there should be a selector cursor as default  If you press escape or you press V,
> this selector tool should be able to select elements move them and resize them without adding
> more elements. give a hotkey to each element for it to be able to add easier, so for example f
> adds fader, b adds a button etc.
> All different elements should have animations on the ZONA.
>
> remove the position and size on the right panel as you can do those all the center panel. you
> should NOT be able to change the type of an element once it was selected.
>
> in the middle section when clicking with selector mouse there should be an option to delete with
> a little icon
>
> every element should have a min max MIDI value that you can set.
>
> faders and xy pad should have a scale mode (or find it a good name): value never jumps, you only
> change the direction of the value. so if this mode is activated, if you touch a fader anywhere it
> doesnt jump or does anything until you start a movement then the value changes subtly to that
> direction.
>
> Fader: Spring mode: put an option to choose visszaugros fader like the pitchwheel. you should be
> able to input on which value the fader jumps to.
>
> Button: rename latch to toggle mode. MIDI output should have an option either note or cc.  should
> have a radio button option like in touchosc
>
> Knob: absolute (0-127) vs relative (minus minus minus or plus plus plus)
>
> add a "Blank" element which just colors the LED.

Questions asked 2026-09-18 (the recommendation first in each; unanswered ones are decided by it):

 1. Two-click area placement. Today a click on an empty cell with nothing armed starts an area
    (second click = far corner). With the selector as default: (a) area placement goes - elements
    are added by hotkey or palette, then a click places the default size, then handles resize;
    (b) keep it behind a hotkey, A.
 2. Hotkeys: F fader, B button, X XY pad, K knob, L blank (LED), V / Escape selector, Delete or
    Backspace deletes the selection. Confirm or rename.
 3. A hotkey (a) arms the kind for one placement and returns to the selector after the click;
    (b) stays armed until Escape / V so several can be dropped in a row.
 4. Moving with the selector: drag the body of a selected element to move it; the eight handles
    resize as today; arrows nudge the selected element one cell, Shift+arrows resize by one cell
    (the keyboard route replacing the removed number fields). Confirm.
 5. Animations on the module, per kind - a proposal: fader fills from its low end to the value
    (a bar), button lights its whole region while on (a toggle stays lit), XY pad draws a
    crosshair through the finger, knob lights an arc from the low position to the value, blank
    is a steady colour. Every element keeps its dim rest colour. Confirm or amend.
 6. Min / max: (a) min above max is allowed and inverts the direction (TouchOSC does this);
    (b) refused. XY pad: (a) one min/max for both axes; (b) a pair per axis. Button: min = the off
    value, max = the on value (note: the velocity).
 7. The no-jump mode's name and speed. Name: "Relative" (vs "Absolute"). Speed: (a) the full
    travel of the region moves the value by 64 - half speed, the subtle feel; (b) 1:1, full
    travel = 127; (c) a Speed option with both.
 8. Spring: on release the fader sends and shows the spring value (default 64, typed 0..127,
    clamped into min/max). Can be combined with Relative. Confirm.
 9. Radio "like TouchOSC": (a) a new element kind, Radio - a region split into N segments along
    its long axis, one lit at a time, a touch selects a segment and sends its index scaled into
    min..max on one CC (hotkey R); (b) a Button option "radio group": buttons in the same group
    are exclusive - pressing one releases the others.
10. Button as Note: a note-number field (typed names and numbers like Orbit), velocity = max,
    note off on release (momentary) or on the second press (toggle). Confirm.
11. Knob relative encoding: (a) two's complement - 1..63 up, 127..65 down (Ableton "Relative
    (2's Comp.)"); (b) binary offset - 65..127 up, 63..1 down; (c) sign magnitude - 1..63 up,
    65..127 down; (d) a selector with all three.
12. Budget: every option is a column in the region table on the module, so a surface using many of
    them fits fewer than sixteen elements; the meter already floors the count. Acceptable?

> 1) a
> 2) confirmed
> 3) b
> 4)confirm
> 5) confirm, and keep in mindg that the irl animation needs to change if theres spring mode on, or
> if theres toggle mode for a button etc.
> 6) a, xy: a, button: confirmed
> 7) c
> 8) confirm
> 9) b
> 10) confirmed
> 11) d
> 12) system event can be used for budget. also hide the character count and limit from the user
> entirely for now

Recorded 2026-09-18. Readings: (3) a hotkey stays armed until V / Escape. (5) the module's picture
follows the mode: a spring fader's bar returns to the spring value on release, a toggle button stays
lit while on, a momentary one goes dark on release, a relative fader's bar shows the held value not
the finger. (7) a Speed option, half and full. (9) a Button option "Radio group": buttons in the same
group are exclusive - pressing one releases the others (sends their off value) - no new kind.
(11) a Mode selector on the knob: Absolute, Relative (2's comp.), Relative (binary offset), Relative
(sign magnitude). (12) the system slots may carry Sandbox text: a Sandbox landing may replace the
library halves in 255/0 and 255/6 with only what the surface's runtime calls (E G N U X and the
calibration tables) and use the room for the runtime; the cost meter, the character count and the
limit disappear from the Sandbox for now - the cap is still enforced, its refusal worded without
numbers. Every other question: the first option.

### Done, 2026-09-18, change 10A - the editor (part A of the Sandbox feature set)

One source commit, no push, no device, no deploy: `e2947fc` feat(sandbox) - the model, the emitter,
the schema, the copy, the four components, the route, the six specs and the two e2e files that
moved with them; then this paragraph with `docs/TESTING.md`'s "2026-09-18 change 10A", the dated
paragraph in `docs/INSTALL-RUNBOOK.md`, a section in `docs/entries/sandbox-runtime.md`, and the
gate records `gate/change-10a.*` (before, at `51e8ff7`, in a clean worktree `../hangar-gate-10a`
with its own `npm ci` and build) and `gate/change-10a-after.*`. Part B (the MIDI options, the modes,
the animations, the budget) is the next executor's; the region schema's MIDI fields are untouched
beyond what A needed (a blank's inert `cc: 0, channel: 1`).

**What a click does now (answers 1a, 3b).** The selector is the default tool: a click on an
element selects it; a click on an empty cell CLEARS the selection and moves the keyboard focus cell
there (chosen, of the brief's two: it is the selector convention, and it is the plate's own way to
the no-selection panel); two-click area placement is gone (`boxBetween`, `kindForBox`, the `area`
placement, the `started` outcome and the release-is-a-second-click accelerator with it). A kind
armed - a palette row or its key - places its default box at every click until `V` or Escape (or a
second click on the row) returns to the selector; an undo does not disarm; at the cap the click is
refused with the cap's line. The plate's cursor is the arrow until a kind is armed (crosshair) and
`move` over a selected body.

**The hotkeys (answer 2).** `F` fader, `B` button, `X` XY pad, `K` knob, `L` blank; `V` and Escape
the selector; `Delete` and Backspace the selection. The map is `editor.ts`'s `HOTKEYS` /
`SELECTOR_KEY` / `kindForKey` (pure, pinned by `sandbox-ui.spec.ts` test 2); the listener is the
route's one `window` `keydown` handler (the Ctrl/Cmd+Z / +Y handler, widened): it returns while an
`<input>`, `<textarea>` or `<select>` has focus (a name field typing "f" arms nothing - the e2e
presses it), with Alt held, and for any event the plate or the element list already handled
(`defaultPrevented`: the plate's Escape / Delete and the list's Delete). The palette rows show their
key as a mono chip (`<kbd>`) and carry `aria-keyshortcuts`; the status line still says which kind is
armed (`Click a cell to place the Fader.`); the empty state's second line names the five keys.

**Moving (answer 4).** A press on the selected element's body starts a body drag (the click
selects first, so a press on an unselected element selects AND drags); the proposed box follows the
pointer less the grab offset, clamped to the plate; the release is ONE `editor.moveSelectedTo(cell)`
- `applyEdit` with the origin under the `move` EditKind, coalesced under the drag key and sealed on
release, refused (the region stays put, the line in the status) where the box would overlap or leave
the plate, and a release on the same cell commits nothing. The handles resize as before; both
drags share `commitBox` in the model. With the selector and a selection the arrows are
`nudgeSelected` (one cell) and Shift with an arrow `resizeSelectedBy` (one cell wider / taller, or
narrower / shorter), coalesced under `nudge:<id>` / `grow:<id>` until the plate's key-up calls
`commitField` - a held arrow is one Undo; with a kind armed or nothing selected the arrows move the
focus cell as before. The model's box-taking methods are exactly `resizeSelectedTo`,
`moveSelectedTo`, `nudgeSelected`, `resizeSelectedBy` (spec 2 pins the list).

**The panel.** The `Position & size` block is gone (Column, Row, Width, Height, the snap helper);
the `N × M units` chip stays beside the name; the type is a plain label under `Element name`
(`field-kind`, a `<span>`, beside Orientation on a fader) - the eyebrow still reads `SELECTED ELEMENT
/ FADER`; the adjacency warnings render under the identity block; Behavior, MIDI output (not on a
blank) and Appearance as before; Duplicate / Delete element pinned as before. `setKind`, `onkind`,
`kindProblem` and the `retype` edit kind are retired; the one refusal that block still needs (an
orientation the geometry refuses) is `orientationProblem` / `orientation-problem`. The numeric
fields are the three MIDI fields (`NUMERIC_FIELDS` = cc, cc2, channel).

**The delete icon.** In the selection group, a 20px square in the action colour with the
workspace's cross, diagonally off the selection's top-right corner (inside the corner where the
plate's edge leaves no room), a 44px hit square under it, `surface-delete`; its press stops the
plate's own pointerdown (which would read the cell under the icon) and its click is the panel's
`ondelete` - one Undo. The numbers are `layout.ts`'s `SANDBOX_DELETE_ICON` / `SANDBOX_DELETE_HIT`. No
radius anywhere; the spec scans the source for `border-radius` and `rx=`/`ry=`.

**The blank kind.** `ElementKind` gains `"blank"` (schema.ts, additive: a draft written with four
kinds reads exactly as it did - spec 11 proves an old record, a blank record and an unknown kind);
label `Blank`, key `L`, default 1 x 1, minimum 1 x 1, colour only, `cc: 0` and `channel: 1` as inert
fields so every region is one shape (`freeController` skips it). On the wire it is PAINT ONLY, the
form chosen from four (`docs/entries/sandbox-runtime.md` lists them): its row in `J` is
`{[9]=r,[10]=g,[11]=b}`, its cells in `M` its 1-based index NEGATED, and the paint reads
`local r=J[M[n]]or J[-M[n]]` - only on a surface that has a blank. So the runtime's `S[i]=M[N(x,y)]`
pins a negative number, `J[S[i]]` is nil, `O` returns before `G`: a finger over a blank draws nothing
on layer 2 and sends nothing, `E` on the lift finds nothing to clear, and the runtime text is
untouched (no sixth type code, no branch, `RUNTIME_CALLS` still five). The shared `G` call would
have made "draw nothing" cost eleven characters on EVERY surface (a guard in `O`) - that is why the
negation is on the data side and a surface without a blank is byte-identical. **The cost** (pinned
`compressScript` after `initLuaFormatter()`, the picker corner, three slots): a blank's row is 27
(`{[9]=255,[10]=255,[11]=255}`) plus its comma, plus ONE character - the minus sign - per cell it
covers in `M`, plus the fallback's 11 once per surface; page 3 with a 2 x 2 blank and a 1 x 1 blank
measures Setup 636 -> 708 (+72 = 27+1+27+1+11+5), the Timer and 255/4 unmoved. The preview runs the
same Lua and shows it. `typeCodeOf(blank)` throws (it has no code), `branchOf` returns undefined.

**The meter (answer 12).** Gone from the Sandbox: the two `BudgetMeter` mounts, `N of 908 · room
for about M more`, `of 908 · no room for another`, `measuring…`, the over line, `surface-meters` and
`meter-line`. The measurement still runs (the route calls `landSurface` on every change and Store
is still disabled when a string is over); `costOf` and its floor are untouched and still pinned by
`emit.spec.ts` (the route no longer calls it - it measured up to sixteen surfaces per edit for a
number nobody sees). The refusals are worded without numbers: the cap `This surface is full. Remove
an element to add another.` (`GEOMETRY_COPY.cap`, a string now), duplicate at the cap `This surface
is full. Remove an element to duplicate another.`, Store's `This surface is too full for a ZONA
page. Remove the last element to fit.` (`TOO_FULL_TO_STORE`, DestinationZone's `refusal`). Spec 10
asserts none of the three carries a digit and `copy.ts` spells neither `908` nor `16 elements`.
`BudgetMeter.svelte` stays in the tree UNMOUNTED for the "for now" - `tune-ui.spec.ts`'s meter
assertion is re-aimed to zero mounts and names the by-name deletion (D-12) as your question.

**Copy retired from `copy.ts` (13):** `POSITION_AND_SIZE`, `COLUMN`, `ROW`, `WIDTH`, `HEIGHT`,
`SNAP_HELPER`, `PALETTE_ADD`, `AREA_START`, `roomLine`, `ROOM_NONE`, `overLine` (already
unread), `MEASURING`, `overElementLine`. **Re-worded (3):** `EMPTY_INSTRUCTION` (`Add an element,
or select an area on the surface.` -> `Add an element to the surface.` - the Bible's verbatim named
a path that went), `EMPTY_SECOND_LINE` (`... or click any cell to begin an area.` -> `... or press
F, B, X, K or L and click a cell.`), `DUPLICATE_AT_CAP` (above). **Added (2):** `KIND_LABELS.blank`,
`TOO_FULL_TO_STORE`. **Geometry:** `GEOMETRY_COPY.cap` a string (above); `tooSmall` gains the blank
case. So the copy-exports term moves: `071be34f…` -> `27be101c…`.

**Test ids:** gone `field-col`, `field-row`, `field-w`, `field-h`, `geometry-grid`,
`kind-problem`, `surface-meters`, `meter-line`; new `surface-delete`, `orientation-problem`,
`palette-blank`; `field-kind` is a `<span>` now. The testid term counts source-level values: 318 -> 316, `74e0cfd6…` -> `017609a1…`.

**Counts, carried + delta:** quick 95 / 983 + 1 todo -> **95 / 986 + 1 todo** (+0 / +3:
`sandbox-ui` 9 -> 11, `emit` 6 -> 7), green twice at `--maxWorkers=2` plus the gate's; check
658 -> **658**, 0 / 0; lint clean; sweep `4 19` green (`lua-entries` 1,804); e2e 88 / 103 ->
**89 / 104** (+1 / +1: the selector's walk); utilities **44** -> **44** (0 appeared, 0 disappeared); catalog **27** (no entry
touched); testids 318 -> **316**; OG 27 files, 159,169 B unmoved; `frames.json`, the golden frames, the
preset baseline and the synthetic ZONA byte-identical.

**The gate's terms** (`--before change-10a` at `51e8ff7` on the worktree; `--after change-10a
--against change-10a --check 658` at `e2947fc`): equal - the wire set `7cda3c4b…` and full `bcf8063c…` (every catalog string byte-identical:
no entry touched), all four fixtures by hash-object, the OG 27 files / 159,169 B `9becd682…`, the
utilities 44 -> 44 (0 appeared, 0 disappeared), check 658, lint, quick exit 0 (986 + 1 todo, the
constant moved with it), the build (stamp `e14fd6e`), the refuse-list `--stat` empty, `src/` 18
modified / 0 added / 0 deleted / 0 renamed; moved as a feature moves them - **the sandbox set
`40b44316…` -> `5fcc2955…`: 154 -> 168 records, 154 byte-identical, 0 moved, 0 removed, 14
added, every one of them `S/emit/page3-blanks/`** (the new fixture's landed five, its emitted
three under two and three slots, and at the corner); the census `8995adf5…` -> `522c873a…`
(2,784 -> 2,779 literals: the thirteen retired copy strings, `area` 7 -> 0, `retype` and `started`
2 -> 0, the `$lib/sandbox/cost` specifier 2 -> 0, the `-type` / `-kind-problem` ids; added `Blank`,
`blank` 0 -> 11, the three numberless refusals, the two blank throws, `delete-hit` / `delete-box` /
`delete-glyph`, `escape`, `backspace`, `-orientation-problem`); the copy exports `071be34f…` ->
`27be101c…`; the testids `74e0cfd6…` -> `017609a1…` (318 -> 316); the SCOPED CSS `7f88b4f4…` ->
`e7045f06…` (the Sandbox's own rules: `.plate` cursor default, `.plate.armed`, `.region.selected
.body`, `.delete-hit` / `.delete-box` / `.delete-glyph` in SurfaceEditor; `.side` -> `.key` and
`.row[aria-pressed="true"] .key` in Palette; `.value` added and `.meter` gone in RegionInspector;
`.meters`, `.meter-line`, `.meter-line.over` gone from the route) and the raw CSS `56d81122…` ->
`8c4ed778…`; the titles `39b22413…` -> `fa250c25…` (984 -> 987 vitest: two added, one retitled
in emit and three retitled in sandbox-ui; 103 -> 104 playwright: one added, one retitled); the JS
`a354133c…` -> `e131c040…` (72 -> 71 files: the route's own `cost` chunk went with the meter).
The script exits 1 at the sandbox set by design; the later terms are compared from the two
records. The first `--after` at `e2947fc` (before the fixture) recorded the same wire, census,
copy-export, testid, CSS and JS hashes and `quick exit 1` on the stale 983 (its JSON: 986 passed,
0 failed); it was overwritten by the re-take.

**Chunks** (fresh detached wrangler dev on 4173 on the build at `e2947fc`, stopped through PowerShell,
HTTP 000 after each; your 5173 untouched): c4 **17 passed** (catalog, fidelity, first-experience,
library, sandbox - the six Sandbox titles among them); c5 **11 passed** (artifacts, radius - its
one title walks the Sandbox route to a knob's panel and the delete icon - skeleton, smoke); c2
**3 failed / 19 passed** at three workers (`browse:299`, `:343` - the grid read before hydration,
the flake changes 3, 4 and 6 recorded - and `:1186`, the same shape; the title that names a
Sandbox surface, `:1672`, passed), then **22 passed** at `--workers 1` on a fresh server (rerun
`c2-w1-change10a`). c1 and c3 not run: no install, session or tuning title reads the Sandbox.

**Questions for the user.** (a) A click on an empty cell CLEARS the selection (the focus cell moves
there); say if it should keep the selection instead. (b) `BudgetMeter.svelte` is unmounted and in the
tree for the "for now"; say whether to delete it by name (D-12) with `tune/copy.ts`'s meter family,
or keep it for the meter's return. (c) The delete icon sits diagonally OFF the corner (inside it at
the plate's edge) so it covers no handle and no cell of its own; say if you want it inside the
region's corner instead. (d) A blank carries `cc: 0` / `channel: 1` as inert fields so the region
shape stays one shape; Part B's min / max fields should skip it the way the inspector does. (e) The
cost of a blank includes one character per cell it covers (the minus sign in `M`); a 9 x 9 blank
would cost 27 + 1 + 81 + 11 - say if that is acceptable or if Part B should look at a cheaper cell
encoding when the budget work comes. (f) The empty state's first line is no longer the Bible's
verbatim (`Add an element to the surface.`); say the words if you want others. (g) With a kind armed
the arrows move the focus cell (the keyboard placement route), not the selected element; the
element takes the arrows only under the selector. (h) Changes 1 to 9's questions still stand.

**Not supported by the tree / departures from the brief:** the Bible's section 8 empty-state
sentence is re-worded (above); the release-on-another-cell accelerator went with the area path;
`costOf` is no longer called by the route (it still floors the cap in `emit.spec.ts`, and the cap
is still enforced by the model's `atCap`); `tune-ui.spec.ts` and `tune/copy.ts` were edited though
the brief's file list did not name them (the first pinned the Sandbox's two meter mounts, the second's
ledger line named the room line); the `install.spec.ts` over-budget title reads `TOO_FULL_TO_STORE`
now; ROADMAP / REQUIREMENTS / STATE untouched; CAT-04 stays `[ ]`; the worktree `../hangar-gate-10a`
was removed after the records were copied.

### Done, 2026-09-18, change 10B - the behaviour (part B of the Sandbox feature set)

Five source commits, no push, no device, no deploy: `e8b3d07` feat(sandbox) - the schema's
optional fields and the model's readers; `4685f38` feat(sandbox) - the runtime, the emitter,
`library-trim.ts`, five slots, the cost model, the landing, `runtime.spec.ts` / `emit.spec.ts` /
`install.spec.ts`, the wire harness; `3381d5a` feat(sandbox) - the editor, the copy, the inspector,
the plate, the route, `sandbox-ui.spec.ts`; `4e37b99` chore(sandbox) - four headers back at the
ten-line rule; `f4c0216` test(sandbox) - the e2e walk and the gate's quick constant; then this
paragraph with `docs/TESTING.md`'s "2026-09-18 change 10B", the dated paragraph in
`docs/INSTALL-RUNBOOK.md`, a section in `docs/entries/sandbox-runtime.md`, and the gate records
`gate/change-10b.*` (before, at `654ee60`, in a clean worktree `../hangar-gate-10b` with its own
`npm ci` and build; removed after the records were copied) and `gate/change-10b-after.*` (at `f4c0216`).

**The row and the flag word (answer 12).** `J`'s row is `{col,row,w,h,t,cc,c7,ch,r,g,b}` and then a
TAIL `min,max,flags` written only past the last non-default value, with a spring fader's spring
POSITION as column 15 and a knob's centre in raw units as columns 15 and 16: a region with every
option at its default has the eleven columns it had (the first four are the BOX in cells now,
13-15's kind frame went - the pictures need the cells and the frame is three characters of Lua per
read), and `O` reads the missing columns as `0, 127, 0` once per sample. `c7` is the XY pad's
second controller or the button's radio group; the toggle moved into the flag word: a fader's bit 0
Relative, bit 1 Full, bit 2 Spring; an XY pad's bits 0 and 1; a button's bit 0 Toggle (the schema
keeps `latch`), bit 1 a Note output; a knob's its mode's index 0..3. **The cost per region** in the
Setup at the widest literals (the pinned `compressScript` after `initLuaFormatter()`, the picker
corner): a min alone `,100` +4, min and max `,100,120` +8, Relative `,0,127,1` +8, Relative at Full
+8, Spring `,0,127,4,64` +11, everything on `,127,100,7,127` +14, a knob's centre `,0,127,0,64,83`
+13; page 3 with every option on 489 -> 525 (+36). The alternatives - one column per option (each
boolean `,0` on every later-optioned row) and one packed word for everything (a spring position and
a centre are numbers) - lost.

**The runtime (answers 5 to 11).** Every continuous kind keeps a POSITION 0..127 along its travel
and sends through one scale, `min + (max - min) * v // 127`, folded into `D(s,r,n,c,v)` - scale,
send on change of the sent value, kept in column `n` - so a min above the max inverts the direction
for nothing and the spring value is stored as the first position that lands it exactly (the span is
at most 127 wide; `model.ts` `springPosition`). Relative faders and XY pads hold their position in
FINE units (127 a step, 0..16129): the onset anchors and changes nothing, a move adds the
displacement times 64 or 127, clamped; the value is held between touches; a spring fader's first
touch starts from the spring. **The note-off spelling is status 128 with velocity 0** (ORBIT's, the
note-off proper; on is 144 with the max as the velocity). **The radio group is 1..8** (`GROUP_MAX`,
the seventh column; None is 0): a press turns every other on member off through `K` first, each
sending its off and going dark; a second press on an on toggle in a group turns it off. **The knob's
step rule:** one message per sample carrying the detents crossed in that sample in the encoding
(two's complement `k%128`, binary offset `64+k`, sign magnitude `k>0 and k or 64-k`) - one
normally, two or three on a fast turn, at most 22 by construction (the wrap bounds a sample at 180
degrees), so no cap is written; Min and Max do not apply and the helper says so. **The pictures**
are the runtime's own `glp` on layer 2 through `Q(r,f)` in the region's colour (the paint sets
layer 2's colour once; the library's `G` finger is no longer called): a fader's bar from its low
end to the position (held under Relative, returning to the spring under Spring, cleared under
Absolute); a button's whole region while on (a toggle stays lit); the XY pad's row and column
through the finger's cell; the knob's arc from 7:30 to `position*270//127` under Absolute and the
sector within 45 degrees of the finger under a relative mode, the centre dark, cleared on release.
Layer 1's rest colour is never touched; the preview runs the same five strings.

**The branches, before -> after** (characters, canonical): R 100 -> 249, O 264 -> 323, the fader
`I[1]` 116 and `I[2]` 116 -> one text 503 for both orientations (`I[2]=I[1]`), `I[3]` 143 -> 298,
`I[4]` 218 -> 502, `I[5]` 330 -> 608; new `Q` 111 (the painter), `D` 100 (the scaled send), `K` 114
(a button's off); the runtime alone 1,340 -> 2,817 with every branch, 1,042 -> 2,230 without the
knob. **The trim:** a Sandbox landing writes `library-trim.ts`'s halves, sliced from
`LIBRARY_PARTS` by name (`library.ts` untouched, every catalog record unmoved): **255/0 keeps the
head, the map, `U E X N` and `self:tim()` - 842 -> 460, 382 freed; 255/6 keeps the marker alone -
873 -> 9, 864 freed.** The runtime spends the names the trim retires (`Q D K`) beside `S F I R O`
and calls `E N U X`. `packRuntime` under five slots fills 255/6, 255/0, 255/4 and the Timer LARGEST
PART FIRST (in the parts' own order the five-kind runtime did not pack). **A full-featured
five-kind surface** (page 3, every kind): 255/6 869 (39 free), 255/0 908 (0 free), 255/4 834 (74
free), Timer 783 (125 free), Setup 489; placement R:255/6 O:255/0 Q:Timer D:255/0 K:Timer
I[1]:255/4 I[3]:255/4 I[4]:Timer I[5]:255/6; every combination of kinds fits five (the dearest,
3,394 of 3,632 across the four runtime slots); two slots carry no kind any more, three one kind
alone (the emitter's default stays 2 for the pins). **The cap floor with every option on** - the
number the user no longer sees: the dearest fader (cc 127, channel 16, the corner, min 127, max 100,
Relative at Full, the spring at 100 -> `,127,100,7,123`) fits **eleven** times from an empty
surface before a string is over; from the dearest twelve, fifteen; the dearest sixteen at their
defaults still fit (892 at five slots).

**The VM cases** (`runtime.spec.ts` 7 -> 14 titles, every new one on the trimmed halves under five
slots): 8 min and max on every kind - the fader 20..80 sends 80, 20, 67, the inverted horizontal
100..50 sends 100, 50, 83, the XY pad's one pair 10..20 on both axes, the knob 64..127 sending on
change of the VALUE (22 messages for 45 detents), the button 5..100 on and off; 9 Relative at Half
(a touch sends nothing; 13 then 38 for two moves; held across a lift; clamped at 0 sent once; the XY
pad per axis, the other axis reporting its held value once) and at Full (26, 77); 10 Spring (127
then 64 on the lift, the bar at three rows; Relative with the spring at 100: 121, 100, 121, 100;
the spring value clamped into min..max lands exactly); 11 the button (144:127 / 128:0 at C4; the
toggle's second press; the radio group across three buttons, One's off before Two's on, a momentary
member's release, a second press on an on toggle); 12 the three encodings up and down, two or three
detents in one message, `KNOB_STEP_CAP` 22, a scaled two's-complement knob sending 1 all the same;
13 the pictures per kind and mode read off layer 2 (12 cells, 4 rows, the crosshair's 5 of 9, the
arc's low cell then two then three, the centre dark, layer 1's 48 untouched; the held bar and the
sector); 14 the trim (842 -> 460, 873 -> 9, the twelve trimmed globals, canonical) and every
fixture pressed, moved and lifted on every region with no error. `emit.spec.ts` 7 -> 8: the tail's
trimming and the flag word, the price per option, the pack (page 3 with every option packs as page
3: the runtime is per kind), the landing's five strings the trimmed halves and canonical.

**The sandbox-set diff per fixture** (the gate's `--sandbox` term, 168 -> 361 records): the
twelve fixtures of 10A each keep `emitted-2-slots/mapmode` (the empty string) and, without a knob,
`landed/timer` and `emitted-at-corner/timer` (the arm and the sweep alone) - 30 byte-identical - and
move every other record (138: 13 on `runtime/page3`, `emit/page3` and `emit/page3-blanks`, 11 on
each of the other nine) and gain the five-slot five (`system`, `systemTimer`, `setup`, `timer`,
`mapmode`) - 60 added; the seven new fixtures (`runtime/scaled`, `relative-half`, `relative-full`,
`spring`, `notes`, `knobs`, `emit/page3-options`) add nineteen each - 133; 0 removed. **The base
wire set moved by its thirteen `S/page3/…` records alone** (the Sandbox's page 3 sits in the base
set: its landed five, its emitted and corner three, its two-slot pair); **zero `E/` or `P/` movers -
every catalog string byte-identical, the system halves' catalog records unmoved.**

**Counts, carried + delta:** quick 95 / 986 + 1 todo -> **95 / 995 + 1 todo** (+0 / +9: `runtime`
7 -> 14, `emit` 7 -> 8, `sandbox-ui` 11 -> 12), green twice at `--maxWorkers=2` (`quick-A` 45.1 s,
`quick-B` 54.2 s); check 658 -> **659** (+1, `library-trim.ts`), 0 / 0; lint clean; sweep `4 19`
green (`lua-entries` 1,804 combinations, worst 906 of 908); e2e 89 titles / 104 runs -> **90 /
105** (+1 / +1: the options walk); utilities **44** -> **44** (0 appeared, 0 disappeared); catalog
**27**, no entry touched; testids 316 -> **319** (`field-latch` gone; `field-toggle`, `field-mode`,
`field-speed`, `field-spring`, `field-output`, `field-group` new; `field-spring-value`,
`field-note`, `field-min`, `field-max` the `field-{id}` template); OG 27 files, 159,169 B unmoved;
`frames.json`, the golden frames, the preset baseline and the synthetic ZONA byte-identical.

**The gate's terms** (`--before change-10b` at `654ee60` on the worktree; `--after change-10b
--against change-10b --check 659` at `f4c0216`): equal - every catalog string, all four fixtures
by hash-object, the OG 27 files / 159,169 B `9becd682…`, the utilities 44 -> 44, lint, the
refuse-list `--stat` empty, the build (stamp `f4c0216`), `src/` 17 modified / 1 added / 0 deleted
/ 0 renamed; check 658 -> 659; moved as a feature moves them - the wire set `7cda3c4b…` ->
`d312d08b…` and full `bcf8063c…` -> `3f1d5374…` (the thirteen `S/page3/…` above); the sandbox set
`5fcc2955…` -> `03d5482c…` (above); the census `522c873a…` -> `e38e23d9…` (2,779 -> 2,835
literals, 194 -> 195 files: the five branch texts and the release replaced, `G(s,i,e,x,y,2,…)`,
`Latch`, its helper and `field-latch` 1 -> 0; the thirty new copy strings, the five mode literals,
`absolute` 3 -> 14, `System` / `System timer` 2 -> 4); the copy exports `27be101c…` -> `fe2f7209…`
(`LATCH` / `LATCH_HELPER` retired by name, thirty added: `TOGGLE`, `TOGGLE_HELPER`, `MODE`,
`MODE_ABSOLUTE`, `MODE_RELATIVE`, `MODE_HELPER`, `KNOB_MODE_WORDS`, `KNOB_RELATIVE_HELPER`, `SPEED`,
`SPEED_HALF`, `SPEED_FULL`, `SPEED_HELPER`, `SPRING`, `SPRING_HELPER`, `SPRING_VALUE`, `OUTPUT`,
`OUTPUT_CC`, `OUTPUT_NOTE`, `NOTE_NUMBER`, `NOTE_HELPER`, `GROUP`, `GROUP_NONE`, `groupWord`,
`GROUP_HELPER`, `MIN`, `MAX`, `MIN_MAX_HELPER`, `BUTTON_MIN_MAX_HELPER`, `VALUE_RANGE`, `NOTE_RANGE`);
the testids `017609a1…` -> `23525109…` (316 -> 319); the SCOPED CSS `e7045f06…` -> `5aa7323e…`
(RegionInspector's one new rule, `.helper + .grid, .check + .grid`) and the raw CSS `8c4ed778…` ->
`e6d0ee3b…`; the titles `fa250c25…` -> `25fb536f…` (987 -> 996 vitest incl. todo, 104 -> 105
playwright); the JS `e131c040…` -> `f1caea60…` (71 files). The script exits 1 at the wire by
design; the later terms are compared from the two records. **The gate's quick term** printed `check-counts: no Vitest summary lines found on stdin` / `quick exit 1` in BOTH records (the pipe inside the script; the JSON beside each: 985 passed / 1 failed before, 994 / 1 after - the one `radius.spec.ts` layer B, which reads the built CSS and the script builds AFTER the quick term); the same pipeline run by hand on the built tree at `f4c0216` read `observed 95 files, 995 tests passed, 1 todo` and `matches the expected counts`, exit 0, and the raw suite was green twice more (`quick-A`, `quick-B`).

**Chunks** (fresh detached wrangler dev on 4173, stopped through PowerShell, HTTP 000 after each;
your 5173 untouched): c4 **18 passed** (catalog, fidelity, first-experience, library, sandbox - the
seven Sandbox titles among them); c5 **1 failed / 10 passed** on the tree's build stamped `654ee60`
(`artifacts:63`, the stale stamp against HEAD `3381d5a`), then **11 passed** on the gate's build at
`f4c0216` (rerun `c5-change10b`); c2 **1 failed / 21 passed** at three workers and again at
`--workers 1` (`browse:343`, the grid read before hydration, the flake changes 3, 4, 6 and 10A
recorded), then `browse:343` alone **1 passed** twice on fresh servers (reruns
`c2-alone-343-change10b`, `-2`). c1 and c3 not run: no install, session or tuning title reads the
Sandbox (`install.e2e.ts` names no Sandbox frame; the trimmed halves reach the fake through
`sandbox.e2e.ts`'s loop, re-aimed).

**Questions for the user.** (a) The note-off is status 128 / velocity 0 (ORBIT's spelling); say if
you want 144 / 0 instead. (b) The radio group is 1..8; say if you want more. (c) A relative knob
sends the detents crossed in one sample as one message (usually 1; 2 or 3 on a fast turn); say if
you want exactly one step per message instead. (d) A relative fader or XY pad starts from 0 on its
first ever touch (from the spring value with a spring); say if it should start from the centre.
(e) A second press on an on toggle inside a radio group turns it off (TouchOSC keeps one member
always on); say which you want. (f) The XY pad's relative mode reports the other axis's held value
once on the first move; say if it should stay silent. (g) Sending is on change of the sent value
per region across touches now (13-15 re-sent per contact): a re-press at the same value sends
nothing; say if a fresh touch should re-send. (h) The knob's arc runs 7:30 to 4:30 (270 degrees)
with the centre cell dark; say if you want a full circle or the centre lit. (i) The inline
contingency (`runtime: "inline"`) is retired by name - it read the old frame and could never carry
a knob; say if it should come back. (j) Two slots carry no kind and three slots one kind alone now;
the emitter's default stays 2 for the specs' pins - say if the default should move to 5.
(k) Changes 1 to 10A's questions still stand.

**Not supported by the tree / departures from the brief:** `W` is not a separate scale function -
it is folded into `D` (47 characters cheaper); the knob's centre is two row columns, not the knots
(85 characters of runtime); the fader's two orientations are one text; the pictures set phases
alone and the Setup's paint sets layer 2's colour (+27 in the paint, once); the packer is first-fit
DECREASING (the parts' own order did not pack the five-kind runtime); the inline contingency is
retired; `install.spec.ts`'s landing assertions read the trimmed halves; `hash-wire.mjs` records
the five-slot halves under `--sandbox` (the base set's `S/page3/` records moved with the feature -
the brief's "catalog wire set byte-identical" holds for every `E/` and `P/` record); the gate's
quick term printed "no Vitest summary lines" in BOTH records (the pipe into `check-counts.mjs`
inside the script; the JSON beside it: 985 / 1 before, 994 / 1 after, the one `radius.spec.ts`
layer B on a build that was absent or stale at that point of the script, green in `quick-A` /
`quick-B` and on the fresh build); `docs/CODE-STYLE.md`'s ten-line rule needed a fifth commit
(`4e37b99`); ROADMAP / REQUIREMENTS / STATE untouched; CAT-04 stays `[ ]`; the worktree
`../hangar-gate-10b` was removed after the records were copied.

> bug: in sandbox fader now jumps back to 0 by default thats not should happen

**Done - change 10C, 2026-09-18 (`R` keeps a fader's bar on the lift).** Read against the deployed
`e55398c`: no value is sent on the lift - `D` sends on change only and the release calls nothing
for an Absolute fader - but 10B's release convention CLEARED the fader's bar on layer 2 when the
finger left (`elseif f%2<1 then Q(r)end`, "an absolute fader clears its bar"), so the picture fell
to dark the moment the finger lifted, which reads as the fader falling to 0. A fader holds its
position; only a Spring fader moves on the lift. The clause is gone from `R` (runtime.ts): every
fader's bar stays where the finger left it, the spring return unchanged, a button / an XY pad / a
knob unchanged. `R` is 22 characters shorter, so the pinned figures moved with it (runtime.spec
test 7: five 2817 -> 2795, four 2230 -> 2208, one fader on two slots 1343 -> 1321, page 3 on two
slots 2847 -> 2825, on three [2010, 869] -> [2010, 847], on five [869, 908, 834, 783, 489] ->
[847, 908, 834, 783, 489]; three slots now also fit a fader with a button and a button with an XY
pad; emit.spec test 2's pair [1867, 3394, 1527] -> [1845, 3372, 1527]); test 13 asserts the bar
after the lift instead of the clear. `docs/entries/sandbox-runtime.md` and the runbook's 10B
paragraph say "held on the lift under every mode". Quick 99 files / 1014 tests + 1 todo green at
`--maxWorkers=2`; check 659 / 0 / 0; lint and prettier clean on the touched files; the whole e2e
suite twice on fresh detached servers (a chunk name alone runs everything): 101 passed / 4 failed
each time, every red a `browse.e2e.ts` title of the recorded hydration family (`:299`, `:343`,
`:1186`, and `:1263` / `:1404`), no Sandbox or install title red; `browse.e2e.ts` alone at
`--workers 1` 13 passed / `:1404` red, and `:1404` alone red twice more on a fresh server at 2.6 GB
free - then checked by hand on the built site at 127.0.0.1:4174: the search "pad" shows 11, the
"show" chip on top of it exactly the three the test computes from the source (aurora, pinwheel,
starfield), so the red is the typed query lost to hydration under load, not the listing. No entry,
no library, no schema touched; the catalog wire is byte-identical by construction (the change is
inside the Sandbox runtime's `R`). Not deployed - on the user's word.

11. Sandbox XY pad - multitouch up to five fingers (2026-09-18, queued for Fable)
--------------------------------------------------------------------------------

> add multitouch option for up to 5 fingers for the XY pad in SANDBOX

Facts read before asking: today `O`'s onset expires any other contact holding the region landed
on (one finger per region, 13-15), and the XY pad's state - the held pair, the last sent pair,
the crosshair cell - is per region (`r[17]`, `r[18]`, `r[19]`, `r[20]`, `r[21]`). Questions put to
the user; answers recorded below.

Questions asked 2026-09-18: (1) the fingers on the wire - (a) each finger its own CC pair, the
pad's CC X / CC Y for finger 1 and the next pair up (+2) for each further finger; (b) the same
pair on successive channels. (2) which finger is which - (a) a new finger takes the lowest free
slot; (b) slots by order of arrival, reused only after all lift.

> a, a

Recorded: a `Touches` select on the XY pad, 1 to 5, default 1 (an untouched pad byte-identical to
today); finger n sends X on CC + 2(n-1) and Y on CC Y + 2(n-1); a new finger takes the lowest
free slot; a crosshair per finger; Min / Max and Relative / Speed per finger; every other kind
keeps one finger per region.

**Done - change 11, 2026-09-18 (the XY pad's `Touches`, 1 to 5).** Four source commits, no push,
no device, no deploy: `881146b` feat(sandbox) - the schema's optional `touches` 1..5 (a region
whose last finger's pair would pass 127 is not a region), the model's readers (`touchesOf`,
`fingerController`, `ccCeiling`, `hasMultitouch`, `seventhOf`); `96f4fa3` feat(sandbox) - the
multitouch runtime VARIANT, the emitter, `runtime.spec.ts` 14 -> 16, `emit.spec.ts` 8 -> 9, the
wire harness's six fixtures; `f331083` feat(sandbox) - the editor's `setTouches` and the ceiling
on a typed CC number, the copy's `TOUCHES` / `TOUCHES_HELPER` / `touchesCcRange`, the inspector's
select with its helper and problem line, the route's wire, `sandbox-ui.spec.ts` 12 -> 13;
`df60006` test(sandbox) - the e2e Touches walk and the gate's quick constant; then this paragraph
with `docs/TESTING.md`'s "2026-09-18 change 11", the runbook's dated read-through,
`docs/entries/sandbox-runtime.md`'s multitouch section, the gate script's quick constant at what
the pipeline proves (95 / 999) and the gate records `gate/change-11.*` (before, at `e8c22ec`, on a
clean worktree `../hangar-gate-11` with its own `npm ci`; removed after the records were copied)
and `gate/change-11-after.*` (at `df60006`).

**The tail / flag form and its cost.** Neither: the count rides in the row's SEVENTH column as
`cc2 + 128(touches - 1)` (`model.ts` `seventhOf`) - a one-finger pad's seventh is its `cc2`,
byte-identical - for one to three characters a pad (`22` -> `150` at two fingers, `534` at five)
and no forced tail; the flag word's bits 2..4 would have cost `,0,127,16` (+10) on an
otherwise-default pad and a column of its own `,0,127,0,5` (+11). The runtime reads `r[7] > 127`
as multitouch, `r[7] % 128` as the second controller, `r[7] // 64` as the last slot's offset. The
flag word keeps Relative and Full alone. **The per-slot state**: six columns from `z = 17 + 3k`, k
the finger's controller offset `2(n-1)` and the one number `F[i]` keeps for a contact - z+1 the
held x, z+2 the held y, z+3 / z+4 the last sent pair (`D`'s own columns, so `D` is unchanged), z+5
the crosshair cell (nil while the slot is free - the entry's "lowest free slot" reads it), z+6 the
Relative anchor; slot 1 is 17..22, the single-touch layout plus the anchor. A subtable per slot
lost (`D` writes `r[n]`). **The rule**: `O`'s onset on a multitouch pad expires nobody, walks the
cell columns for the lowest free slot, ignores the finger when every slot is held (no picture, no
message, its samples find no region, its lift silent); `R` runs the pad's branch with no finger so
the slot's cell goes and the union of the other fingers' crosses is redrawn, `F[i]` freed last.
**The CC-base refusal**: `ccCeiling(touches) = 127 - 2(touches - 1)`; a CC number or CC number (Y)
typed past it is refused on its field with `With N touches a CC number is 0 to M, so the last
finger stays inside 127.`, the model untouched; a count the controllers cannot carry is refused
on the select with the same line kept in the state (`touchesProblem`, `touches-problem`) until
the next accepted edit, the select snapping back to the model's value; the schema refuses the
combination whole. **The preview** routes every pointer by its id (`onfinger`, `host.touchDown
(pointerId, …)`, `pointercancel` a lift): a touch screen shows several fingers, a mouse is one
pointer and cannot; multitouch is proved in the VM.

**The runtime, before -> after.** Three texts swapped ONLY when a pad has more than one finger
(`runtime.ts` `MULTITOUCH_TEXT`; every other part the same text): R 249 -> 234 (the guard `if r
then … end`, `r[21]=nil` gone), O 323 -> 392 (+69: the slot walk; the tail defaults moved out of
the per-sample path into the variant's Setup paint, +50 there once, which is what keeps the entry
inside 255/0's 423 beside the trimmed library - at 442 the fader + pad + knob surface was over),
I[4] 502 -> 601 (+99: the union painter, the six columns, the finger's pair); the runtime alone
with every branch 2,795 -> 2,970. **The placement and the fit**: beside a multitouch pad thirteen
subsets of the other kinds fit five slots (`x vx hx vhx bx vbx hbx vhbx xk vxk hxk vhxk bxk`) and
three do not - `vbxk`, `hbxk`, `vhbxk`, the Timer at 956, 48 over: the knob (608), the pad (601)
and the fader (503) each need a runtime slot of their own, the button (298) fits only beside the
fader, the entry fills 255/0, and `R` 234 / `K` 114 / `Q` 111 / `D` 100 have 31 / 27 / 47 / 51 to
share - no packing exists, first-fit or perfect (the union painter as its own part `W` 129 and a
perfect packer were tried and measured; the shared texts stay). The PDF's page 3 with its pad at
two fingers is 869 / 876 / 846 / 956, Setup 489 -> 540, refused on the Timer - Store's
over-budget line; without the knob (869 + 876 + 834 + 381), the button or the fader it fits; the
placement of the multitouch page 3 R:255/6 O:255/0 Q:255/4 D:Timer K:255/4 I[1]:Timer I[3]:Timer
I[4]:255/6 I[5]:255/4. **Every pinned figure unmoved**: `runtime.spec.ts` test 7's (2795 / 2208 /
1321 / 2825 / [2010, 847] / [847, 908, 834, 783, 489]), `emit.spec.ts` test 2's pair (1845 / 3372
/ 1527), the cap floor (`floorFromEmpty` 11, `capFromTwelve` 15, `sixteenFive` 892) and page 3's
placement - the single-touch runtime is the same bytes; **eleven of the dearest faders still fit
from an empty surface with every option on** (the representative is a fader). New pins: test 16's
234 / 392 / 601, 2,970, the thirteen / three, page 3 multitouch's five and its +51; `emit.spec.ts`
test 9's +50 and the 48 over.

**The VM cases** (`runtime.spec.ts` 15, wasmoon, the trimmed halves under five slots): two
fingers on a Touches-2 pad on 50 / 51 and 52 / 53 with independent positions (the second's onset
moves the first's values not at all); the union crosshair - eight of nine cells under two fingers,
five after one lifts, cleared when every finger is up; a third finger ignored (no pair 54, the
held pairs and the picture as they were, its lift silent); the lowest free slot (a new finger is
finger 1 again on 50 / 51, never 54); Relative per finger (31 on one finger's x with the other's
pair silent, 0 then 32 on the other's y, held between touches and continued at 63); a one-finger
pad beside a two-finger one giving test 4's values and taking over as test 2 says; five fingers on
60..69 with a sixth ignored, the middle slot's lift leaving 24 cells lit and the next finger
retaking slot 3 on 64 / 65; `Touches` 1 identical to today's cases (tests 1 to 14 unchanged and
green, page 3's five strings byte-identical with the field at 1). Tests 6, 7 and 14 run the five
change 11 fixtures beside the ten of 10B (the class gates, canonical, a gesture on every region).

**The sandbox-set diff** (the gate's `--sandbox` term, 361 -> 475 records): **every existing
fixture record byte-identical - 361 same, 0 moved, 0 removed** - and 114 added, six new fixtures
times nineteen (`runtime/multitouch`, `runtime/five-fingers`, `runtime/multitouch-relative`,
`runtime/two-pads`, `runtime/page3-multitouch`, `emit/page3-touches`); **the base wire set and
`--full` equal to the before-record** (`1e4ba5c9…`, `627cfb5b…`) - every catalog string and the
base `S/page3/…` records unmoved. **Counts, carried + delta:** quick (the `server` project, the
gate's pipeline) 95 / 995 + 1 todo -> **95 / 999 + 1 todo** (+4: `runtime` 14 -> 16, `emit` 8 ->
9, `sandbox-ui` 12 -> 13), green twice at `--maxWorkers=2` (51.8 s, 52.9 s) - 10C's "99 / 1014"
was both projects (95 / 995 + the sweep's 4 / 19), so with the sweep the tree is 99 / 1018 + 1
todo; check 659 -> **659** (0 / 0); lint clean; sweep `4 19` green (87 s, 1,804 combinations);
e2e 90 / 105 -> **91 / 106** (+1 / +1, the Touches walk); utilities **44** -> **44** (the built CSS
byte-identical, raw and scoped); catalog **27**; testids 319 -> **321** (`field-touches`,
`touches-problem`); copy exports +3 (`TOUCHES`, `TOUCHES_HELPER`, `touchesCcRange`); OG 27 files /
159,169 B, the four fixtures, the refuse-list unmoved; `src/` 11 modified / 0 added. **The gate's
terms** (`--before change-11` at `e8c22ec`, `--after change-11 --against change-11 --check 659` at
`df60006`): wire set and full equal; the sandbox set `4a691b04…` -> `3bdb5974…` (114 added); the
census `96d137f0…` -> `0cb162ee…` (2,835 -> 2,860 literals); the copy exports `fe2f7209…` ->
`09a12df9…`; the testids `23525109…` -> `ee1f20c1…`; the scoped CSS `5aa7323e…` equal, the raw
`e6d0ee3b…` equal; the titles `140f8ef8…` -> `a110ec22…` (996 -> 1,000 incl. todo, 105 -> 106);
the JS `bd8eae97…` -> `3d9cf59c…` (71 files); the script exits 1 at the sandbox set by design; its
quick term read `no Vitest summary lines` before (the pipe, as 10B) and 95 / 999 against the test
commit's 99 / 1018 after - the constant is 95 / 999 since this commit.

**Chunks** (fresh detached wrangler dev on 4173, stopped through PowerShell, HTTP 000 after each;
your 5173 untouched; the build stamped `df60006`): c4 by its five files **19 passed** (the eight
Sandbox titles among them), c5 by its four files **11 passed**; before them two runs of the whole
suite by mistake (a bare chunk name runs everything, 10C's note): **103 / 3** (`browse:343`,
`:1186`, `:1404`) and **105 / 1** (`browse:299`), every red a `browse.e2e.ts` title of the
recorded hydration family and no Sandbox or install title red; then `browse.e2e.ts` alone on a
fresh server **14 passed**. c1, c2, c3 not run as chunks (the two whole-suite runs covered them
green but the four browse reds).

**Questions for the user.** (a) A fader, the button, the knob AND a multitouch pad on one surface
- the PDF's page 3 with its pad at two fingers - does not fit five slots and Store refuses it with
the over-budget line; say if that combination matters enough to re-cut the shared knob (608) or
fader (503) texts, which would move every fixture. (b) A finger past the count is ignored; say if
the newest finger should take the oldest's slot instead. (c) A count the controllers cannot carry
is refused and the select snaps back; say if the controllers should be moved down to fit instead.
(d) Changes 1 to 10C's questions still stand.

**Departures from the brief:** the count is in the seventh column, not the flag word or a column
(measured); the per-slot columns are `17 + 3k` by controller offset with a sixth column for the
anchor, not `17 + 5(slot-1)`; the multitouch machinery is a variant of three texts, never an edit
of the shared `O` / `R`; the variant's Setup paint reads the tail defaults once; the five-kind +
multitouch surface is over and refused; the gate's quick constant is 95 / 999 (the brief's 99 /
1014 counted the sweep); the route's header was already twelve lines at `e8c22ec` and is not
touched beyond one wire line; the worktree was removed after the records were copied. Not
deployed - on the user's word.

12. MIDI clock sync on GHOST, RADAR POINTS, RADAR and STEPS (2026-09-18, queued for Fable)
--------------------------------------------------------------------------------

> MIDI sync works perfectly. implement it to Ghost, Radar points, Radar and Steps.

The bench's word on ORBIT's sync (change 8: `s.rtmrx_cb` fed by `grxm(2,3)`, Start / Continue /
Stop, a Division knob) - the first hardware pass on that shape. Change 8's answer 6 saved the
idiom for the sequencer cards; the user now names four: GHOST, RADAR POINTS, RADAR, STEPS.

RADAR: the card is a ported BOTOR preset compiled from `src/vendor/botor/_pad.ts` (untouchable)
and the front door's ring requires it to stay `padsim` (front-door.ts:122), so the sync cannot go
into it as it is. Options put to the user: (1) leave RADAR as is, RADAR POINTS carries the sync;
(2) rebuild RADAR as a hand-authored Lua card with sync and take it off the front-door ring;
(3) a new card beside the untouched preset.

> 2

Recorded: RADAR is rebuilt as a hand-authored Lua entry (the same sweep, faithful to the ported
preset's picture and sends, plus Sync / Division), and leaves the front-door ring; the ported
preset's Lua stays in the vendor tree untouched. Queued for Fable after the three cards land
(change 12b).

**Done - change 12, 2026-09-18 (STEPS, RADAR POINTS and GHOST on the DAW's clock; RADAR is 12b's).**
Three source commits and one audition commit, no push, no device, no deploy: `63ad1ed`
feat(catalog) - STEPS; `4dd7837` feat(catalog) - RADAR POINTS; `d1519f3` feat(catalog) - GHOST;
`adfaae8` docs(audition) - rows 35, 36, 37 and `ROW_COUNT` 37; then this paragraph with
`docs/TESTING.md`'s "2026-09-18 change 12", a dated section in each of `docs/entries/steps.md`,
`radar-points.md` and `ghost.md`, a dated line in `docs/entries/library.md`, the gate script's quick
constant at the proved 95 / 1002 and the gate records `gate/change-12.*` (before, at `6fd3260` -
the 12b docs commit landed on `cdc1548` while this ran, `src/` identical - on a clean worktree
`../hangar-gate-12` with its own `npm ci`, removed after the records were copied) and
`gate/change-12-after.*` (at `adfaae8`).

**The idiom, ORBIT's spelling on every card.** The Setup: `self.rtmrx_cb=function(s,h,b)if
b==250 then … s.k=0 s.q=0 end if b==250 or b==251 then s.r=1 elseif b==252 then s.r=nil …
elseif b==248 and s.r then local f=s.f if s.q%@DIV==0 and f then f(s)end s.q=s.q+1 end end` then
`grxm(2,@SYNC and 3 or 0)`; `self.q=0` beside the state. The Timer publishes its step routine as
`s.f=f` on every call and ends `if @SYNC then return end f(s)`; its other work stays under both
modes. Where a card holds notes (STEPS, RADAR POINTS) the Timer also publishes a release `s.u=u`,
and the callback reads it on 250 (before the reset) and on 252 - `local u=s.u if u then u(s)end`.
`Sync` Internal / External (`false / true`, `previewIndex` 0: the browser has no clock and the
inspector says so), TUNE-01's six lifted for a sync card by change 8's answer 2.

**STEPS** (eight knobs: `@BPM @ARMC @SWEEPC @TRAIL @SYNC @DIV @NOTE @CH`): the column advance is
the step, `f(s)` = release, k = s.k%8, advance, column k's decay pair, the armed rows' note-ons;
`u(s)` = note-off for the SOUNDING column's armed rows, `(s.k+7)%8` - what Start and Stop release,
so nothing hangs; Division 8th / 16th / 32nd (default 16th); the Tempo rail reads BPM ascending,
`gtt(0,15000//@BPM)` in both events - a 16th a column - `75 100 125 166 250`, today's `200 150 120 90
60` ms exactly, 125 the 120 ms default so the rest frame did not move (a 16th and not the eighth
the header sentence implied: the eighth would put the default at 250 BPM; the old knob comment
already called 120 ms "125 bpm"). The header's "Clock sync is not built" sentence is gone. Setup
420 -> **727** (181 free), Timer 260 -> **361** (547 free); 720 / 359 at the defaults.
**RADAR POINTS** (seven: `@SCALE @ROOT @SWEEPC @PERIOD @SYNC @DIV @CH`): the ring step k is the
step, the eight-step cycle with its three quiet steps kept under the clock (a ping is eight 16ths
at the default Division); `u(s)` releases the pending list `s.z` - Start and Stop release it;
`X(s,20)` stays on the Timer; `@PERIOD` is the finger sweep's period under External. Setup 592 ->
**892** (16 free - the tightest Setup in the catalog; the callback in the Timer was costed ~625 /
~530 and not taken, it would lose a Start inside the first period), Timer 288 -> **380** (528
free); 891 / 378. **GHOST** (seven: `@RECC @GHOSTC @LEN @SYNC @DIV @CCX @CH`): recording stays
real-time on the 20 ms Timer under both modes; the REPLAY index advances on the clock - `Clocks a
point` 1 / 2 / 3, default 1 (at 120 BPM a clock is 20.8 ms against the 20 ms recording, so the loop
plays at very nearly the drawn speed and stretches with the DAW's tempo; ORBIT's 8th / 16th / 32nd
would play a five-second drawing over a minute, so not that set; a `count` rail with the bare
number, because `1 2 3` as mode literals collide with CHORUS's `2` and ORBIT's `3`); Start resets j
(the loop restarts on the bar), Stop freezes the ghost with no sends and the key still pulsing,
Continue resumes; the reset key, the recording, the pair and the decay pair unchanged. Setup 491
-> **730** (178 free), Timer 409 -> **484** (424 free); 725 / 480. No system slot anywhere. **The
default records** moved only by the idiom's own strings (`self.q=0`, the callback, `grxm(2,0)`,
the published routines, `if false then return end f(s)`; STEPS's `gtt(0,15000//125)` is 120) -
`frames.json` `ccb860ca` and the OG `9becd682…` are byte-identical, unregenerated. A STEPS or GHOST
link shared before today lands `unreadable` and opens the card at its defaults (RADAR POINTS is
not in the captured fixture).

**The release published as `s.u`, not `s.o`:** the first RADAR POINTS draft published it as `s.o`,
which is that entry's pitch table; every Timer call then indexed a function, and the existing
RADAR POINTS, residue and parity cases went red on the first full run. Renamed on both cards
before any commit. **The VM cases** (`lua-smoke` 43 -> 46): per card Internal's wire unchanged
(every existing case green untouched), External's Timer moving nothing, Start resetting (and
releasing), a step every `@DIV` clocks, Stop halting (and releasing), Continue from the kept count,
Start again, the Division variants with ORBIT's one-period caveat, the words and the preview;
GHOST's loop replayed one point a clock with the recorded coordinates on CC 16 / 17 and the LED at
phase 252, restarting on Start, three clocks a point over thirty.

**Counts, carried + delta:** quick 95 / 999 + 1 todo -> **95 / 1002 + 1 todo** (+0 / +3), green
twice at `--maxWorkers=2`; check 659 -> **659**; lint clean; sweep `4 19` green; e2e 91 / 106 ->
**91 / 106** (no title added; c3 **21 passed** on a fresh detached server, HTTP 000 after, 5173
untouched; c1 not run - no title reads a card); audition rows 34 -> **37**; utilities **44** -> **44**
(scoped CSS `5aa7323e…` and raw `e6d0ee3b…` equal); catalog **27**; testids **321** equal; copy
exports equal; OG 27 files / 159,169 B equal; the four fixtures equal. **The gate** (`--before
change-12` at `6fd3260`, `--after change-12 --against change-12 --check 659` at `adfaae8`): the wire
set `1e4ba5c9…` -> `53a8e114…`, full `627cfb5b…` -> `850747bf…`, 2,802 -> 2,832 records - **2,595
byte-identical, 204 moved (STEPS 54, RADAR POINTS 76, GHOST 74), 3 removed / 33 added (the three
cross-product records renamed by their state counts 5,120 / 10,000 / 8,000 -> 30,720 / 60,000 /
48,000, and ten new knob-position records a card), 0 outside the three cards**; the sandbox set
`3bdb5974…` equal; the census `0cb162ee…` -> `7300d394…` (2,860 -> 2,862); the titles `a110ec22…`
-> `79ce1bc2…` (1,000 -> 1,003; 106); the JS `3d9cf59c…` -> `35983e18…`; everything else equal; the
script exits 1 at the wire by design; its quick term 95 / 1002 after (the before's read `no
Vitest summary lines`, the pipe as 10B and 11).

**Departures from the brief:** STEPS's column decay pair moved with the step, not left on the
Timer (it names column k; on the Timer under External it would re-arm the held column every period
and paint a column the clock had not reached); a second published routine `s.u` for the release;
STEPS's tempo as a 16th; GHOST's Division as a `count` rail labelled "Clocks a point"; RADAR
POINTS's Setup at 892; the audition rows in their own docs commit before the after-run (two docs
commits); GHOST's channel list generated (the same sixteen strings); the before HEAD `6fd3260`.
`src/vendor/`, `library.ts`, `sequence.ts`, the manifest, `Knob.svelte`, `ColourPicker.svelte`,
`pad-sim.ts`, `firmware-oracle.spec.ts`, RADAR and every other entry untouched; STATE / ROADMAP /
REQUIREMENTS untouched; CAT-04 stays `[ ]`. Not deployed - on the user's word.

**Questions for the user.** (a) STEPS's Stop releases the SOUNDING column's armed rows; say if
all eight rows' notes should be released instead (a DAW that missed a note-on would still be
covered). (b) On all three, a clock inside the first Timer period after a Store is counted and not
stepped (ORBIT's caveat, change 8 (d)). (c) Under External every card waits for the DAW's Start or
Continue - clock alone runs nothing (ORBIT's (c)); GHOST in particular sits after a lift until
play is pressed. (d) RADAR POINTS's Setup has 16 free: the next knob on that card needs the
callback moved to the Timer (costed) or a cut elsewhere. (e) The inspector's held-preview sentence
says "runs the internal tempo here" on GHOST too, which has no tempo knob - a user-facing literal
left as is; say if GHOST should have its own line. (f) STEPS reads Tempo (BPM) at a 16th a column;
say if the eighth reading (the header's old "bar of eighths") is wanted despite 250 BPM as the
default. (g) GHOST's `Clocks a point` is 1 / 2 / 3; say if a "2 points a clock" (faster than
drawn) rung is wanted. (h) Changes 1 to 11's and 8's (b)-(j) questions still stand.

**Done - change 12b, 2026-09-18 (RADAR rebuilt by hand, on the DAW's clock, off the front door).**
Five source commits and one docs commit, no push, no device, no deploy: `9f56f5b` feat(front-door)
- RADAR leaves the row; `7afa4be` feat(catalog) - `src/lib/catalog/entries/radar.ts`, the wiring,
the spec pins, `frames.json`, audition row 38 and `docs/entries/radar.md`; `829473d`, `a72f6a0`,
`85a0f64` test(sweep) - the sweep specs' pins, one run at a time; then this paragraph with
`docs/TESTING.md`'s "2026-09-18 change 12b", a dated line in `docs/entries/library.md`, the gate
script's quick constant at the proved 95 / 1003 and the gate records `gate/change-12b.*` (before,
at `6e09278`, on a clean worktree `../hangar-gate-12b` with its own `npm ci`, removed after) and
`gate/change-12b-after.*` (at `85a0f64`).

**The preset, as read** (its compiled strings are pinned in `docs/entries/radar.md`): layer 2 the
ring colour at phase `sqrt(u*u+v*v)*45` per cell, the firmware's sine walked at fre 254 - the
firmware ADDS fre, so -2 a tick, the crest OUTWARD, 128 ticks a ring at the default detent 2 - under
a keeper the Timer re-arms every 300 s; layer 1 the comet colour 255,170,34 and the library's
`K(x,y,1,252)`; the wire the compiler's first-finger xy emitter - the raw pair on CC 16 / 17,
channel 1, on every sample of the claimant (press, move, fast tap), the release on 3 / >=5, a
second finger silent. Three tune knobs: Colour (the lattice), Speed (1 2 3 4 6 8 12 16), Send
(twelve CC bases). **The rebuild:** that Setup verbatim with `@COL @SPEED @CC` as tokens, the
Timer verbatim (no token), plus `@SYNC @DIV` - five knobs, no `@CH` (the preset had none). Under
External `@SYNC and 0 or 256-@SPEED` freezes the walk at the rest picture and `grxm(2,3)` routes
the clock; the step is a SETUP LOCAL `f(s)` called by the callback directly - not the published
`s.f` of change 12, because this Timer runs every 300 s and `s.f` is the compiler's finger id -
writing every cell's phase to `(s.a[n]-k*32)%256`, eight steps a ring (half a bar at the 16th, a
bar at the 8th, a beat at the 32nd); Start resets k and q so the next clock lands the ring at the
centre, Stop holds it where it is, Continue resumes, no release. `self.rtmrx_cb=function(s,h,b)if
b==250 then s.k=0 s.q=0 end if b==250 or b==251 then s.r=1 elseif b==252 then s.r=nil elseif
b==248 and s.r then if s.q%@DIV==0 then f(s)end s.q=s.q+1 end end grxm(2,@SYNC and 3 or
0)gtt(0,3e5)`. Setup **758** at the defaults, **763** at the worst of the 2,880 states (145 free),
Timer **50**; no system slot. The Speed knob keeps the preset's rates (no BPM ladder lands 1.28 s;
the rail ascends, bigger is faster).

**Faithful, proved:** `frames.json`'s radar block is byte-identical through the regeneration (it
moved to the end with the entry), the OG set is byte-identical (`radar.png` included), and
lua-smoke holds the Lua card against `new PadSim(presetById("radar").state)` frame for frame at
seven ticks, under a finger, after its lift and sixty ticks on, with the wire asserted literal.
**The divergences, D-14 style:** nothing on the wire; the colour knob is a five-colour palette
with the picker rather than the lattice; the sentence carries the house apostrophe at source
(copy.spec.ts; `typographic.spec.ts` now proves the transform on the shelf preset through
`portedEntry`); lua-smoke's keeper guard reads the layer's shape (a sign-turned walk at `+@SPEED`
from `128-p` was measured to keep the rate low and not taken: the sine is symmetric only to
rounding and the frames moved); touch-guard's eighth declared row for the compiler's release
`e==3 or e>=5`; `addedAt` 2026-09-18. **The front door** is seven - `radar` excluded with its
reason, RADAR POINTS's reason no longer names a preset at position 5, the spec's floor of eight
lowered to seven with the reason. **The wire diff:** set `53a8e114…` -> `852b8c14…`, 2,805 ->
2,775: 2,711 identical, 0 moved, 94 `P/radar/…` out, 64 `E/radar/…` in, nothing else; full
`850747bf…` -> `2932fc5a…` (2,737 identical, 95 out, 65 in). **Old links:** `pradar` and any tuned
BOTOR RADAR stamp land `unreadable` under the Lua card and open it at its defaults (stamp.spec.ts,
against real stamps encoded through the shelf preset); RADAR is not in `wild-stamps.json`.
**The VM cases** (lua-smoke 46 -> 47): Internal beside the preset; External - fre 0 from the
Setup, the Timer moving nothing over 300 and 200 ticks, clocks before Start nothing, step 0 on the
first clock after Start (no first-period caveat: the step is in the Setup), the centre at 224 on
the seventh, the finger's pair unchanged, Stop, Continue from clock 7, the eight-step wrap, Start
again; Division 12 and 3.

**Counts, carried + delta:** quick 95 / 1002 + 1 todo -> **95 / 1003 + 1 todo** (+0 / +1), green
at `--maxWorkers=2` four times (one direct, three through the gate); check 659 -> **660** (0 / 0);
lint clean; sweep `4 19` green; e2e 91 / 106 -> **91 / 106** (no title added; four full runs at
104 / 103 / 102 / 104 with every red but two on the recorded browse lines, c3 by its files **21
passed**, c2 **20 passed** with `:299` and `:343` red, alone `:299` green and `:343` red - the
fill-before-hydration shape on a grid that stayed at 27, its expected six untouched by this
change; c4's and c1's files green in all four full runs); audition rows 37 -> **38**; utilities
**44** -> **44**; catalog **27** (stays 27); testids **321** equal; copy exports equal; OG 27 files
/ 159,169 B / `9becd682…` equal; `frames.json` `ccb860ca` -> `3bf1ef05` (moved, not changed), the
three other fixtures equal; census 2,862 -> 2,872; titles 1,003 -> 1,004; `src/` 19 modified / 1
added. **The gate** exits 1 at the wire by design.

**Departures from the brief:** the step in the Setup, not published from the Timer; no `@CH`;
the Speed rail in rates, not BPM; `divergence.ts`'s rows and `presets.ts`'s declaration stay (they
describe the shelf preset, which `presets.spec.ts` still holds against the vendored nine; the tpad
precedent); the description's apostrophe; the keeper guard's shape read; five source commits (the
sweep found three pins one run at a time; each re-ran the gate); the OG not regenerated in effect;
`library.ts`, `sequence.ts`, `src/vendor/`, the manifest, `Knob.svelte`, `ColourPicker.svelte`,
`pad-sim.ts`, `firmware-oracle.spec.ts`, every other entry untouched; STATE / ROADMAP /
REQUIREMENTS untouched; CAT-04 stays `[ ]`. Not deployed - on the user's word.

**Questions for the user.** (a) Eight steps a ring under External (32 phase a step: half a bar at
the 16th) - say if sixteen steps of 16 (a bar at the 16th, a smoother crest) is wanted instead.
(b) The ring under External is FROZEN until the DAW's Start; say if it should keep rolling at the
Speed knob until the first clock lands (the fold would go, the first clock would snap the phase).
(c) No channel knob: say if `@CH` is wanted on RADAR as on the other sync cards (six knobs; the
Setup has 145 free). (d) The Ring speed rail reads the firmware rate 1 to 16; say if a period in
seconds (2.56 to 0.16) is the wanted reading. (e) `browse.e2e.ts:343` is red alone today
(`:299` green alone); its expected six are untouched by this change - say if the recorded flake
should be given a hydration wait in the test. (f) Changes 1 to 12's questions still stand.

13. Sandbox quality of life - clipboard, multi-select, multi-edit, and more (2026-09-20, queued for Fable)
--------------------------------------------------------------------------------

> next up: add basic quality of life improvements that are staple for most modern softwares. make
> it possible to copy, paste and cut elements. Make it possible to be able to select multiple
> elements either with holding shift and clicking or with the selection tool when dragging and
> making a square. Make it that you can adjust/edit multiple elements. research more of these
> typical quality of life and user experience ease features and suggest me more to add.

Readings put to the user 2026-09-20 with thirteen suggestions: (the four) cut / copy / paste on
Ctrl+X / C / V (Cmd on Mac) and Ctrl+D duplicate, paste at the focus cell if free else one cell
down-right of the original, the clipboard kept in the app so it pastes into another surface;
multi-select by Shift+click, a marquee dragged on empty plate with the selector, Ctrl+A; one
outline round the group, moved / deleted / cut / copied / duplicated as one, resize handles on a
single selection only; multi-edit through the inspector - shared fields, "Mixed" where the values
differ, a typed value applies to all, kind-specific fields only when every selected element is
that kind, one Undo per edit. The suggestions: (1) a right-click menu on the plate; (2) a shortcut
cheat-sheet on ? and hints in tooltips; (3) align and distribute a multi-selection; (4) flip /
rotate the surface; (5) fill-to-fit placement; (6) lock an element; (7) double-click to rename on
the plate, auto-numbered names on add; (8) sticky defaults per kind; (9) Tab / Shift+Tab cycles
elements; (10) export / import a surface as .json; (11) a MIDI monitor in Play; (12) grid helpers -
toggle the numbers / names on the plate, a conflicts pass for two elements on one CC and channel;
(13) colour tools - recent colours, apply to all selected.

> this is for sandbox mode obv
> love them all. do it

Recorded: all seventeen, Sandbox only. Where the readings left a choice: a pasted element keeps
the original's settings and colour and takes an auto-numbered name; the marquee selects what it
touches. Built in three serial parts on Fable: 13A the selection model, the clipboard, multi-edit,
lock, Tab; 13B the geometry tools - align / distribute, flip / rotate, fill-to-fit, auto-names and
the plate rename, sticky defaults; 13C the surfaces - the right-click menu, the cheat-sheet,
export / import, the MIDI monitor, the plate toggles and the conflicts pass, the colour tools.

> also keep in mind. the json file for export should be in the way the Grid Editor can implement
> it. this is important:
> we need to be able to export the full lua config from the hangar app as a .json file onto our
> harddisk, locally for our own computer, whether it's mac os, linux or windows - where the app
> website was itself opened. The full lua config is equivalent of a profile-cloud profile. Use a
> reference json file found under grid-userdata/configs. although hangar is a separate app
> developed specifically for ZONA, the configuration created in hangar should be copyable into
> the grid-userdata/configs folder, so it can be opened with the grid-editor desktop app as well.

Recorded 2026-09-20: suggestion 10's export is a Grid Editor profile file - the same shape as a
profile-cloud profile in `Documents/grid-userdata/configs` on this machine - holding the full Lua
config (every slot HANGAR writes), downloaded by the browser to the user's own disk on any OS, so
the file dropped into grid-userdata/configs opens in the Grid Editor desktop app. The export
covers what HANGAR lands: a tuned playground configuration and a Sandbox surface alike. Import
reads the same file back where the content is HANGAR's. Folded into part 13C.

Reference read 2026-09-20 (for 13C): `Documents/grid-userdata/configs/4 Virtual Pages.json` (EF44)
and `New VSN1L Profile 1.json` - a profile file is `{ id (uuid), name, description, type (the
module type string, "ZONA" here), version {major, minor, patch} (the Editor's, strings),
configType: "profile", configs: [ { controlElementNumber, events: [ { event, config } ] } ],
createdAt, modifiedAt (ISO), virtualPath: "" }`; every element the module has is listed with
every event it owns, the untouched ones as `--[[@cb]] --[[Init]]`. HANGAR's ZONA file: element 0
(the pad) with its events and element 255 (system) with 0, 4, 6 - the five slots HANGAR writes,
the rest as Init placeholders; the element / event lists from `@intechstudio/grid-protocol`
(`get_module_element_list`, `get_element_events` for ZONA), the loader in
`C:/Users/sabot/Documents/Claude/grid-editor` read for what it requires. The download is the
browser's own (a Blob and an `<a download>`), which lands in the Downloads folder on every OS.

**Done - change 13B, 2026-09-20 (the geometry tools, the names, the remembered defaults).** Five
source commits, no push, no device, no deploy: `e5ffe7a` feat(sandbox) - the model; `8b05a70`
feat(sandbox) - the plate, the inspector, the palette, the transforms row, the route; `fee54ef`
test(sandbox) - the e2e walk and the gate's quick constant; `a9707ae` fix(sandbox) - the word
"sticky" reworded to "remembered" under `src/` (the gate's CSS term caught Tailwind emitting
`.sticky` off the bare word in comments and a test title, CODE-STYLE section 8; the docs keep
the readings' word); `e7dc6a4` docs - two dash-led lines in TESTING.md's 13A section that read as
a Markdown list and failed the gate's lint term since `715c509`; then this paragraph with
`docs/TESTING.md`'s "2026-09-20 change 13B" and the gate records `gate/change-13b.*` (before, at
`715c509`, on a clean worktree `../hangar-gate-13b` with its own `npm ci`, removed after the
records were copied) and `gate/change-13b-after.*` (at `e7dc6a4`). The runbook is untouched. Every
command below is an editor method, so 13C's menu and cheat-sheet call what the buttons call.

**Align and distribute (suggestion 3).** `editor.alignSelected(to)` with `to` one of `left`,
`right`, `top`, `bottom`, `centre-x`, `centre-y`, and `editor.distributeSelected(axis)` with
`horizontal` or `vertical`; the arithmetic is `geometry.ts`'s `alignBoxes` and `distributeBoxes`
(pure, on boxes). ALIGN: every member to the set's bounding box's edge, sizes kept; a centre puts
the member's middle on the box's, and a half-cell centre FLOORS toward the left or the top.
DISTRIBUTE: the members in the order of their near edge (ties by the other axis), the free cells
between the first's near edge and the last's far edge shared as equal gaps, the REMAINDER to the
first gaps one cell each, the outer two never moving (three at 0 / 2 / 7 two wide, two wide, one
wide: gaps 2 then 1, the middle at 4). Each is one entry under `align` / `distribute` through
`applyEdits` (13A's pattern): refused whole with the first line where a member would overlap
(`overlapLine`, the bystander named) or leave the plate; a locked member refuses with 13A's
`lockedMoveLine` BEFORE anything else is read; the same boxes are no entry (`nothing`); the set
is re-selected on Undo. Spacing with the members wider than the span is refused with its own
line, `DISTRIBUTE_NO_ROOM` (no number, no exclamation mark); under two selected, or three for
spacing, or in Play, `nothing`. THE UI: an Arrange section at the top of the inspector over a set
(`arrange`, `role="group"`): eight 44px icon boxes with straight-line glyphs (an edge and two bars;
a centre line through two bars; three bars) and accessible names `Align left edges` / `Align right
edges` / `Align top edges` / `Align bottom edges` / `Center horizontally` / `Center vertically` /
`Space out horizontally` / `Space out vertically`, the two spacings DISABLED under three members
(chosen over a click that does nothing), the helper `ARRANGE_HELPER` describing each; the status
line reads `Aligned N elements.` / `Spaced out N elements.` (`alignedLine` / `spacedLine`). The
test ids are `arrange-left` / `-right` / `-top` / `-bottom` / `-centre-x` / `-centre-y` /
`-space-x` / `-space-y` - NOT `align-*`: Tailwind's scanner reads a test id as a token and
`align-top` / `align-bottom` are its `vertical-align` utilities (D-21's 44 would have moved).
Keyboard: none here - the cheat-sheet is 13C's; the names above are the commands'.

**Flip and rotate (suggestion 4).** `editor.transformSurface(kind)` with `flip-horizontal`,
`flip-vertical` or `rotate`; the box arithmetic is `geometry.ts transformBox` on the 9 x 9:
`col' = 9 - col - w`, `row' = 9 - row - h`, and a quarter turn clockwise sends the cell (c, r) to
(8 - r, c), so a box's origin is (9 - row - h, col) and its sides swap. Every region moves - a
LOCKED one too, because a surface transform is not an element edit (the row's helper says so) -
the kinds are unchanged, a fader's orientation FLIPS on a turn (a 2 x 6 vertical becomes a 6 x 2
horizontal; spec 18 holds four turns and two of either flip to the identity, orientation
included), and the result is re-validated whole (each region through `validate`, the map) and
refused with the first line if it fails (it cannot for a valid surface: a square plate and the
orientation following the turn keep every minimum). One entry under `transform`, the selection
kept; `nothing` in Play or on an empty surface. THE UI: a row of its own under the toolbar,
`SurfaceTransforms.svelte` (`surface-transforms`) - three 44px straight-line icon boxes
`flip-horizontal` / `flip-vertical` / `turn-surface` (not `rotate-*`, the same Tailwind reason)
named `Flip left to right` / `Flip top to bottom` / `Rotate a quarter turn clockwise`, the helper
`TRANSFORM_HELPER` beside them, all three off in Play (described by the mode line) and on an empty
surface; the status line reads `Flipped the surface left to right.` / `... top to bottom.` /
`Rotated the surface a quarter turn clockwise.`. NOT in `SurfaceActions.svelte`: that file is the
export control (the brief's "wherever Undo / Redo / Save copy live" is the route's toolbar row,
which is at 353px of 372 at the compact band - three more boxes would wrap it), so the row is new.

**Fill-to-fit (suggestion 5).** THE READING: a plain click places the default size as at 10A;
**Alt+click (Option+click on a Mac) places the armed kind grown to fill the free area around the
cell**; the palette's helper says so while a kind is armed (`palette-fill-helper`, described from
the armed row: "A click places the element at its default size. Alt+click (Option+click on a
Mac) fills the free area around the cell."); Alt+Enter on the plate is the same at the focus
cell. THE ALGORITHM, `geometry.ts largestFreeBox(cell, map, square)`: the TRUE maximal-area
rectangle of free cells containing the cell - every rectangle that holds it is enumerated (at
most 2,025 on a 9 x 9) and the one with the MOST CELLS wins, on a tie the
SQUARER (the smaller |w - h|), then the HIGHER, then the FURTHER LEFT; a knob takes the largest free
SQUARE holding the cell (`square`); a held cell is undefined. Pinned in spec 19 on a fixture with
obstacles (a 2 x 6 fader at the top-left, a 2 x 2 button at (4, 0), a 1 x 1 blank at (3, 4)): the
cell (5, 4) fills columns 4..8 by rows 2..8 (35 cells, beating the 7 x 4 strip below the blank);
the square around (7, 7) is 5 x 5 at (4, 3) - a 6 x 6 would hold the blank, and of the two 5 x 5
that hold the cell the higher wins; the cell (2, 2) takes the 7 x 2 band under the button (14,
beating the 1 x 9 column); an empty plate's corner cell is the whole plate. The editor
(`clickCell(col, row, shift, fill)`, `mark(fill)`, `fillBox(kind, cell)`): the kind's MINIMUM
refuses a hole too small with rule 3's line (a knob on a 2 x 2 hole: "A knob needs at least 3 x 3
cells..."); a held cell FALLS BACK to the default size, which the overlap refuses; and A FADER
TURNS ALONG THE LONGER SIDE - horizontal when the rectangle is wider than tall, vertical otherwise
(an addition to the brief, recorded below as a question). While Alt is held over the plate the
proposed bounds show the fill instead of the default size (`altHeld`, off the state's cell map).
A hotkey stays armed as at 10A; the click's outcome and its name are `place`'s.

**Names (suggestion 7).** Every creation path takes `autoName(kind, regions)` - the kind's label
and the lowest free number: the starter (`Fader 1`), a hotkey or palette placement, a fill
placement, a duplicate and a paste (spec 19 walks all five and the gap refilled after a delete);
THE TEMPLATE'S TWO KEEP THE PDF'S NAMES `Filter` and `Hold` (a named starting point, not an add;
recorded as a question). THE INLINE RENAME: a double-click on an element with the selector opens a
field over it (`surface-rename`: absolutely positioned in the plate's own percentages from the
region's box, the display face, uppercase like the name it replaces, square by preflight's input
rule; its text selected on open), Enter commits, Escape cancels, blur commits;
`editor.renameElement(id, name)` trims, refuses an empty or unchanged name, and is ONE entry under
`rename` sealed at once (a second rename is a second entry; the inspector's field keeps its
coalescing key); the selection is not moved; never in Play. THE FIELD'S KEYS AND POINTER STAY ITS
OWN (`stopPropagation` on keydown and pointerdown), so Escape in it clears no selection, Delete in
it deletes nothing, and the window's listener ignores it as it ignores every input; a double-click
inside the field is the field's. A11Y: the inspector's name field stays the keyboard route (no F2;
a question below); the field is labelled `Element name`.

**Remembered defaults (suggestion 8; "sticky" in the readings).** THE STORE:
`hangar.sandbox-defaults.v1`, the eighth owned key (`schema.ts`: `StoreName` gains
`sandbox-defaults`, `SANDBOX_DEFAULTS_KEY`, `OWNED_KEYS` eight - `local.spec.ts` test 1's pin
moves 7 -> 8), holding `{ schema: 1, kinds: { fader?: {...}, button?: {...}, knob?: {...}, xy?:
{...} } }` - one `KindDefaults` record per kind that has any: `channel`, `min`, `max`, `mode`,
`speed`, `spring`, `springValue`, `latch`, `output`, `group`, `touches`, `note` (a button's note
number while its output is a note - the region's `cc` then), every one optional and validated
field by field (`isKindDefaults`, the region's own ranges and word lists; `isSandboxDefaults`
refuses another version, an unknown kind, a bad field, a non-object `kinds`); a corrupt or foreign
envelope reads as the one empty value `NO_DEFAULTS` and is replaced by the next write.
`store/sandbox-defaults.ts` reads, writes and removes (the store an argument, no-ops on undefined).
THE FIELDS THAT ARE REMEMBERED, per kind (`editor.ts REMEMBERED_FIELDS`): a fader's channel, min,
max, mode, speed, spring, spring value; an XY pad's channel, min, max, mode, speed, touches; a
knob's channel, min, max, mode; a button's channel, min, max, toggle, output, group, note; a blank
nothing. NOT remembered: the controller (`freeController` as before), the colour (the palette's
cycle, as the brief said), the orientation (not in the readings' list; a question), the name, the
geometry, the lock. WHEN: whenever a field is edited on ONE element - a typed MIDI field, an
option, the toggle (`applyPatches` with one edit under `midi` / `option` / `latch`) - the kind's
record is REPLACED by that region's remembered fields as they stand (`rememberKind`; a field the
region lacks is absent, so a later region takes the model's own) and the route is told
(`EditorOptions.ondefaults`) and writes the store; a MULTI-EDIT REMEMBERS NOTHING (the brief's
default); nor does a rename, a recolour, a move, a lock or the orientation. HOW A NEW ELEMENT
TAKES THEM (`withKindDefaults`, in `newRegion` after the kind's shape): only the kind's own
fields, only a mode the kind offers; a button on Note takes the remembered note as its
controller; an XY pad's controllers are kept under the ceiling its remembered touch count allows
(`cc <= ccCeiling(touches) - 1`, `cc2 = cc + 1`). A paste and a duplicate keep the ORIGINAL's
settings, not the defaults (13A's rule, spec 20 holds it). RESET: `editor.resetDefaults()` -
every kind back to the model's own, told to the route (an empty envelope REMOVES the key), NOT an
entry (Undo does not take it back; the helper says so). WHERE: the inspector's no-selection panel,
a `New elements` section after Appearance - the helper `DEFAULTS_HELPER` ("A new element takes
the settings you last gave its kind: channel, range, mode and the rest. Its color still follows
the palette."), the outlined `Reset defaults` (`reset-defaults`, one click, no confirmation,
disabled in Play with the other fields), `RESET_DEFAULTS_HELPER` ("Reset returns every kind to its
built-in settings. It changes the stored defaults, not the surface, so Undo doesn’t take it
back."); the panel's notice reads `DEFAULTS_RESET_LINE` ("Defaults reset. The next new element
starts from the built-in settings.") until the next change - the notice renders with or without
a selection now (it was under `{#if any}`). The schema of a region is unchanged.

**Copy added (26, `copy.ts`):** `ARRANGE`, `ARRANGE_HELPER`, `ALIGN_LEFT`, `ALIGN_RIGHT`,
`ALIGN_TOP`, `ALIGN_BOTTOM`, `ALIGN_CENTRE_X`, `ALIGN_CENTRE_Y`, `DISTRIBUTE_X`, `DISTRIBUTE_Y`,
`alignedLine`, `spacedLine`, `DISTRIBUTE_NO_ROOM`, `TRANSFORM_HELPER`, `FLIP_HORIZONTAL`,
`FLIP_VERTICAL`, `ROTATE`, `FLIPPED_HORIZONTAL`, `FLIPPED_VERTICAL`, `ROTATED`,
`PALETTE_FILL_HELPER`, `NEW_ELEMENTS`, `DEFAULTS_HELPER`, `RESET_DEFAULTS`,
`RESET_DEFAULTS_HELPER`, `DEFAULTS_RESET_LINE` (D-05's register: sentence case, the action then
the result, no exclamation mark; "Center" as `Color` is spelled). **Test ids** (327 -> 332 by the
census, which counts literal attributes): `arrange`, `reset-defaults`, `palette-fill-helper`,
`surface-rename`, `surface-transforms` literal; the eight `arrange-*` and `flip-horizontal` /
`flip-vertical` / `turn-surface` ride through an array's `data-testid={id}` like `field-locked`
through the check snippet; every existing id kept. **The Bible's register:** no `border-radius`
anywhere new (the plate's spec forbids even the token in `SurfaceEditor.svelte`, so the rename
field is square by preflight's `input { border-radius: 0 }` - radius.e2e.ts's header; the icon
boxes declare `0`; every glyph is `<line>`s, no `<path>`, no `rx`); the allowlist stays empty.

**Counts, carried + delta:** quick 95 / 1006 + 1 todo -> **95 / 1010 + 1 todo** (+4:
`sandbox-ui` 16 -> 20), green twice at `--maxWorkers=2` (run A at the interface tree, run B at
`e7dc6a4` on the gate's build) and once more in the first after-run at `fee54ef`; check 661 -> **663**
(`sandbox-defaults.ts`, `SurfaceTransforms.svelte`); lint clean (after `e7dc6a4`); e2e 92 / 107 ->
**93 / 108**; utilities **44** -> **44** (0 appeared, 0 disappeared - after the reword; the first
after-run at `fee54ef` read 45 with `sticky` appeared); catalog **27**; testids 327 -> **332**;
copy exports +26; OG, the four fixtures, the refuse-list unmoved; `src/` 11 modified / 2 added /
0 deleted / 0 renamed. **The gate's terms** (`--before change-13b` at `715c509`; `--after
change-13b --against change-13b --check 663` at `e7dc6a4`): **the wire set `852b8c14...`, the full
`2932fc5a...` and the sandbox set `3bdb5974...` equal - byte-identical, every catalog string and
all 475 Sandbox fixture strings** (no Lua moved: no fixture was rotated, and nothing 13B adds is a
row column); the census `0df9e009...` -> `7cdbc682...` (2,914 -> 2,973 literals); the copy
exports `eea5099f...` -> `5e3df867...`; the testids `ed13451f...` -> `62f8f758...`; the scoped
CSS `68eedc24...` -> `8ef5e915...` and the raw `4e8f13ba...` -> `d81eb2c0...` (the Sandbox's own rules,
named in TESTING.md); the titles `6c609832...` -> `e664ad7d...` (1,007 -> 1,011 vitest incl. todo;
107 -> 108 playwright); the JS `c32244aa...` -> `06754f71...` (71 files); comment lines: every header at
the rule by `comment-lines.mjs --todo`. The script exits 1 at the census by design; the before's
quick term read `quick exit 1` (the pipe, as 13A); the first after's (at `fee54ef`, the build
fresh) 95 / 1010, exit 0; the second after's (at `e7dc6a4`) the recorded ordering hole - the
quick term runs before the build term and `a9707ae` touched the route, so layer B refused the
stale build (JSON 1009 / 1 / 1 todo) - and run B on that record's build read 95 / 1010, exit 0.

**Chunks** (a fresh detached wrangler dev on 4173 each, stopped through PowerShell, HTTP 000 after
each; 5173 untouched): at `fee54ef` c4 by its five files **21 passed** (the ten Sandbox titles
among them) and c5 by its four files **11 passed**; at `e7dc6a4` c4 **21 passed** and c5 **11 passed**.
One hole: the first run of `sandbox.e2e.ts` alone (at `8b05a70`'s tree, free memory 0.51 GB)
lost its server after one document - every title `ERR_CONNECTION_REFUSED`, wrangler's log ending
at `GET /sandbox/ 200` with no crash line; the same file on a fresh server read **10 passed** in
20.5 s. c1, c2 and c3 not run: no install, session, browse or tuning title reads the Sandbox.

**Questions for the user.** (a) Plain click = default size, Alt+click = fill, as decided; say if
fill should be the default (or a tool of its own). (b) A filled fader turns along the longer side
(horizontal when wider than tall); say if it should stay vertical. (c) The template's two keep
`Filter` and `Hold`; say if they should be `Fader 1` / `Button 1`. (d) Orientation and colour are
not remembered (the readings' list); say if orientation should be. (e) The three transforms sit
in a row of their own under the toolbar, not beside Save copy; say if you want them in the
toolbar (it wraps at 1024) or in the inspector. (f) The two spacing boxes are disabled under
three members; say if they should click to a line instead. (g) A half-cell centre floors toward
the left / top and the spacing remainder goes to the first gaps; say if you want the other side.
(h) The inline rename has no keyboard trigger (F2 would be three lines) - the inspector's field
is the keyboard route as briefed. (i) Reset defaults is disabled in Play like every field; say
if it should stay live. (j) A locked member refuses align / distribute whole with its line (13A's
rule) rather than being skipped. (k) The source says "remembered" where the readings say
"sticky" (Tailwind); the interface says neither - `New elements`. (l) 13A's and 1 to 12b's
questions still stand.

**Departures from the brief:** the transforms are not under `SurfaceActions.svelte` (the export
control) but a new `SurfaceTransforms.svelte`; the arrange ids are `arrange-*` and the turn
`turn-surface` for Tailwind's sake; a filled fader's orientation follows its shape; `onclick`
takes a fourth argument (`alt`) and `onmark` a `fill` flag - test 2's and 14's pins moved with the
wiring, 9's with the no-selection sections; `local.spec.ts`'s `OWNED_KEYS` pin at eight; the word
"sticky" is not in `src/`; TESTING.md's 13A section lost two line-leading dashes (its words are
the same) so lint passes; the runbook untouched; STATE / ROADMAP / REQUIREMENTS untouched; CAT-04
stays `[ ]`; not deployed.

**Done - change 13C, 2026-09-21 (the surfaces: the menu, the sheet, the profile file, the Play
monitor, the view toggles and the shared-controller pass, the recent colours; the user asleep,
nothing asked).** Four source commits and one docs commit, no push, no device, no deploy:
`ac2e602` feat(sandbox) - the model (menu.ts, shortcuts.ts, conflicts.ts, play-monitor.ts,
share/profile.ts with profile-copy.ts, transfer.ts's `downloadText`, schema.ts's two new keys with
store/sandbox-view.ts and store/sandbox-colours.ts, copy.ts's thirty-eight strings, profile.spec.ts
new, sandbox-ui.spec 20 -> 27, local.spec's `OWNED_KEYS` pin at ten); `5960847` feat(sandbox) - the
interface (ContextMenu, ShortcutSheet, ViewToggles, PlayMonitor and ProfileActions new; the plate,
the inspector, the palette, the two routes); `018bfe5` test(sandbox) - the e2e walk and the gate's
quick constants at 96 / 1026; `c177e20` fix(sandbox) - the menu's action ids reworded (`align-*` to `arrange-*`, `distribute-*` to `space-*`, `select-all` to `select-every`: the first after-run's CSS term found Tailwind emitting `align-top`, `align-bottom` and `select-all` off the string literals - CODE-STYLE section 8, 13B's own lesson - so the stylesheet stays at 44; no label moves); then this paragraph with `docs/TESTING.md`'s "2026-09-21 change
13C", the runbook's "Grid Editor profile files" section and the gate records `gate/change-13c.*`
(before, at `8878084`, on a clean worktree `../hangar-gate-13c` with its own `npm ci`, removed
after the records were copied) and `gate/change-13c-after.*` (at `c177e20`, in the working tree -
change 14's commits `27ef042` .. `5cad32c` landed in the same tree between the two records, and
the wire's movers are theirs, stated below). Every command the menu and the sheet name is an
editor method 13A / 13B exposed; nothing here is a second implementation.

**The menu (suggestion 1).** `sandbox/menu.ts menuItems(state, clipboardHeld)`: TEN ITEMS IN ONE
ORDER on an element, a set and the empty plate alike - Cut, Copy, Paste, Duplicate, Delete, Lock
(reading Unlock when every member is locked), Rename, Align ▸ (13B's six alignments by their own
names), Space out ▸ (the two spacings), Select all - each with its keys (`Mod+X` and so on, `Mod`
the platform's word); an item that cannot run is DISABLED WITH ITS REASON AS ITS TITLE, never
hidden: nothing selected -> `Select an element first.` on the five selection-bound ones; a locked
member -> 13A's `lockedDeleteLine` on Cut and Delete and `lockedMoveLine` on every alignment and
spacing; no clipboard -> `NOTHING_TO_PASTE`; the cap -> `PASTE_AT_CAP` / `DUPLICATE_AT_CAP`; under
two -> `Select two or more elements to align them.`; under three -> `Select three or more elements
to space them out.`; more or fewer than one -> `Select one element to rename it.`; an all-locked or
empty surface -> `There’s nothing to select.`; in Play no menu at all. THE PLATE
(`SurfaceEditor.svelte oncontextmenu`): a right-click on an element outside the set selects it
alone first (the click's own rule), on an empty cell keeps the set and moves the focus cell there
(so Paste lands on it - `onfocuscell` -> `editor.setFocus`), never in Play (the browser's own menu
is left alone); `Shift+F10` and the Menu key open it over the selection's corner or the focus
cell. `ContextMenu.svelte` (`surface-menu`, `menu-<id>`, `menu-align-submenu`): real menuitems at
the 44px floor, the submenus opening on hover, click or the right arrow, the arrows walking the
enabled items and wrapping, Home and End, Enter or Space running the focused one, the left arrow
or Escape closing a submenu, Escape or Tab closing the menu, a press outside closing it before the
plate reads the press; every key is the menu's while it is open; the menu grows toward the other
edge past the plate's middle so it stays on the plate; focus returns to the plate on close. Rename
opens the plate's own inline field (13B's) on the single; every other action is the route's
`menuAction` switch onto `cut` / `copy` / `paste` / `duplicate` / `remove` / `toggleLock` /
`selectAll` / `align(...)` / `distribute(...)` - the same functions the keys call, so the status
line reads the same words. The clipboard's state for Paste is the route's (`clipboardHeld`, set on
copy and cut, read from the session store on mount).

**The sheet (suggestion 2).** `sandbox/shortcuts.ts SHORTCUT_GROUPS` is THE SOURCE OF TRUTH: five
groups (Add elements, Select, Edit, History, Menus and help), every row with the keys as shown
and `reads` - the `event.key` values the handlers read for it - so spec 22 holds the sheet
COMPLETE AGAINST THE HANDLERS' SOURCE in both directions: every literal the route's
`onWindowKeyDown` reads (`z y c x v d a l escape delete backspace ?` and the hotkeys through
`kindForKey`) and every one the plate's `onkeydown` reads (the arrows, Tab, Enter, Space, Escape,
Delete, Backspace, Alt, ContextMenu, F10) is named by a row, and no row names a key nothing reads
(the pointer rows - click, Shift+click, a drag on empty, Alt+click, a double-click, a right-click
- read none). THE PLATFORM IS READ IN ONE PLACE: `platformOf(navigator)` (User-Agent Client Hints'
`userAgentData.platform` first, else `navigator.platform`) into `isMacPlatform`
(`/^mac|^ip(hone|ad|od)/i`), and `keysWord` turns `Mod` into Ctrl or Cmd and `Alt` into Option;
the route passes `mac` down; spec 22 scans every Sandbox component and the route for
`userAgentData` and `navigator.platform` and finds neither. `ShortcutSheet.svelte`
(`shortcut-sheet`, a `role="dialog"` with `aria-modal`, one table with Keys and Does, a `tbody` per
group with its title row, `shortcut-row` per row, `<kbd>` per key): `?` opens it (the route's
window listener; modal while open - Escape closes and every other key waits), the `?` box in the
View row (`shortcuts-open`) opens it for the mouse, Escape / Close / a press on the scrim close it,
Close takes focus on open and Tab stays inside, focus returns to the opener. TOOLTIPS (D-05's
register, `titledWithKeys`): every palette row `Fader (F)`, Undo `Undo (Ctrl+Z)`, Redo `(Ctrl+Y)`,
Duplicate `(Ctrl+D)`, Delete element `(Delete)`, the View toggles their helpers, the `?` box
`Keyboard shortcuts (?)`; Cmd on a Mac.

**The Grid Editor profile file (suggestion 10, the user's note - the important one).**
`share/profile.ts`. THE SHAPE, the reference files' (`4 Virtual Pages.json`, `New VSN1L Profile
1.json`): `{ id, name, description, type: "ZONA", version: { major: "1", minor: "6", patch: "8" },
configType: "profile", configs: [ { controlElementNumber, events: [ { event, config } ] } ],
createdAt, modifiedAt, virtualPath: "" }` - ten keys, the version the desktop Editor's (its
package.json at HEAD reads 1.6.8; the reference files carry the same three strings), `id` a uuid
v4 from `crypto.randomUUID()`, both moments the export's ISO moment. THE ELEMENT AND EVENT LIST,
read from `@intechstudio/grid-protocol` at 1.20260825.1135 (`grid.get_module_element_list(
ModuleType.ZONA)` is a 256-long list with `touch` at 0 and `system` at 255 and nothing else;
`get_element_events`): the pad 0 with setup (0) and timer (6), the system element 255 with setup
(0), utility (4) and timer (6) - EXACTLY sequence.ts's five SLOTS, proved both ways in
profile.spec.ts test 1, so every event ZONA owns carries one of HANGAR's five strings verbatim
(0/0 setup, 0/6 timer, 255/0 system, 255/4 systemUtility, 255/6 systemTimer) and THE INIT
PLACEHOLDER IS NEVER WRITTEN on a ZONA; its spelling is kept all the same - `--[[@cb]] --[[Init]]`,
the Editor's own for an untouched event, from `4 Virtual Pages.json` (`New VSN1L Profile 1.json`
shows the Editor also writes a kind's default template there - `--[[@cb]] --[[Button Init]]` on
event 0, the default action blocks on event 3, `--[[@cb]] print("tick")` on event 6 - so the
placeholder's word is the Editor's for a hand-written profile) - kept so the builder is total over
the package's list if a module with more events is ever named. A SAMPLE for the template surface
(Filter and Hold), abridged:
`{ "id": "9b4c...", "name": "My performance", "description": "2 elements. Made in HANGAR.",
"type": "ZONA", "version": {"major":"1","minor":"6","patch":"8"}, "configType": "profile",
"configs": [ { "controlElementNumber": 0, "events": [ { "event": 0, "config": "--[[@cb]]J={{...}}
..." }, { "event": 6, "config": "--[[@cb]]..." } ] }, { "controlElementNumber": 255, "events": [
{ "event": 0, "config": "--[[@cb]]<the trimmed 255/0 half>" }, { "event": 4, "config":
"--[[@cb]]<the runtime's second slot>" }, { "event": 6, "config": "--[[@cb]]<the marker and its
parts>" } ] } ], "createdAt": "2026-09-21T...Z", "modifiedAt": "2026-09-21T...Z", "virtualPath":
"", "hangar": { "schema": 1, "kind": "sandbox", "exportedAt": "...", "app": "hangar", "record":
{ ...the SandboxRecord with the surface... } } }` - four-space JSON with no trailing newline, as
the Editor's own writer (`JSON.stringify(config, null, 4)`) indents. THE STRINGS ARE WHAT A STORE
WRITES: the Sandbox's export reads `landing.config` - land.ts's five under `SLOTS` 5, the trimmed
system halves with the runtime's parts, 255/4 the runtime's second slot, canonical under the
pinned minifier, the same object `install.observeConfig` is handed - and the workspace's reads
the tuner's `configStrings` (the entry's Setup and Timer at the current knobs plus the full
library halves and 255/4), the same pair Store on ZONA writes; profile.spec.ts test 3 lands the
template surface through `landSurface` and holds every event's config `toBe` the landing's.
THE LOADER'S REQUIREMENTS, read from `grid-editor` at HEAD (2026-09-21): `src/electron/src/
profiles.ts loadConfigsFromDirectory` lists every `.json` under `<profileFolder>/configs` whose
parsed `configType` is truthy (`obj.fileName = file; configs.push(obj)`, the object kept whole);
`src/renderer/runtime/runtime.ts GridProfileData.createFromCloudData` reads `configs`, `type`,
`name`, `description`, `id` and NOTHING ELSE - for every index of `get_module_element_list(type)`
that names a type it does `configs.find(e => e.controlElementNumber === index).events` (a missing
element would throw), then `new GridPresetData(type, index, events)` looks each event up with
`element.findEvent(Number(data.event))` (an event the element does not own is SKIPPED, not
refused) and parses each config with `GridAction.parse` (whitespace runs folded, one action per
`--[[@short]]` marker with the body up to the next); `ProfileLoadOverlay.svelte`'s
`isCompatible(module.type, config.type)` is equality outside the VSN1 pair, so `type: "ZONA"`
matches a connected ZONA; `GridPage.loadProfile` sends the system element first, then each
element's events through `event.sendToGrid`, which minifies `toLua()` (`ActionData.toLua` puts ONE
space after the marker) with `GridScript.compressScript` - and HANGAR's canonical string is that
minifier's fixed point, so THE MODULE RECEIVES THE SAME BYTES. PROVED: the loader is not
importable as a pure function (it is a method on runtime classes over the Editor's stores), so
profile.spec.ts test 4 MIRRORS IT READ FOR READ (the `configType` gate, the five keys, the
per-index `find` over the package's list, the per-event ownership, one parsed action per config)
and test 3 runs the Editor's parse regex verbatim on every config, re-serialises with the one
space, minifies through `canonical` and holds the result `toBe` HANGAR's string. WHERE THE HANGAR
PAYLOAD LIVES AND WHY: under a top-level key `hangar`, holding exactly what Export as a file
writes (transfer.ts's `ExportFile` - the surface's record, or the card's record and its rack) -
NOT a comment inside a config string, because a comment costs budget (the pad's Setup is measured
at 908 and a `--[[@hangar {...}]]` block would count) and the loader's reads are the five keys
above: the key is ignored on load (proved by the mirror; the Editor's `saveConfig` writes the
object it is handed whole, so a re-save from the panel is likely to keep it - NOT proved, the
panel is the profile-cloud web component outside this reading - and a fresh export from the
module rebuilds the object, which drops it). THE FILE NAME: `profileFileName` keeps the surface's
or the card's name as the Editor names its own files (`My performance.json`, `ARC.json`), less
the nine characters no filesystem takes (`< > : " / \ | ? *`) and the controls (each to a
space), runs of space folded, a trailing dot dropped, `ZONA profile.json` when nothing is left.
THE DOWNLOAD: `transfer.ts downloadText` - the one door, a Blob, an object URL, an `<a download>`,
one click, the URL revoked after - which Export as a file now goes through too (`downloadExport`
calls it; transfer.spec test 1 unchanged); no Web Serial, no secure context needed. WHERE THE TWO
BUTTONS SIT: (a) the Sandbox's new View row under the transforms - `ProfileActions.svelte`
(`export-profile`, `import-profile`, `profile-outcome`) right of the View toggles, the export
DISABLED WITH ITS REASON while the landing is measuring (`Measuring the surface. Try again in a
moment.`) or over the budget (`A string is over the budget, so the file would not fit a ZONA page.
Remove an element first.` - a Store refuses the same landing), the description `2 elements. Made
in HANGAR.` (the element count line); (b) the workspace's INSPECTOR'S PINNED ACTIONS beside Save
copy and Share snapshot (`export-profile`), NOT the destination zone - the zone renders only with
a ZONA connected (`destination: reportedPage === undefined ? undefined : destination`) and the
export needs no hardware; disabled while the tuner measures or over the budget with the same
reasons; the description the card's listing sentence plus `Made in HANGAR.`; the button reads
`Exported` for two seconds. THE IMPORT (Sandbox only): a file input dressed as the same outlined
box (`accept` transfer.ts's `EXPORT_ACCEPT`); `readProfile` parses, takes the `hangar` payload
only when its `kind` is `sandbox`, and runs it through transfer.ts's own `classifyImport` (the
app word, the schema, the cap, the bounds, the overlap - no second implementation), then the
route mints a fresh surface id, writes the draft, holds the surface for `open()` in case the store
refused, navigates, and says `Imported <name> as a new surface.`; THE REFUSAL RULE: a profile
without the key, or whose payload is a configuration's (a workspace export), or whose payload is
not HANGAR's, reads `This profile doesn’t hold a HANGAR surface, so there’s nothing to open in the
Sandbox.`; a file that is not JSON `This file isn’t JSON, so it can’t be a Grid Editor profile.`;
a payload transfer.ts refuses carries transfer.ts's own line (the region named). The profile's
config strings are never read on import: the surface is the source, the strings its product.

**The MIDI monitor in Play (suggestion 11).** THE HOOK: the Sandbox's preview engine is a
`LuaPadSim` over a `LuaHost` whose `midi` getter is the log of every `midi_send` (`__hangar_gms`
through `self:gms`, the same log runtime.spec.ts reads as `host.midi`); `sim/monitor.ts midiLogOf`
already finds it structurally through the sim's `host` field, and the route hands `() =>
midiLogOf(engine)` to `PlayMonitor.svelte`, which samples it every 100 ms through the Playground
monitor's own `MonitorLog` (the same coalescing window and 200-row cap - no second log) and
formats with `sandbox/play-monitor.ts playMonitorLine`: `CC 16 ch 1 → 64`, `Note on C4 ch 1 →
100` (a note by tune/view.ts's name), `Note off`, `Pitch bend ch 1 → 8192`, and `×N` after a row
that folded N alike messages; the newest TWELVE, newest first, mono and tabular. ITS HOME: under
the plate in the centre column, mounted in Play only (`{#if play}`), so Edit has no list; a
`Clear` box (disabled until a line), the helper `The last twelve messages the surface sent, newest
first. Nothing here reaches a MIDI port.`, the empty line `Nothing sent yet. Touch the surface and
what it sends shows here.` - not the inspector's Play panel, because the inspector is read-only in
Play and the list belongs with the picture it explains.

**The view toggles and the shared-controller pass (suggestion 12).** THE TOGGLES: a View row under
the transforms (`ViewToggles.svelte`, `view-toggles`, `view-numbers`, `view-names`, pressed boxes
reading by their border and ink, never a fill alone), kept per viewer under
`hangar.sandbox-view.v1` (`{ schema: 1, numbers, names }`, both on by default - the plate as it
was; a corrupt envelope reads as both on; store/sandbox-view.ts; the ninth owned key), never in
the draft. THE PLATE: `show` puts `no-numbers` / `no-names` on every region and the CSS drops the
numerals or the names (`display: none`); with the numbers on, every sending kind shows its
controller - the fader its own numeral as before, a button, a knob and an XY pad a NEW one
(`surface-cc`, bottom-left inside the region; a note button its note name; an XY pad `cc cc2`; a
blank none). THE CONFLICTS RULE (`sandbox/conflicts.ts`): what a region SENDS is read off the same
fields the emitter reads - a fader, a knob and a CC button their controller; an XY pad both axes
for every finger (`cc + 2(n-1)`, `cc2 + 2(n-1)` for n = 1..touches); a note button nothing (a note
is not a controller); a blank nothing - and two regions on the SAME CHANNEL sharing a controller
are a pair, reported once per shared controller in the surface's order (`Fader 1 and Button 1
both send CC 16 on channel 1.`), never refused: the editor accepts the second element on the
controller as before (spec 24 types 16 onto both). THE INSPECTOR: a `Shared controllers` section
FIRST with nothing selected, the helper (`Two elements on one controller number and channel send
over each other. That may be what you want; if not, change one of them.`) and one `conflict-line`
per pair; with a selection the panel is the element's. THE PLATE: a mark on each member
(`surface-conflict`: a triangle of straight lines with its bar, top-left inside the region, in the
INK - not the error red, which tune-ui's census keeps to the two meters, the message, the
destination's refusal, the refused field and the brightness field; a warning, not a refusal).

**The colour tools (suggestion 13).** VERIFIED, as the brief asked: 13A's `setColour` applies the
picker's colour to EVERY member of the set as one entry (`applyPatch` over `_selection` under
`recolour`; spec 26 recolours two buttons and reads both), so "Apply to selected" IS the existing
swatch and no second control was added. THE STRIP: under the swatch in Appearance
(`recent-colours`, up to eight 44px `recent-colour` chips in their colour, newest first, each named
`Apply color r, g, b` in the 0..255 the firmware takes), a chip's click is the swatch's own
`oncolour` - the route's `colour()` - so a set takes it whole; disabled in Play; the helper `The
last eight colors you applied. Click one to apply it to every selected element.`. THE STORE:
`hangar.sandbox-colours.v1` (`{ schema: 1, colours: [[r,g,b], ...] }` RGB444, at most eight,
newest first; store/sandbox-colours.ts; `rememberColour` puts the colour first once, the same
object back when it already is; a corrupt or oversize envelope reads as empty; the tenth owned
key), written by the route 300 ms after the picker last moved (a drag through the picker is one
colour), read on mount. `ColourPicker.svelte` and `Knob.svelte` untouched.

**Copy added (52).** `sandbox/copy.ts` (38): `MENU_NAME`, `MENU_CUT`, `MENU_COPY`, `MENU_PASTE`,
`MENU_DELETE`, `MENU_LOCK`, `MENU_UNLOCK`, `MENU_RENAME`, `MENU_ALIGN`, `MENU_DISTRIBUTE`,
`MENU_SELECT_ALL`, `MENU_NEEDS_SELECTION`, `MENU_RENAME_ONE`, `MENU_ALIGN_TWO`,
`MENU_SPACE_THREE`, `MENU_NOTHING_TO_SELECT`, `SHORTCUTS_TITLE`, `SHORTCUTS_KEYS_COLUMN`,
`SHORTCUTS_DOES_COLUMN`, `SHORTCUTS_CLOSE`, `SHORTCUTS_OPEN_GLYPH`, `SHORTCUTS_LEDE`,
`titledWithKeys`, `PLAY_MONITOR`, `PLAY_MONITOR_HELPER`, `PLAY_MONITOR_EMPTY`,
`PLAY_MONITOR_CLEAR`, `VIEW_GROUP`, `VIEW_NUMBERS`, `VIEW_NAMES`, `VIEW_NUMBERS_HELPER`,
`VIEW_NAMES_HELPER`, `CONFLICTS`, `CONFLICTS_HELPER`, `conflictLine`, `RECENT_COLOURS`,
`RECENT_COLOURS_HELPER`, `recentColourName`; `share/profile-copy.ts` new (14): `EXPORT_PROFILE`,
`IMPORT_PROFILE`, `EXPORT_PROFILE_HELPER`, `IMPORT_PROFILE_HELPER`, `EXPORT_PROFILE_MEASURING`,
`EXPORT_PROFILE_OVER`, `MADE_IN_HANGAR`, `surfaceDescription`, `configDescription`,
`profileExportedLine`, `PROFILE_EXPORTED`, `importedLine`, `PROFILE_NOT_JSON`,
`PROFILE_NOT_HANGAR_SURFACE` (D-05's register; "color" as the interface spells it; the sheet's
group titles and rows are shortcuts.ts's own words). **Test ids** (332 ->
359 by the census): `surface-menu`, `menu-{item.id}`, `menu-{item.id}-submenu`,
`menu-{sub.id}` (the ten ids and the submenus ride through the template), `shortcut-sheet`,
`shortcut-sheet-close`, `shortcut-table`, `shortcut-group`, `shortcut-row`, `shortcuts-open`,
`view-toggles`, `view-numbers`, `view-names`, `play-monitor`, `play-monitor-clear`,
`play-monitor-empty`, `play-monitor-lines`, `play-monitor-line`, `profile-actions`,
`profile-outcome`, `export-profile` (both routes), `import-profile`, `surface-cc`,
`surface-conflict`, `conflict-line`, `recent-colours`, `recent-colour`; every existing id kept.
The Bible's register: no `border-radius` above zero anywhere new (every new box declares `0`; the
plate's spec still forbids the token in SurfaceEditor.svelte; the mark is a `<polygon>` and two
`<line>`s, no `rx`); the allowlist stays empty; identity's selection rule untouched (no
`--color-raised` fill for a pressed or hovered state - the menu's hover and the toggles' pressed
state read by ink and border).

**Counts, carried + delta:** quick 95 / 1012 + 1 todo (after change 14's `38cda37`) -> **96 /
1026 + 1 todo** (+1 file, profile.spec.ts's seven; +7 in sandbox-ui 20 -> 27), green twice at
`--maxWorkers=2` (run A the first after-run at `018bfe5` - `check-counts` observed 96 files, 1026 passed, 1 todo, exit 0; run B `vitest run --project server --maxWorkers=2` at `c177e20` on the gate's build - the JSON reporter 1026 passed, 0 failed, 1 todo, and `check-counts.mjs 96 1026` on its log matches); check 663 -> **677** (the thirteen new files and one more the tree gained under change 14 - `npm run check` read 676 at the interface tree before the commits and 677 after them); lint clean;
e2e 93 / 108 -> **94 / 109** (the surfaces walk); utilities **44** -> **44**
(0 disappeared, 0 appeared after the reword at `c177e20`; the first after-run at `018bfe5` read 47 with `align-top`, `align-bottom` and `select-all` appeared off the menu ids); catalog **27**; testids 332 -> **359**; copy
exports +52; OG, the four fixtures, the refuse-list unmoved by this change; `src/`
14 modified, 14 added, 0 deleted, 0 renamed against `8878084` (change 14's `snake.ts` and `frames.json` among the modified,
theirs). **The gate's terms** (`--before change-13c` at `8878084` on the worktree; `--after
change-13c --against change-13c --check 677` at `c177e20`): **the sandbox set `3bdb5974…` equal - every one of the Sandbox fixture strings byte-identical, nothing 13C adds is a row column or a runtime part**; the wire set `852b8c14…` -> `654e202e…` and the full `2932fc5a…` -> `1c4acf19…` - EXACTLY change 14's after-record (`gate/change-14-after.txt`: SNAKE's E/snake records, theirs; this change moves no Lua); the census
`7cdbc682…` -> `a684e569…` (2973 -> 3156 literals, 199 -> 212 files); the copy exports `5e3df867…` -> `cc7a68db…` (7 -> 8 copy modules); the testids `62f8f758…` -> `bcd5150b…` (332 -> 359); the scoped CSS `8ef5e915…` -> `d1c9ab44…` and the raw `d81eb2c0…` -> `703fb565…` (the
new components' own rules and the plate's, the inspector's and the route's, named in
TESTING.md); the titles `e664ad7d…` -> `d85f7867…` (1011 vitest titles incl. todo, 108 playwright runs -> 1027 vitest titles incl. todo, 109 playwright runs); the JS `06754f71…` -> `76858d0e…`; comment lines: every header at the rule by
`comment-lines.mjs --todo`. The script exits 1 at the census by design; the before's quick term
read `quick exit 1` (the pipe, as 13A / 13B / 14); the after's first (at `018bfe5`) `observed 96 files, 1026 tests passed, 1 todo`, exit 0; the second's (at `c177e20`, the record kept) the pipe again (`no Vitest summary lines`, exit 1), and run B on that record's build read 1026 / 0 / 1 todo.

**Chunks** (a fresh detached wrangler dev on 4173 each, stopped through PowerShell, HTTP 000
after each; 5173 untouched; the build stamped `c177e20`): c4 by its five files **22 passed** (the
eleven Sandbox titles among them), c5 by its four files **11 passed**, c3 by its two files
**21 passed** (the workspace's inspector gained a pinned button); `sandbox.e2e.ts` alone twice on the
way, 11 passed each time (26 s, 34 s). c1 and c2 not run: no install, session or browse title
reads the Sandbox or the workspace's inspector.

**Questions for the user.** (a) A right-click on an element outside the set selects it alone
before the menu opens, on an empty cell the set is kept and the focus cell moves there (so Paste
lands under the pointer); say if the empty-cell click should clear the selection as a plain click
does. (b) The menu's disabled items show their reason as a title on hover; say if the reason
should be a line under the menu instead (a title needs a hover and a screen reader hears the
disabled state, not the reason). (c) The workspace's Export for Grid Editor sits in the
inspector's pinned actions beside Save copy, not the destination zone (which renders only with a
ZONA connected); say if you want it in the zone's header as well. (d) The profile's `version` is
pinned at the Editor's 1.6.8 as three strings; the loader does not compare it; say if it should
follow something else. (e) The HANGAR payload rides under a top-level `hangar` key the loader
ignores; whether the Editor keeps it on a re-save through the panel is not proved (the
profile-cloud component is outside this reading) - a re-export from the module drops it; say if
the surface should also be carried as a comment in the pad's Setup at the cost of budget. (f) The
Sandbox's export is disabled over the budget with the same reason a Store gives; say if the file
should be written anyway. (g) The Play monitor shows twelve lines with no pause and no time
column (the Playground's monitor has both); say if you want either. (h) With the numbers on, a
button, a knob and an XY pad now carry a controller numeral the PDF's plate did not draw (the
fader always had one); say if the toggle should be off by default or the new numerals should go.
(i) The shared-controller mark is the ink, not the red, and the pass reads only the
CC-on-channel pairs (a note button on a CC's number is not a pair; two note buttons on one note
are not either); say if notes should pair too. (j) The recent strip remembers a colour 300 ms
after the picker last moved; say if every step of a drag should count. (k) The sheet lists the
pointer gestures (click, Shift+click, a drag, Alt+click, a double-click, a right-click) beside the
keys; say if it should be keys only. (l) Changes 1 to 13B's and 14's questions still stand.

**Departures from the brief:** the workspace's export button is in the inspector's pinned actions,
not the destination zone (the zone needs a ZONA on screen); the commits are by layer (the model,
the interface, the tests - 13A / 13B's split) rather than one per feature group, because the
plate, the inspector, the route and the spec carry every feature's change in one file each; the
sandbox-ui spec's shape halves were red at the model commit until the interface commit (13B's
precedent); the profile's JSON is four-space with no trailing newline (the Editor's writer)
where Export as a file is two-space with one; a profile's name keeps its spaces (`My
performance.json`) where the HANGAR export slugs it; the Init placeholder is unreachable on a
ZONA and the spec says so; the after-record was taken in the working tree at `018bfe5` while
change 14's commits had landed in it, so the wire terms moved by SNAKE's records alone (stated
above); `transfer.ts` gained `downloadText` (a refactor of `downloadExport`, its spec unchanged);
`Knob.svelte`, `ColourPicker.svelte`, `sequence.ts`, `library.ts`, every catalog entry, the
manifest, `pad-sim.ts`, `firmware-oracle.spec.ts`, the Sandbox runtime and emitter Lua, the
fixtures untouched; STATE / ROADMAP / REQUIREMENTS untouched; CAT-04 stays `[ ]`; not deployed.

14. SNAKE is broken - remake it (2026-09-20, lowest priority, queued last)
--------------------------------------------------------------------------------

> nice, one more note but thats on the lowest part of the priority list rn: the SNAKE gimmick
> profile is broken, we'll need to remake that

Recorded: SNAKE (`src/lib/catalog/entries/snake.ts`) is broken on the bench; a remake, queued
after change 13's three parts. What is broken - which hardware behaviour - is not yet described;
the question to ask before the remake is what the user saw (the snake not moving, the steering,
the food, the death, the picture) so the remake starts from the fault, not a guess.

The quiz, 2026-09-20 (stored through HANGAR, module rebooted): the picture "runs the same sequence
over and over again i have barely any affect on it"; steering "turns the wrong way / late"; the
notes "hang or spam", and something else unnamed. Reading: the deterministic autopilot-and-reset
loop is what the user sees (death is a fixed reset, so the shelf's short game repeats forever),
a finger's control credit is three generations and the steer reads the touch cell against the
head - if that cell comes from the raw pair rather than the calibrated map `N(x,y)` (12.1) the
finger lands on the wrong cell and the turn is wrong or late; the bite's note has no note-off in
the header's "at most one message per generation" rule, so notes hang. The remake: a real game
first - the finger steers for as long as it is down and the snake keeps the last direction after
the lift (no three-generation credit), the touch cell through `N(x,y)`, the food placed by a
seeded walk that does not repeat the same short game (the sweep refuses `math.random`; a
generation counter into the LCG is enough), death shown (a flash) and restarted after a pause,
note-on at the bite with a note-off a generation later, the death note the same way. To be
briefed when it reaches the top of the queue.

> ill go to sleep soon, proceed to 14 at your best discretion

Recorded 2026-09-20: the queue runs on without further questions - 13A, 13B, 13C, then 14 (the
SNAKE remake on the reading above). Nothing deploys until the user's word.

**Done - change 13A, 2026-09-20 (the selection set, the clipboard, multi-edit, lock, Tab).** Four
source commits, no push, no device, no deploy: `285aa7b` feat(sandbox) - the model; `99dda92`
feat(sandbox) - the plate, the list, the route; `278ac92` feat(sandbox) - the inspector;
`cff8f01` test(sandbox) - the e2e walk, the list's row at D-03's three signals, the gate's quick
constant; then this paragraph with `docs/TESTING.md`'s "2026-09-20 change 13A" and the gate
records `gate/change-13a.*` (before, at `34732c6`, on a clean worktree `../hangar-gate-13a` with
its own `npm ci`; removed after the records were copied) and `gate/change-13a-after.*` (at
`cff8f01`). The runbook is untouched (no row's steps move). Parts 13B and 13C are the next
executors'; the set is theirs to build on: a selection is an ordered list of ids, every structural
command takes it, and a landed group goes through one placement rule.

**The selection model.** `editor.ts`'s `_selection` is an ORDERED list of ids (the order the set
was made in); `selection` reads it and `selectedRegions` the set's regions in the surface's order;
`selectedId` and `selected` are the one region ONLY while the set has exactly one, so every
single-selection caller reads as it did. `select(id)` selects alone, `toggleSelect(id)` (Shift on
the plate or a list row) adds or removes, `selectAll()` (Ctrl+A) takes every UNLOCKED element in
the surface's order, `selectTouching(box, add)` is the marquee, `selectNext(step)` the Tab walk,
`escape()` disarms an armed kind or, with the selector, clears the set (V only disarms). The plate:
a plain click on an element selects it alone; a press on a member of a set keeps the set and moves
it whole, the click DEFERRED to a release with no motion (which then selects the pressed one
alone); Shift+click toggles and starts no drag; a click on empty clears (Shift keeps the set, so a
marquee can add). Every member draws its own action-colour outline and the set draws ONE dashed
outline round its bounding box (`surface-group`); the eight handles only on an unlocked single;
the delete icon at the set's corner deletes the set (none on a locked single, the model would
refuse); the list marks every member `aria-pressed` (a button takes no `aria-selected` - svelte-
check warns and D-03's three signals are the row's) and the one selected row `aria-current`. A
history entry carries the set (`selection`) and undo / redo re-select it.

**The marquee rule (the recorded reading).** A pointer drag from an EMPTY cell with the selector
draws a 1px action-colour rectangle, no fill, no radius (`surface-marquee`), from the cell it
started on to the cell under the pointer, drawn once the pointer has left the first cell (a click
is a click); on release every unlocked element the box TOUCHES - one cell in common - becomes the
set, or joins it with Shift held (`geometry.ts` `touching`). A locked element is skipped unless
Shift-clicked. Nothing touched clears the set.

**Group commands.** `moveSelectedTo(cell)` puts the set's bounding-box origin at the cell, every
member keeping its place in it; `nudgeSelected` shifts the set; both go through `geometry.ts`
`applyEdits` - one patch per member validated as a whole (each against the untouched ones first,
then the map over everything) - so a move that would overlap, leave the plate or touch a locked
member is refused whole with the first line and the surface stays the same object; one entry per
command, a held arrow one entry until the key-up. `resizeSelectedBy` / `resizeSelectedTo` are a
single selection's (a set returns undefined, silently). `remove()` deletes the set as one entry
(`delete`, or `cut` for Ctrl+X) and returns a `CommandOutcome` - done with a count, refused with a
line, nothing - the shape the route puts on the plate's status line.

**The clipboard and the placement rule.** Ctrl+C is `copySelection()` - clones in the surface's
order - held by `src/lib/sandbox/clipboard.ts` in TWO homes: the module's own slot (the route
navigates inside the app, so it outlives a surface and pastes into another) and, when the route
hands it in, `sessionStorage` under `hangar:sandbox-clipboard` (`browse/return.ts`'s namespaced
family; a reload in the tab keeps it, a new tab starts empty; the content is validated on the way
back through schema.ts's `isRegion`, now exported, so a foreign value reads as empty). Ctrl+X is
the copy then `remove("cut")` - one Undo. Ctrl+V is `paste(content)` and Ctrl+D / the panel's
Duplicate `duplicate()`, both through `land()` and `geometry.ts placementFor`: the group's
bounding-box origin goes to the FOCUS CELL if every member fits there with the layout kept, else
ONE CELL DOWN-RIGHT of the group's own origin, else the FIRST ORIGIN IN READING ORDER (row-major)
at which every member is on the plate and covers only free cells (cell by cell, so another element
may sit in the group's gaps); nowhere fits -> refused whole. Read plainly: a multi-cell element
covers its own down-right cell, so that candidate lands only a one-cell element (a blank) - a
fader's or a button's copy falls to the reading-order scan and lands beside the original. A
pasted element keeps the original's settings and colour (the controller too, as Duplicate always
did - the Bible's "retain the original mapping"), takes a fresh id, and takes an AUTO-NUMBERED
name from `editor.ts autoName(kind, regions)` - the kind's label and the lowest number no region
carries (`Fader 2` beside `Fader 1`; `Fader 1` again once it is gone), each member in turn so a
pasted pair is `Fader 3`, `Fader 4`. The one function 13B calls for adds (`newRegion` already
does). The palette's colour cycle is not consulted by a paste. The landed set becomes the
selection; one entry under `paste` / `duplicate`; Undo leaves nothing selected (the copies are
gone), Redo re-selects the copies. Refused without a number: the cap (`This surface is full.
Remove an element to paste another.` - counted on the whole group: fifteen plus two is refused,
fifteen plus one lands), no room (`There's no free area for what you copied. Clear some room and
paste again.`), nothing held (`Nothing to paste yet. Copy or cut an element first.`). The status
line's words: `Copied N elements.` / `Cut ...` / `Pasted ...` / `Duplicated ...` / `Locked ...` /
`Unlocked ...` (through `elementsLine`, `1 element` singular), `N elements selected.` with the
focus cell while a set is selected.

**Multi-edit.** With more than one selected the inspector reads SELECTED ELEMENTS / the shared
kind (or MIXED), the count as the headline (`inspector-count`), `A change here applies to every
selected element.` as the lede, no name field, no units chip. Shared: Locked, Colour, Channel, Min,
Max; kind-specific only when every member is that kind: Orientation (faders), Mode / Speed /
Spring and its value (faders; Speed while any is relative), Touches (pads), Toggle / Group /
Output (buttons), Note (buttons all on Note); the controller fields are single-only; a blank in
the set drops MIDI output. MIXED: a typed field reads empty with the placeholder `Mixed`
(`data-mixed`), a select shows a blank disabled `Mixed` option, a checkbox is `indeterminate` with
`aria-checked="mixed"`, a mixed colour writes `Mixed` under the swatch (the swatch shows the first
member's). Applying writes every member through `applyEdits` under ONE entry of the existing
`EditKind` with a group key - `field:<id1>+<id2>+...:<field>` for the typed fields (so keystrokes
coalesce over the set) - and the entry's `selection` re-selects the set on Undo; a refusal on any
member refuses the whole edit with its line (the orientation's on `orientation-problem`, a typed
field's on its message, the cc ceiling read off every pad's count). Delete on a set reads `Delete
N elements`.

**Lock.** `locked?: boolean` on `Region` (schema.ts, optional, validated as a boolean; an old
draft reads as it did; off is NO field - `applyEdits` removes a key whose patch value is
undefined). A locked element cannot be moved, resized, nudged or deleted: `commitBoxes` and
`remove` refuse with `<name> is locked. Unlock it to move or resize it.` / `... Unlock it to
delete it.` (on a set, the FIRST locked member's name), the surface the same object; the fields
still edit (chosen: the brief's default). The plate draws a padlock of straight lines inside the
region's top-right corner (`surface-lock`, `SANDBOX_LOCK_ICON` 12: a square body and a square
arch), hides the handles and the delete icon on a locked single, and gives it the default cursor;
the marquee and Ctrl+A skip it; the inspector's `Locked` checkbox (`field-locked`, under
Identity, with `A locked element stays where it is: it can't be moved, resized or deleted. Its
settings still change.`) is `setLocked`; Ctrl+L is `toggleLock()` - a mixed set locks all, an
all-locked set unlocks - one entry under `lock`. `locked` never reaches the emitter (rows read
named fields; test 16 holds `regionRow` and `regionTail` byte-identical with it on; the gate's
three wire terms equal).

**Tab.** With the plate focused and a selection, Tab selects the next element in the surface's
order (from the set's LAST member) and Shift+Tab the previous (from the FIRST), alone, wrapping at
either end; with a kind armed Tab is the browser's. THE TAB ORDER: with nothing selected Tab
leaves the plate as it always did, so the keyboard's way off the plate is Escape (clears the
selection) then Tab - chosen over the documented `]` / `[` alternative because Tab is what the
readings named; say if you want the brackets instead (or as well). The model's `selectNext` from
an empty selection takes the first (or last) element, for a caller that wants it.

**Copy added (18, `copy.ts`):** `SELECTED_ELEMENTS`, `MULTI_LEDE`, `MIXED`, `deleteElements`,
`selectedCountLine`, `LOCKED`, `LOCKED_HELPER`, `lockedMoveLine`, `lockedDeleteLine`,
`lockedLine`, `unlockedLine`, `copiedLine`, `cutLine`, `pastedLine`, `duplicatedLine`,
`NOTHING_TO_PASTE`, `PASTE_NO_SPACE`, `PASTE_AT_CAP` (D-05's register; the three refusals carry
no digit, spec 15 asserts). **Test ids:** `surface-member`, `surface-group`, `surface-marquee`,
`surface-lock`, `inspector-count`, `colour-mixed` new (321 -> 327), `field-locked` through the
check snippet; every existing id kept. The Bible's register: no `border-radius` anywhere new (the
plate's spec scans for it and for `rx=` / `ry=`; the padlock is rects and a polyline).

**Counts, carried + delta:** quick 95 / 1003 + 1 todo -> **95 / 1006 + 1 todo** (+3:
`sandbox-ui` 13 -> 16), green twice at `--maxWorkers=2` and in the gate's after; check 660 ->
**661** (`clipboard.ts`); lint clean; e2e 91 / 106 -> **92 / 107**; utilities **44** -> **44**;
catalog **27**; testids 321 -> **327**; copy exports +18; OG, the four fixtures, the refuse-list
unmoved; `src/` 12 modified / 1 added / 0 deleted / 0 renamed. **The gate's terms** (`--before
change-13a` at `34732c6`; `--after change-13a --against change-13a --check 661` at `cff8f01`):
**the wire set `852b8c14...`, the full `2932fc5a...` and the sandbox set `3bdb5974...` equal -
byte-identical, every catalog string and all 475 Sandbox fixture strings**; the census
`e2585d8c...` -> `0df9e009...` (2,872 -> 2,914 literals); the copy exports `09a12df9...` ->
`eea5099f...`; the testids `ee1f20c1...` -> `ed13451f...`; the scoped CSS `5aa7323e...` ->
`68eedc24...` and the raw `e6d0ee3b...` -> `4e8f13ba...` (the Sandbox's own rules, named in
TESTING.md); the titles `63c35e7a...` -> `6c609832...` (1,004 -> 1,007 vitest incl. todo; 106
-> 107 playwright); the JS `62a0b202...` -> `c32244aa...` (71 files); comment lines: 12 files
moved, every header at the rule (the route's back at ten). The script exits 1 at the census by
design; the before's quick term read `no Vitest summary lines` (the pipe, as 10B / 11 / 12b; JSON
1002 / 1 - layer B on the absent build - / 1 todo), the after's 95 / 1006, exit 0.

**Chunks** (fresh detached wrangler dev on 4173 each, stopped through PowerShell, HTTP 000 after
each; 5173 untouched; the build stamped `cff8f01`): c4 by its five files **20 passed** (the nine
Sandbox titles among them); c5 by its four files **11 passed**; c2 by its two files **3 failed /
19 passed** at three workers (`browse:299`, `:1186`, `:1404`, the recorded hydration family; the
Sandbox-naming `:1672` passed), `browse.e2e.ts` alone at `--workers 1` **13 passed / `:343`
red**, `:343` alone **1 passed**. c1 and c3 not run (no install, session or tuning title reads
the Sandbox).

**Questions for the user.** (a) The plain click on a member of a set keeps the set for a drag and
selects alone on release with no motion (Figma's rule); say if a plain click should select alone
at once. (b) Ctrl+A takes every UNLOCKED element and the marquee skips locked ones; a set that
holds a locked member (Shift-clicked in) refuses a move and a delete whole with the locked one's
name - say if the locked member should be skipped instead. (c) "One cell down-right of the
original" is taken literally and lands only a one-cell element; say if it should mean past the
original's far corner (col + w, row + h). (d) Undo of a paste or a duplicate leaves nothing
selected; say if the originals should be re-selected. (e) Tab leaves the plate only with nothing
selected (Escape first); say if you want `]` / `[` too. (f) A locked element's fields still edit
and its colour still changes; say if lock should freeze those too. (g) The delete icon is hidden
on a locked single (the handles too); say if it should show and refuse. (h) The list's rows carry
`aria-pressed`, not the brief's `aria-selected` (a button role does not take it). (i) Changes 1
to 12b's questions still stand.

**Departures from the brief:** the pasted element's name numbering is `autoName`, the same
function `newRegion` already used (13B's "auto-numbered names on add" is already the case);
`aria-pressed` for `aria-selected`; `remove()` returns a `CommandOutcome` rather than a boolean
(three spec pins moved); `Escape` with the selector clears the selection (new; V does not); a
group does not resize; the inspector's swatch shows the first member's colour over a mixed set
with a `Mixed` line rather than a blank; the route's header, twelve lines since `e8c22ec`, is at
ten now; the first commit's message says "twenty-one strings" where the count is eighteen (not
amended - no history rewrite); `identity.spec.ts` test 6 red on the first cut of the list's row
(fixed in the fourth commit); the runbook untouched; STATE / ROADMAP / REQUIREMENTS untouched;
CAT-04 stays `[ ]`; not deployed.

**Done - change 14, 2026-09-21 (SNAKE remade on the reading above; the user asleep, nothing
asked).** Three source commits and one docs commit, no push, no device, no deploy: `27ef042`
feat(catalog) - the entry, `frames.json`, the two VM cases, `docs/entries/snake.md`'s dated
section with the old Setup and Timer verbatim and the fault as read; `38cda37` docs(audition) -
row 39, the cost row 581 / 870 -> 871 / 732, `ROW_COUNT` 39, the gate's quick constant at 1012; `42a8ec7` docs(audition) - row 39's cells padded to the table's columns
by prettier (the first after-run's lint term refused the file; one line, no text moved); then this paragraph with `docs/TESTING.md`'s "2026-09-21 change 14" and the gate records
`gate/change-14.*` (before, at `8878084`, on a clean worktree `../hangar-gate-14` with its own
`npm ci`, removed after the records were copied) and `gate/change-14-after.*` (at `42a8ec7`, on a
clean worktree `../hangar-gate-14-after2` with its own `npm ci`, because the working tree carried
change 13C's uncommitted files the whole time; removed after). Change 13C ran in the same tree at
the same time; every commit here is `--only` SNAKE's paths, and no Sandbox file, `copy.ts`, route
or `e2e/sandbox.e2e.ts` is touched.

**The fault, confirmed against the Lua.** (1) Death was `s.o={}s.b={}s.p=1 s.l=2 s.u=1 s.v=0
s.f=41` - a reset to the same two cells and the same food - and the walk `(w*7+23)%81` ran from
that fixed food, so every game after a death was the first game: in the VM at 220 ms five bites
and a death at ticks 22, 110, 308, 550, 594, 638, then the identical game from 660, three times
over in 2,200 ticks. That is "the same sequence over and over". (2) The handler read the cell
from `x*9//128` / `y*9//128` - the raw pair, SNAKE being one of 12.1's "untouched Lua entries" -
so on the module the finger sat a cell out near the edges and the turn came off the wrong cell;
and `s.t=3` gave a finger three generations, after which the autopilot steered onto the food
again, so a still finger "had barely any affect". (3) Bites and deaths sent `144` and nothing
sent `128`, under the header's "at most one message per generation" - "hang or spam". All three
as section 14's reading had them; nothing to correct.

**The steering rule.** The touch cell is the library's `N(x,y)` (the calibrated map every other
finger on the site reads); the direction is the dominant axis from the head to that cell; a
horizontal steer only while `s.u` is 0, a vertical one only while `s.v` is 0 - a reversal refused
by construction, as before. Every live sample re-steers, so the finger steers for as long as it is
down; a still finger sends no sample (the firmware's change gate) and the direction stays; the
lift changes nothing. **The autopilot rule:** `s.t=1` on any live sample - a refused steer
included: a finger that touched the game owns it - and never decremented; the Timer's autopilot
(the old arithmetic: the perpendicular axis onto the food's row or column) runs only while `s.t`
is nil, and the restart clears it. So the shelf plays itself until the first finger of a game,
and again from the next death. Chosen over an `Autopilot: Off / On` knob, measured at ten Timer
characters (`if @AUTO and not s.t then`, 741 / 749) plus a sixth knob, a rack change and
`stamp.spec.ts`'s grown-rack exception; the rule is the cheaper honest form and needs no line on
the card. A finger during the death pause sets `s.t` and the restart clears it - harmless.

**The food walk.** `F(s,n)` in the Setup: the first free cell first as the fallback, then
`w=(w*7+23+s.g)%81` from the food just eaten, re-rolled while it lands on the body or the new
head, bounded by the literal 12. `s.c` counts Timer calls from Setup and is never reset; the
restart copies it into `s.g`, so the first game runs with `s.g` 0 (the old walk, byte for byte)
and every later game from a different count. Deterministic from a cold start; no `math.random`
(the sweep's D-08 term). Two consecutive games are asserted to differ (the second game's first
food at cell 21 against the first game's 67). **The death sequence, in generations:** the death
call sends the low note and paints every occupied cell in `@FOODC` (one bounded pass over 0..80
testing `s.o[k]`, never the ring buffer's stale slots) and sets `s.d=6`; the next two calls hold
the flash; the third blacks the board; two more stay dark; the sixth runs the restart - six
generations, 1.32 s at the default 220 ms, 0.66 s at 110, 1.8 s at 300. The food colour rather
than white (the snake becomes food; the same eleven characters at the corner). **The note-off:**
a bite sends `144, @NOTE+s.l%12, 100` and leaves the pitch in `s.z`; the death sends `144,
@NOTE-12, 110` the same way; the next Timer call sends `128, s.z, 0` before anything else and
clears it - RADAR POINTS's pending list one note deep, a bite and a death being mutually
exclusive. Nothing hangs; one note-on per bite, ever; at most two messages a generation.
**Speed:** `@SPEED` as before, the period in ms in both events - no honest BPM ladder lands 220
exactly and the rest frame and the first game had to stay.

**The function names spent, and the split.** `P` (the painter, as before - `brightness.ts`'s
declared painter row unchanged), `I` (the restart: black all 81 cells, the two-cell snake at 39
and 40 travelling right, the food at 41, `s.t` and `s.d` cleared, `s.g=s.c`) and `F` (the placer)
are `local function`s of the Setup published as `self.P self.I self.F` and read back into Timer
locals (`local P,I,F=s.P,s.I,s.F`, 24 characters against the 89 of the painter declared twice);
the Setup runs `I(self)` once after `self.c=0`, so an Apply over another card's picture starts
from black. NOT a global function, and that is a departure from the brief's words: the
host-surface classifier admits a bare call only as a host name, a library name or a local of the
SAME event, so a global `I(` in the Timer would be refused there - and every single capital is
spoken for between the library's twenty-one, `R`, the emitter's `J` and `M` and the runtime's `S F
I R O Q D K` anyway. The fields are the element's own; `I` and `F` collide with nothing that runs
on it. **Setup 585 -> 877 at the RGB444 picker corner (323 -> 31 free; 871 at the defaults), Timer
880 -> 739 (28 -> 169 free; 732)**, both fixed points, both accepted by `checkSyntax`, every knob
state through the sweep; no system slot. The placer back in the Timer was measured at 663 / 938 -
thirty over - so the split stands; the Setup's corner is the tighter of the two now.

**The first game, the frames and the OG.** Kept: with `s.g` 0 and the old autopilot the VM's wire
for the first game is the old sequence to the tick, so `frames.json`'s records at ticks 0, 37,
101 and 500 and the OG (tick 64) did not move. Tick 1009 moved (12 -> 0 lit bytes): the old entry
restarted at the death tick and 1009 was the second game's fifteenth generation; the remade card
restarts six generations later on a different walk, dies again at 924 and is inside that death's
dark pause at 1009. Regenerated; SNAKE's block alone, one record in it. **The VM cases**
(`lua-smoke` 47 -> 49): the shelf's first game held equal to the OLD note-on list pasted from a
run at `8878084` with an off one period after each, the flash (eight cells in the food colour,
none in the snake's), held two generations with the death note released on the first, black on
the third through the fifth, the restart on the sixth with nothing sent through the pause, the
second game's first bite and its food elsewhere; the steer through the measured knots - up on the
very next generation, kept after the lift for two more with the autopilot off, a reversal
refused, a right turn kept, the wire the first bite's pair alone. The catalog-wide gates green
untouched. The residue probe's own SNAKE comment already says its phase test cannot see this
card; since the Setup's clear every cell is at phase 255 in the untouched run too, so it cannot
flag the card either way - stated, not hidden.

**The wire diff:** set `852b8c14…` -> `654e202e…`, full `2932fc5a…` -> `1c4acf19…`: the set 2,775 records, 2,731 byte-identical, 44 moved, 0 removed, 0 added; the full 2,802, 2,757 identical, 45 moved, 0 / 0 - the moved records are every `E/snake/` record there is (44 in the set, 45 in the full: the defaults, the picker corner, every single-knob position and the cross-product) -
`E/snake/` records only, nothing outside them; the sandbox set `3bdb5974…` equal. **Counts,
carried + delta:** quick 95 / 1010 + 1 todo -> **95 / 1012 + 1 todo** (+0 / +2), green
twice at `--maxWorkers=2` on the after-worktree's build through `check-counts.mjs 95 1012` (runs A and B, both `matches the expected counts`); check 663 -> **663** on the clean tree (the working tree read 676 files with 5
errors and 2 warnings, every one in 13C's untracked Sandbox files - theirs); lint clean; sweep
`4 19` green; e2e 93 / 108 -> **93 / 108** (no title added; c3 by its two files **21 passed** in 28.0 s on a fresh detached wrangler dev on 4173, stopped through PowerShell, HTTP 000 after; a first attempt read 21 failed because the fresh worktree had no `.dev.vars` and every page was the Basic Auth gate's 401 - the file copied in, the harness and not the tree; 5173 untouched; c4 not
run - no title reads SNAKE); audition rows 38 -> **39**; utilities **44** -> **44**;
catalog **27**; testids equal; copy exports equal; OG 27 files / 159,169 B / `9becd682…` equal (the OG at tick 64 is inside the kept first game); `frames.json` `3bf1ef05` ->
`290ff266` (one record), the three other fixtures equal; the census `7cdbc682…` -> `0a2a46b1…` (2,973 literals either side: SNAKE's two strings out, two in); the titles
`e664ad7d…` -> `42737061…` (1,011 -> 1,013 incl. todo; 108); the JS `06754f71…` -> `74b13066…` (71 files; snake.ts is in the bundle); the scoped CSS
`8ef5e915…` equal; the comment-lines record 1 file moved (snake.ts, its header at the ten-line rule by `comment-lines.mjs --todo`); `src/` 4 modified / 0 added / 0 deleted against
`8878084`. The gate exits 1 at the wire by design; the before's quick term read `no Vitest
summary lines` (the pipe, as 13A / 13B), the after's the same line (its JSON 1011 passed / 1 failed / 1 todo - the recorded ordering hole, `radius.spec.ts` layer B on the worktree's absent build, the quick term running before the build term).

**Departures from the brief:** published Setup locals, not a global (above); the autopilot rule,
not the knob; the flash in the food colour; six generations, not a fixed second; `@SPEED` in ms;
the Setup clears the board (new behaviour, a correctness gain on an Apply); `frames.json`
regenerated for one record; the description, `listing.ts`, `brightness.ts` and `stamp.spec.ts`
untouched (neither the rack nor the painter's name moved); the after-run on a clean worktree
rather than the working tree; `src/vendor/`, `library.ts`, every other entry, `sequence.ts`, the
manifest, `Knob.svelte`, `ColourPicker.svelte`, `pad-sim.ts`, `firmware-oracle.spec.ts` untouched;
STATE / ROADMAP / REQUIREMENTS untouched; CAT-04 stays `[ ]`. Not deployed - on the user's word.

**Questions for the user.** (a) The autopilot switches off at a game's first finger and comes
back only after a death; say if it should come back after a quiet spell instead (a `s.t` stamp
against `s.c`, about twenty Timer characters), or never. (b) A refused steer (a reversal, the axis
already travelled) still switches the autopilot off; say if only an accepted steer should. (c) The
death sequence is six generations at whatever Step time is set (0.66 s at 110 ms); say if it
should be a fixed time. (d) The flash is the food colour; say if white, or the snake colour
blinking, reads better on the diffuser. (e) The walk is seeded per game by the generation count;
say if a per-bite fold (every food unpredictable inside a game too) is wanted - it would move the
first game and the OG. (f) The Setup now clears the board on an Apply; say if the old
three-cells-over-whatever behaviour was relied on. (g) Row 39 is the bench's; the steer's "wrong
way / late" fix rests on the calibrated map being this unit's (12.1's one-prototype map, row 24).
(h) Changes 1 to 13B's questions still stand.

15. Sandbox toolbar - a vertical icon rail beside the inspector (2026-09-21, Fable)
--------------------------------------------------------------------------------

> [screenshot: the Sandbox's three toolbar rows - Undo / Redo / Save copy / Export as a file; the
> flip-horizontal, flip-vertical, rotate boxes with the transforms helper; View - CC numbers,
> Names, ? / Export for Grid Editor / Import a profile]
> put these into the right handside, next to the selected element config panel vertically with
> icons only

Recorded: every control in those three rows moves into one vertical rail of icon-only boxes on
the right-hand side, beside the inspector (the selected element's panel), with the label as the
tooltip and the accessible name; the helper sentences go (the cheat-sheet and the tooltips carry
the words). The row above the plate and the View row are gone.

**Done - change 15, 2026-09-21 (the tool rail; the user's word with the screenshot, nothing
asked).** Two source commits and one docs commit, no push, no device, no deploy: `6b41d99`
feat(sandbox) - the model, the interface, the shell's column, the route, the four retirements and
the specs in one commit (the rail is one thing: a model file, one component, one shell track, one
route snippet - splitting it would leave the spec's shape halves red between commits, 13B / 13C's
precedent inverted); `6fdb80b` test(sandbox) - the e2e walk and the gate's quick constant at 1027;
then this paragraph with `docs/TESTING.md`'s "2026-09-21 change 15" and the gate records
`gate/change-15.*` (before, at `76d96da`, in the working tree - clean, nobody else editing) and
`gate/change-15-after.*` (at `6fdb80b`).

**Where the rail sits.** The shell already names a rail: `Rail.svelte`, the LEFT column (the
palette, the element list, `+ New surface`) - the tool rail does not join it. It is a FOURTH SHELL
COLUMN between the centre and the inspector: `ShellFill` gains `tools` (a snippet like `rail` and
`inspector`), `+layout.svelte` draws `shell-tools-column` after `<main>` and before
`shell-inspector-column` only when a route fills it, and `.frame.with-tools` is the grid
`var(--rail-w) minmax(0, 1fr) auto var(--inspector-w)` - the rail draws its own width and the
CENTRE gives it up, never the inspector (`layout.ts` untouched: the inspector's clamp, the 2 x 2
reflow at 454 and the compact band's 300 stand; putting the rail inside the inspector's column
would have taken 60px off a panel section 13 already floors). Every other route's frame is
byte-identical - no class, no column (shell.spec test 5). The rail is immediately left of the
inspector, on the panel colour, a divider hairline on its left edge; the inspector keeps its own.

**The rail, top to bottom.** `sandbox/tool-rail.ts` (menu.ts's pattern - pure, the source of
truth, rendered by `ToolRail.svelte`): five groups, a hairline between each - (1) Undo, Redo; (2)
Save copy, Export as a file, Export for Grid Editor, Import a profile; (3) Flip left to right, Flip
top to bottom, Rotate a quarter turn clockwise; (4) CC numbers, Names - toggles with
`aria-pressed`, lit by border and ink as the View row's were; (5) Keyboard shortcuts. THE GLYPHS,
straight lines on a 20 box, stroke 2, no curve, no `rx`, no `<path>`: 13B's mirror (two arrowheads
either side of a centre line, and its swap) and turn (three sides of a square, an arrowhead at the
open corner) reused verbatim; Undo an arrow pointing left whose shaft turns down and comes back,
Redo its mirror; Save copy two squares, the second offset over the first; Export as a file an arrow
down into an open tray; Import a profile the arrow up out of the same tray; Export for Grid Editor
an arrow down into a square of four cells (the Editor's grid); CC numbers a number sign; Names a
label tag (a pentagon, five lines); the `?` box the glyph as text in the mono face (a `?` in
straight lines reads as nothing). EVERY BOX: 44 x 44, `border-radius: 0`, `aria-label` = the
label, `title` = the label with the keys in the platform's words through 13C's `titledWithKeys` and
`keysWord` (`Undo (Ctrl+Z)` / `(Cmd+Z)`, `Redo (Ctrl+Y)`, `Keyboard shortcuts (?)`; the rest the
label alone) or the disabled reason. THE STATES, the rows' own by `railBoxes(flags)`: Undo / Redo
off in Play and with nothing to take back or redo; the transforms off in Play and on an empty
surface; Export for Grid Editor off with its reason as its title while the landing measures or is
over the budget; Save copy, Export as a file, Import a profile, the toggles and `?` always live; a
box Play disables is described by the mode line (`sandbox-mode-line`) as the rows' were. THE
DESCRIPTIONS kept as sr-only paragraphs: the no-link explanation on Export as a file
(`no-link-explanation`, the fake-ZONA loop reads it), `EXPORT_PROFILE_HELPER` (or the reason) and
`IMPORT_PROFILE_HELPER` on the profile boxes, the two view helpers on the toggles. The import is a
file input dressed as the same box (`accept` the export's types), the label the box. The `?` box
hands its element to `openSheet(opener)` so Close returns focus to it (13C's rule; the window's `?`
still reads `document.activeElement`). The group is `role="group"` named `Tools` - NOT
`role="toolbar"`: a toolbar owes arrow-key roving between its controls and the sheet's spec holds
every key a handler reads against a row; twelve tab stops are what the three rows were.

**Short and narrow.** ONE STACK of twelve is 583px with the hairlines and padding; the shell's row
is 535 at 1280 x 720 (720 less the header, the bar and the footer), so under 768 of viewport
height (`@media (max-height: 767.98px)` - the exact height at which one stack fits) the rail is
TWO STACKS of 44 (`repeat(2, 44px)`, the hairlines spanning both), 108 wide - the e2e holds every
box inside the viewport at 720 and one stack at 900. The tools column scrolls its own body
(`overflow-y: auto`) as the fallback below that. AT THE PHONE WIDTH the shell stacks from 1024 down
(section 13's own row; the narrow band is the same stack tighter), and there the rail is a
HORIZONTAL STRIP above the inspector: the tools column is a row of its own between the centre and
the inspector's column, the rail `repeat(auto-fill, 44px)` at the full width with the hairlines
gone (a hairline per group would cut the strip into five rows), 12px / 16px padding, the boxes
flowing left to right in rail order - nothing is lost; the e2e holds the rail wider than tall, 390
wide, above the inspector's column, `?` to the right of Undo at 390 x 844.

**The outcome lines and the helpers.** The three rows drew three transient lines (Save copy's,
Export as a file's, the profile's) beside their buttons and two helper sentences (the transforms'
`TRANSFORM_HELPER`, the View word). The helpers GO - the cheat-sheet and the titles carry the words;
`TRANSFORM_HELPER` and `VIEW_GROUP` (`View`) are retired by name in `copy.ts`, `TOOLS` (`Tools`)
added for the group's name. The outcome lines GO TO THE PLATE'S STATUS LINE (`surface-status`) -
not a line under the rail (a 44px column has no room for `Exported as My performance.json.`, and a
sentence under the shell column would be a third status surface) - through the route's `say(line)`:
`plateNotice` for `CONFIRM_MS` (4 s), cleared only if the line is still the one written, so a
command's line that lands inside the four seconds is not clipped by the timer; `saved`,
`exported`, `profileOutcome` and their three timers collapse into one. The status line is
`role="status"`, so the announcement section 14 asks for ("announce completed actions") is kept,
now from one place. `savedLine`, `exportedLine`, `profileExportedLine`, `importedLine` and the
refusal lines unchanged.

**Retired by name (D-12).** `SurfaceActions.svelte` (the export box, its line, the sr-only
explanation - the explanation lives on the rail's box), `SurfaceTransforms.svelte` (the three boxes
and the helper; the glyphs live in `tool-rail.ts`), `ViewToggles.svelte` (the View word, the two
toggles, the `?` box) and `ProfileActions.svelte` (the two profile boxes and the outcome; the file
input's read-and-clear moved whole) - all four deleted, none reduced: each was its row and the row
is gone. The route: the `.tools` row, the transforms mount, the `.view-row`, their styles, the
compact band's tightening, the `@container (width < 480px)` query and the column's
`container-type` (the container existed for the row's fold), the four imports, the three states
and three timers. `device-ui.spec.ts`'s two `SurfaceActions.svelte` reads (no destination half, no
skip words) are re-aimed at `ToolRail.svelte`, the successor. Test ids kept on the boxes: `undo`,
`redo`, `save-copy`, `export-surface`, `export-profile`, `import-profile`, `flip-horizontal`,
`flip-vertical`, `turn-surface`, `view-numbers`, `view-names`, `shortcuts-open`,
`no-link-explanation`; added `tool-rail`, `shell-tools-column`; retired `surface-share`,
`export-outcome`, `save-copy-outcome`, `surface-transforms`, `view-toggles`, `profile-actions`,
`profile-outcome` (the two e2e reads of the outcomes moved to `surface-status`). Strings: `TOOLS`
added; `TRANSFORM_HELPER`, `VIEW_GROUP` retired; the labels, helpers, keys and outcome lines are
13B / 13C's, verbatim.

**Proved.** `sandbox-ui.spec.ts` 27 -> 28 (18, 22, 25, 27 re-aimed at the rail - 25's and 27's
titles reworded where they named the rows; 28 the rail whole: the order, the kinds, every glyph's
lines inside the box and none zero-length, the keys, `railBoxes` under six flag sets, the shape
with its names, titles per platform, descriptions, four hairlines, eleven `<svg>` at the model's
line count, two `aria-pressed`, the 44 square, both media queries, the route's snippet between
`rail` and `inspector`, the dispatch, the four files gone from disk and the route, the layout's
column and track list); `shell.spec.ts` test 5 the column's absence and place; spec 22's
completeness test green as it stood (no key moved); `identity.spec.ts` test 6 untouched and green
(the pressed toggles paint no `--color-raised`); `radius.spec.ts` both layers green on the build.
The e2e: the eleven Sandbox walks green by their test ids; the twelfth (the rail's placement at
1280 x 720, 1280 x 900 and 390 x 844, Save copy on the status line). Counts carried + delta: quick
96 / 1026 + 1 todo -> **96 / 1027 + 1 todo** (run A the gate's quick term, exit 0; run B `vitest run --project server --maxWorkers=2` at `6fdb80b` on the gate's build - the JSON reporter 1027 passed, 0 failed, 1 todo, and `check-counts.mjs 96 1027` on its log matches, exit 0);
check 677 -> **675** (-4 +2); lint clean; e2e 94 / 109 -> **95 / 110**; utilities **44 -> 44** (0
disappeared, 0 appeared - `hidden`, `flex`, `grid`, `block`, `fixed`, `sticky`, `relative`,
`absolute`, `contents`, `border`, `rounded` kept out of the new comments, ids and titles; the two
stacks are "stacks", never the other word); catalog **27**; testids 359 -> **346** (the census reads
literal `data-testid="…"`: -8 retired, -7 kept but riding through the template as the transforms'
already did, `export-profile` / `save-copy` 2 -> 1 with the workspace's literal left, +2); copy
exports -2 +1. **The gate's terms** (`--before change-15` at `76d96da`; `--after change-15
--against change-15 --check 675` at `6fdb80b`): **the wire set `654e202e…`, the full `1c4acf19…`
and the sandbox set `3bdb5974…` ALL EQUAL - no Lua moved, no row column, no runtime part**; the
census `a684e569…` -> `be886210…` (3156 -> 3141 literals, 212 -> 210 files); the copy exports
`cc7a68db…` -> `1ce943b4…`; the testids `bcd5150b…` -> `60e7351e…`; the SCOPED CSS `d1c9ab44…` ->
`11a91a6a…`, the raw `703fb565…` -> `8ab7ccda…` (the rail's six rules and the layout's two come;
the route's five and the query, the four components' rules go); utilities 44 -> 44; the titles
`d85f7867…` -> `8783d842…` (1027 -> 1028 vitest incl. todo, 109 -> 110 playwright); the JS
`76858d0e…` -> `acfddf4d…`; OG and the four fixtures unmoved; `src/` 7 modified, 2 added, 4
deleted, 0 renamed; every header at the ten-line rule (the route's trimmed 12 -> 10). The script
exits 1 at the census by design. **Chunks** (a fresh detached wrangler dev on 4173 each, stopped,
HTTP 000 after; 5173 untouched; the build stamped `6fdb80b`): c4 by its five files **23 passed on its second run (the first run's fresh server dropped about a minute in - 8 passed, then 15 refused connections and fetches that never landed, the wrangler log ending mid-request with no error; run again fresh: 23 passed in 38 s)**; c5
by its four files **11 passed**. c1, c2, c3 not run: no install, session, browse or tuning title reads
the rail, and the workspace is untouched.

**Questions for the user.** (a) The rail is a fourth shell column that takes its width from the
centre, not from the inspector; say if the inspector should give it up instead (it would fall
under section 13's 268-300 at the compact band). (b) Under 768 of viewport height the rail is two
stacks of 44 side by side (108 wide) rather than a scrolling column; say if you would rather it
scrolled, or the boxes shrank below the 44 floor there. (c) Below 1024 the rail is a strip above the
inspector with the hairlines gone; say if the groups should keep a gap in the strip. (d) The
outcome lines (Save copy, the exports, the import) read on the plate's status line for four
seconds; say if you want them elsewhere (a line under the shell column, a toast). (e) The two
helper sentences are gone and the labels are titles (hover) plus the accessible names - a mouse
user reads the words on hover only; say if the rail should show labels on a wide screen. (f) The
group is `role="group"` with twelve tab stops, not `role="toolbar"` with arrow-key roving; say if
you want the toolbar pattern (it would add arrow reads the sheet's spec must then name). (g) The
`?` box keeps its text glyph rather than a drawn one; say if you want a drawn question mark. (h)
The rail hides nothing in Play - Save copy, the exports, the import, the toggles and `?` stay live
as the rows did; say if Play should lock the file boxes too. (i) Changes 1 to 14's questions still
stand.

**Departures from the brief:** one source commit for the model, the component, the shell, the
route, the retirements and the specs (the brief said atomic; a split would leave the shape halves
red between commits, and every file carries the one change); the four components are deleted, none
reduced (each was its row); spec 25's and 27's titles reworded where they named the rows (the gate's
titles term moves anyway with test 28 and the twelfth e2e title); the testid census drops seven
kept ids because they ride through `data-testid={box.id}` now (the transforms' precedent) - the
boxes still carry them and the e2e clicks them; the profile outcome went to the plate's status line
(the brief's first option); `layout.ts`, `Knob.svelte`, `ColourPicker.svelte`, `sequence.ts`,
`library.ts`, every catalog entry, the manifest, `pad-sim.ts`, `firmware-oracle.spec.ts`, the
Sandbox runtime and emitter Lua, `src/vendor/`, the fixtures untouched; STATE / ROADMAP /
REQUIREMENTS untouched; CAT-04 stays `[ ]`; not deployed; no worktree, no `node_modules` junction.

16. The playground's tuning controls reworked (2026-09-21, Fable)
--------------------------------------------------------------------------------

> review the configuration sliders for each playground configs. give them a rework for the UI and
> graphic style would be more modern and better. make the straigthforward, and use input fields
> where you seem fit instead of sliders. use your own best discretion to come up with design ideas
> how to make the configuration experience better.

Recorded: the tuning panel's controls (Knob.svelte's four skins - words, select, rail, colour -
KnobRack, TuningRegion, MidiField) are redesigned on the executor's discretion under a direction
set in the brief: one consistent field row per knob; numeric knobs as typed stepper fields
snapping to the entry's declared rungs (the budget is measured over those rungs, so a typed value
lands on the nearest one, never between); worded knobs as segmented controls up to four options
and a house-styled select beyond; CC / note / channel as typed fields with steppers; colour as a
swatch chip opening the picker inline; per-field reset kept; a per-card review table of every
knob and the widget it gets. The Bible's section 7 (segmented / select / slider) yields to the
user's word here.

**Done - change 16, 2026-09-21 (the tuning controls reworked; the user asleep, nothing asked).**
Five commits before the docs, no push, no device, no deploy: `0233414` feat(tune) - the view
rule, the six components, the new Stepper.svelte and sections.ts, the specs, seven labels; `e0ff250`
test(tune) - the e2e walks and the gate's quick constant at 1028; `b07c9c4` fix(tune) - the rule
read against the after-census (a segment character budget, three word tables, the e2e's lock words
imported from the module they live in); `9d30644` test(tune) - two walks read on the deployed bytes
(the stepper measured as its control, the hidden radios clicked through their labels, five leftover
rail calls in install.e2e.ts); `176e6ba` test(tune) - the walk reads ARC's arms as the segmented
row the fix commit made it; then this paragraph with `docs/TESTING.md`'s
"2026-09-21 change 16", `docs/TUNING-REVIEW.md` new, `scripts/knob-census.mjs` new (the review's
raw material) and the gate records `gate/change-16.*` (before, at `d4456a8`, on a clean worktree
`../hangar-gate-16` with its own `npm ci`, removed after the records were copied) and
`gate/change-16-after.*` (at `b07c9c4`). D-14 Q5 kept typed numerics to the Sandbox; the user's
word brings them to the playground, and this paragraph says so.

**The design, per widget (before -> after).** ONE ROW PER KNOB: 44px, a hairline between rows, the
label left in the micro face (quiet, ink while the row is hovered or holds focus), the control right,
then the reset box and the lock box - the tool rail's 44 squares with straight-line glyphs (an arrow
with a returning shaft for reset; a padlock whose shackle seats when held, the second channel beside
the word Lock / Locked in its name and title). The changed-from-default marker is a 2px rule in the
action colour down the row's start (the brief's item 7), with FIELD_CHANGED for a screen reader; the
reset box is present on every row and disabled at the default, so nothing reflows. Every row is its
own container and stacks under 364px (the label on a line, the control under it with the boxes at its
end): at the compact band (1280, the inspector 300) and on the phone (393) the rows are stacked, at
1440 and above (the body 386) they are one line; the e2e measures all three. NUMERIC KNOBS: the dot
rail and the track (a range input at opacity 0 under painted dots, a read-only readout) -> a typed
stepper field, `Stepper.svelte`: a spinbutton in the mono face with the unit after the value, a step
box either side out of the tab order (the field is the row's one stop), a hairline ladder of the rungs
under the text with the rung the field is at in the action colour (the one nod to where you are;
drawn up to 32 rungs, a mark alone above), ArrowUp / ArrowDown one rung, Home / End the ends, Enter
and blur committing. THE SNAPPING RULE, EXACTLY: a typed value is read on Enter or blur only (a
keystroke moves nothing); a number (integer or decimal) lands on the DECLARED rung nearest it by
absolute difference, a tie on the lower value, a number past either end on that end; text that is not
a number (or not a note name or number on a note ladder) leaves the field on the rung it showed, no
recompile; a landing on the rung already shown moves nothing. The boxes and the arrows walk the rungs
in VALUE order whatever order the entry declared them in (`valueOrder`, a stable sort, the declared
order kept where a rung is not a number), so SNAKE's 300 220 160 110 steps up in milliseconds and
POMODORO's 15 20 25 50 1 5 walks 1 5 15 20 25 50; the stamp encodes the declared index as before.
WORDED KNOBS: the pill-outlined radios -> a segmented control of joined boxes (real radios in labels
under a radiogroup, the shared edge collapsed, the chosen one on the action colour by edge and word,
no fill) up to four words that fit one line (SEGMENTED_MAX 4, SEGMENT_CHARS_MAX 40 across them), the
house select above (appearance none, the boundary hairline, the two-edge arrow); the twelve-key roots
and a scale list read as selects; a note ladder past NOTE_SELECT_MAX (24) is typed - ORBIT's 128.
CC / CHANNEL / NOTE / SEND: the two-column numeric grid of text fields (D-21's ResizeObserver) -> the
same row with a stepper over the closed list, VALIDATING as before (typedIndex: an offered value moves
the knob, an unoffered one is refused with the offered values named, never a snap), the boxes and
arrows walking the offered values in value order, the Lua channel's cue read by a screen reader and
on the label's title; no lock (never rolled); no XY pair exists on the playground (finding: none of
the 27 cards declares one). COLOUR: the 34px square + hex + Edit color link -> a chip (the 28px
square of the stored value beside its readout: the three channels for a lattice knob in the mono face,
the hue word for a palette) that opens the picker inline under the row as before, Edit color / Close
its hidden verb and its title, the reset box and the lock box on the row; the picker's head lost its
own lock (two locks on one knob) and its palette row its label, reset and lock (the row carries them);
the five-colour palette row stays inside the opened picker. BRIGHTNESS: the same row, the arrows
stepping by one, no boxes (a free 1..255 is not a ladder). THE SECTIONS: PDF page 5's Behavior /
Appearance / MIDI output -> Look (the colours, then the brightness field; always drawn), Feel (the
amounts, speeds, trails, sizes, modes), Sound (kind note or scale, and the words key, root, velocity,
inversion, modifier, octave, note), MIDI (surprise.ts's wire predicate, so the section is exactly what
Randomize preserves), Sync (the words sync, division, clock) - in that fixed order, an empty one not
drawn; `sections.ts sectionOf` is total and tune-ui.spec.ts walks every card's knobs through it (39 /
38 / 18 / 33 / 10). Randomize, Reset settings and Undo randomize sit after the last section (the
inspector's child; "where it is" read as a row of buttons under the fields, no longer under a Behavior
that does not exist); the stamp notice is the inspector's lead; the preview's hold line (change 8) is
under Sync; MIDI_HELPER and LUA_CHANNEL_CUE are read by a screen reader (no helper prose in the rows).
GONE: the rail and both skins, the forecast delta and its props (dead since 13.1-07 hid the meters),
the held bar and the home dot, the three older reset gestures (double-click, long press, Delete /
Backspace - the box is the reset, and Delete on a text field is an editing key), Knob.svelte's three
circles (D-15's six are three: ColourPicker.svelte :785 / :812 / :827, `CIRCLES` and radius.spec.ts at
three, radius.e2e.ts measuring the picker's on AURORA's opened block), the pill class off Knob.svelte's
word row (instrument.spec.ts's PILLED list says so), KnobRack.svelte's list / grid layouts.

**The per-card review** is `docs/TUNING-REVIEW.md`: every card, every knob, its kind, rungs, values,
default, section, widget, unit and finding, generated from `scripts/knob-census.mjs` with the findings
by hand. FIXED (no Lua, no rung value moved): seven labels gained their unit word (Step time (ms),
Key delay (ms), Ping speed (ms), Sweep speed (ms), Loop length (points), Interval (min) for Minutes,
Flash width (cells)) - `splitUnit` takes a trailing parenthetical off any label and the stepper shows
it after the value; three display bugs the census caught - TRACKPAD's edge flash and COMET's comet on
scroll read Internal / External since change 8's sync words collided on `true` / `false` (the sync
table is read by the knob's ID now, every other boolean under `mode` is On / Off), the presets' Send
(DIAL, FOUR FADERS, JOYSTICK: the vendored `note` kind over CC numbers) read E0 for CC 16 in the MIDI
field since change 8's note names (`isControllerNumber`: a MIDI destination that names no note is a
controller number), CULL's and STAGE's first key (USB HID usage ids under `note`) read as note names
(30 read F#1, 104 read G#7; KEY_WORDS reads 1 / F1 / Keypad 1 / F13); and six ladders that read as
numbers read as words through the knob's ID - QUADRANT's fill (Colour only / Colour and fill / High
contrast), STAGE's modifier (None / Ctrl / Alt / Shift), STARFIELD's edge (Soft / Hard, a rail of two
unnamed dots before), ARC's arms (41 / 82 / 123 as 1 / 2 / 3; PINWHEEL's 1 / 2 / 3 untouched), CULL's
legend brightness (2 3 4 6 as 33% / 50% / 67% / 100%), WHEELS' spring (256..2048 as the 640 / 320 /
160 / 80 ms a full deflection takes home), plus a comma list of integers as its numbers (ORBIT's pulse
sets) and four RGB triples as their hue words (QUADRANT's palettes). RECOMMENDED, the user's call:
the eight four-rung channel ladders (0 1 9 15) that refuse a typed channel outside them (sixteen would
be a value change inside one stamp character); the unsorted note ladders on POMODORO, QUADRANT, SNAKE
and STEPS (a re-sort moves the stamp indices); the tick-of-10-ms ladders (CULL's flash, STEPS',
MORPH's and ORBIT's trails, TRACKPAD's fade, COMET's tail) that read as bare numbers - a ms word table
keyed by id, as WHEELS' spring now is, needs no value change.

**Copy and test ids.** inspector-copy.ts: SECTION_BEHAVIOR and SECTION_APPEARANCE retired by name,
SECTION_MIDI "MIDI output" -> "MIDI", SECTION_LOOK / SECTION_FEEL / SECTION_SOUND / SECTION_SYNC,
STEP_DOWN, STEP_UP, SNAP_HINT added (-2 +7); KNOB_HOLD / KNOB_HELD, EDIT_COLOR / POPOVER_CLOSE,
FIELD_RESET / FIELD_CHANGED, MIDI_HELPER, LUA_CHANNEL_CUE kept with new homes (a title, a hidden
verb, a description). Test ids (the census reads literal `data-testid="…"`, 346 -> 351): -1
`colour-hold` (the picker's head lock); +7 `swatch-{knob.id}-changed` / `-reset` / `-hold`,
`{testid}-stepper` / `-input` / `-down` / `-up` (Stepper.svelte's, so `knob-<id>-input` and
`midi-field-<id>-input` ride through it - the MIDI field's input id is unchanged for the e2e);
`knob-{view.id}-changed` 2 -> 1 literal; every knob row and swatch row carries `data-index`,
`data-changed` and (a knob) `data-widget`; `midi-grid` kept on the MIDI rows; `edit-color` kept on
the chip. Three view exports retired (`WORD_ROW_MAX`, `railSkin`, `RailSkin`), five added
(`NOTE_SELECT_MAX`, `SEGMENT_CHARS_MAX`, `splitUnit`, `valueOrder`, `rankOf`, `nearestRung`);
`wordFor` and `widgetFor` take the knob's id.

**Proved.** `tune-ui.spec.ts` 13 -> 14 (the floor test re-aimed at the lock box, the stepper's three
controls and the chip; the census at 23 with the marker and the ladder tick named; the error ink's
carriers with Stepper.svelte; D-21's test replaced by the sections' - every card's knobs through
`sectionOf`, the witnesses, the five snippets, Look unconditional, the actions after the sections;
the widget rule with the shelf's split 39 colour / 30 words / 10 selects / 59 steppers through the
effective kind; the MIDI row rendered; the changed marker; the rows rendered - a stepper at SNAKE's
220 ms with rank 2 of 0..3 and four ticks, the foot's down box disabled, a long ladder's mark alone, a
segmented row's radios, a select's twelve options, the joined boxes' rules, every row a container
stacking under 364px); `view.spec.ts`'s three widget tests rewritten (the total mapping, the two-
integer row and PINWHEEL's stepper, the stepper's arithmetic - nearestRung on SNAKE's ladder, ties,
ends, decimals, a word, valueOrder, rankOf, splitUnit, the id-keyed words); `knobs.lua.spec`'s widget
list; `lua-smoke`'s five sync reads by id; `colour-picker.spec`'s tick against the picker's own home;
`copy.spec`'s lock as the 44 box; `instrument.spec`'s mono list at six and the pill list without
Knob's row; `radius.spec` at three circles; `stamp.spec.ts` untouched and green; every wire term
byte-identical (below). The e2e: `tuning.e2e.ts` 11 -> 12 titles (the walks step spinbuttons and read
the rows' data-index; the CC title steps the boxes and the arrows and reads the cue as a description
and a title; the chip's Close / Edit color as text and title; tpad's Scroll with Home / End; the
twelfth walks every widget kind on SNAKE and ARC at 1280 - a typed 100 snapping to 110, ArrowUp to
160, the up box to 220, End to 300, a word refused on blur with no recompile, 999 clamped, the reset
box and the lock, a segment as one click, the chip opening the palette with its readout following,
the sections in order, ARC's wave shape select landing on Square, every control on the row at 44px);
`tuning-webkit.e2e.ts` (stacked at both projects' widths, side by side at 1440, stacked again at 320,
no sideways scroll at any of the three, every stepper control 44px on the phone - the project's phone
is the iPhone 15 at 393, the brief's 390); `install.e2e.ts` and `session.e2e.ts` step the first
stepper and read LUMEN's depth off its row; `radius.e2e.ts` three circles, none a knob's. Counts
carried + delta: quick 96 / 1027 + 1 todo -> **96 / 1028 + 1 todo** (run A the gate's quick term at
`b07c9c4`, `check-counts` 96 / 1028 / 1 todo, exit 0; run B `vitest run --project server --maxWorkers=2` at `176e6ba` on the gate's build through `check-counts.mjs 96 1028` - 96 files, 1028 passed, 1 todo, exit 0); check 675 -> **677** (+2:
Stepper.svelte, sections.ts); lint clean; e2e 95 / 110 -> **96 / 111**; utilities **44 -> 44** (0
disappeared, 0 appeared - the census's class words `box`, `field`, `ladder`, `tick`, `at`, `chip`,
`rows`, `knob`, `stepper` spell no utility; `inline-flex`, `grid`, `flex`, `border` were already
named); catalog **27**; testids 346 -> **351**; copy exports -2 +7; OG 27 files / 159,169 B /
`9becd682…` and the four fixtures unmoved; `src/` 28 modified, 2 added, 0 deleted, 0 renamed against
`d4456a8`; the seven relabelled entries' Lua untouched (`hash-wire` set and full equal). **The gate's
terms** (`--before change-16` at `d4456a8`; `--after change-16 --against change-16 --check 677` at
`b07c9c4`): **the wire set `654e202e…`, the full `1c4acf19…` and the sandbox set `3bdb5974…` ALL
EQUAL - no Lua moved, no row column, no runtime part**; the census `be886210…` -> `cd28c1e3…` (3141
-> 3163 literals, 210 -> 212 files); the copy exports `1ce943b4…` -> `d73d361a…`; the testids
`60e7351e…` -> `f29b0454…`; the SCOPED CSS `11a91a6a…` -> `a373572a…` and the raw `8ab7ccda…` ->
`0c4948f8…` (Knob.svelte's row / option / select / box rules, Stepper.svelte's, MidiField.svelte's,
Swatch.svelte's chip, BrightnessField.svelte's row, KnobRack.svelte's hairline, TuningRegion.svelte's
rows; the rail, the track, the dots, the delta, the picker's lock and the region's grid gone);
utilities 44 -> 44; the titles `8783d842…` -> `5e64e0ca…` (1028 -> 1029 vitest titles incl. todo, 110 -> 111 playwright runs); the JS `acfddf4d…` ->
`db4331c6… (74 -> 73 files)`; comment lines: every touched header at the ten-line rule (ColourPicker.svelte's 62 is as it
was, the refuse-list's). The script exits 1 at the census by design; the name-status term reads 2
added, stated above. **Chunks** (a fresh detached wrangler dev on 4173 each, stopped through
PowerShell, HTTP 000 after each; 5173 untouched; the build stamped `b07c9c4`): c3 by its two files **22 passed** at `176e6ba` (its first run at `b07c9c4` read 20 passed with the two walks red on the tests themselves - the phone walk measured the stepper's input at 42px between the field's two hairlines, the widget walk called check() on a radio hidden inside its label - and its second at `9d30644` 21 passed on the walk's stale arms expectation, each fixed in the test commit named above); c5 by its four files **11 passed** at `176e6ba` (radius's title on both engines measuring the picker's three circles and none of a knob's; 11 at `b07c9c4` and at `9d30644` too); c1 by its two files **33 passed** at `9d30644` (its first run at `b07c9c4` read 31 passed with the two store-then-change titles red on five stale rail calls, fixed in `9d30644`; not rerun at `176e6ba`, which touched tuning.e2e.ts alone). c2 and c4 not run: no browse, catalog, fidelity, first-experience, library or sandbox title reads the workspace's rows, and the Sandbox's swatch row and brightness field - the same components - are walked by radius's title in c5.

**Questions for the user.** (a) The sections are Look, Feel, Sound, MIDI, Sync in that order - Look
first because the colour is the first thing a card shows; say if Feel should lead. (b) Randomize,
Reset settings and Undo randomize sit after the last section; say if you want them under Feel (where
Behavior's were) or pinned. (c) CULL's and STAGE's keystroke knobs (the first key, the modifier, the
key delay) sit under Sound by the word `key`; say if you want a sixth group for keystrokes, or them
under Feel. (d) The step boxes are out of the tab order (the field's arrows step); say if you want them
as tab stops. (e) A typed value commits on Enter or blur and snaps silently; say if a landing should be
announced (the live region is silent on a knob move by Phase 5's contract). (f) The unit is taken from
a label's trailing parenthetical only; say if you want a unit on the tick ladders (a ms word table
needs no value change) or on the unit-less amounts. (g) The four-rung channels and the unsorted note
ladders are recommendations in `docs/TUNING-REVIEW.md`; say which to take (each moves the stamp). (h)
The picker's palette row shows its swatches right-aligned under the head; say if you want them left.
(i) The lock and the reset are icon boxes with titles; say if you want their words visible on a wide
screen. (j) The three older reset gestures are gone (double-click, long press, Delete / Backspace);
say if any should return. (k) Changes 1 to 15's questions still stand.

**Departures from the brief:** five commits before the docs, not two (the fix commit is the rule read
against the after-census, which the brief's "read every card's rack" asked for and the first cut
under-served; the second and third test commits are what the chunk runs found in the walks
themselves - a measure on the wrong box, a hidden radio, five stale calls, one stale expectation); the Sandbox's `RegionInspector.svelte` gained one line (`lock={false}` on its swatch
row - the swatch rows carry a lock now and the Sandbox rolls nothing); `ColourPicker.svelte` lost its
head lock, its `onhold` / `onforecast` props and its palette row's label / reset / lock - a
behaviour change on the refuse-list file, by the brief's word ("if the rail dots go, their circle
exemptions go with them"), its three circles untouched at their new lines; `lua-smoke.spec.ts`'s five
sync reads pass the knob's id (not on the brief's list; the word split needed it); `layout.ts` kept
its D-21 numbers unread by the region; the brightness field has no step boxes (a free range, not a
ladder); the picker's palette row inside an opened block is right-aligned; STATE / ROADMAP /
REQUIREMENTS untouched; CAT-04 stays `[ ]`; not deployed; the worktree removed.

> [screenshot: ARC's rack after change 16 - the controls at five different widths and x positions,
> the reset and lock boxes floating, the chips of varying width, the three action buttons wrapped
> 2 + 1]
> the aesthetic of this panel is atrocious. look at the state of this

Recorded 2026-09-21, change 16b: the rack is put on one strict grid. Every row is the same
four-column grid - label | control | reset | lock - with the label column a fixed width, the
control column filling the rest, the reset and lock columns fixed 44 px and ALWAYS present (an
empty cell where a knob has none, so nothing floats); every control fills the control column
edge to edge at one height (44 px): the colour chip, the stepper, the segmented control (its
boxes sharing the width equally), the select, the brightness field; one gap, one hairline
rhythm; section heads with one spacing; the action buttons in a grid of equal cells. Then the
result is checked against a screenshot before it lands.

**Done - change 16b, 2026-09-21 (the rack on one strict grid; the user asleep, nothing asked).**
One source commit before the docs, no push, no device, no deploy: `a65a95f` feat(tune) - the six
row components on the grid, the inspector's rhythm and its pinned actions, the share control's
width, the two specs, `e2e/rack-grid.e2e.ts` new, `tuning-webkit.e2e.ts`'s numbers; then this
paragraph with `docs/TESTING.md`'s "2026-09-21 change 16b" and the gate records `gate/change-16b.*`
(before, at `26bdab3`, on a clean worktree `../hangar-gate-16b` with its own `npm ci`, removed
after the records were copied; its first run read no quick summary with the preview server and a
chunk beside it and was re-recorded alone) and `gate/change-16b-after.*` (at `a65a95f`).

**The grid, per breakpoint.** EVERY ROW (Knob.svelte, Swatch.svelte, MidiField.svelte,
BrightnessField.svelte) is `grid-template-columns: var(--tune-label-w, 96px) minmax(0, 1fr) 44px
44px`, areas `label control reset lock`, `column-gap: 8px`, `min-block-size: 44px`, the label
centred and wrapping at spaces only (`overflow-wrap: normal` - the first cut broke BRIGHTNESS as
BRIGHTNE / SS), the two box columns always drawn (a knob with no lock - CHORUS's every row, the
MIDI rows, the brightness row, the Sandbox's swatch row - keeps an empty 44 cell; the reset box
disabled at the default), the changed rule in a 4px start padding. THE CONTROL fills its column
(`.control` a grid, `inline-size: 100%`, `min-block-size: 44px`) and every skin is 44 tall and
border-box: the chip a 42 x 42 square of the stored value inside the chip's hairline at its left
edge (reads as 44) with a hairline after it, the readout 12px on, the text clipped never widening;
the stepper `44px minmax(0, 1fr) 44px`, the value 12px in, the unit 11px quiet 8px from the box,
the ladder under the field; the segmented control equal cells (`repeat(var(--segment-cols),
minmax(0, 1fr))` once the words and the column are measured - four words 2 x 2 never 3 + 1, three
that cannot share a line 2 + 1, QUADRANT's fill three lines; `repeat(auto-fit, minmax(chars *
0.5em + 8px, 1fr))` for the server's render; 3px cell padding so None / Ctrl / Alt / Shift holds
four cells of 43 at 1440); the select full width, the arrow inside its 36px end padding; the
brightness field the stepper shape (Stepper.svelte with rank value - 1 over 255 rungs, the boxes
and the arrows stepping by one, the mark alone, read-only under Play holding every step).
`--tune-label-w` is 96px at EVERY breakpoint, set once on Inspector.svelte's `.body` (the rack's
root in the playground, the probe and the Sandbox): the brief's 136 / 112 do not fit - the
inspector's body is 385 at 1440 (438 less 26 + 26) and a one-line row is 4 + label + 8 + control +
8 + 44 + 8 + 44, so 136 leaves 130 for a control whose stepper needs 44 + a field holding `250
points` (GHOST, 83) + 44 = 171; 96 leaves 173 and holds the longest label word (CROSSFADER /
CONTROLLER 98, BRIGHTNESS 94.5) within 2px. THE SWITCH: under 380px of container the label takes a
line of its own (`grid-template-columns: minmax(0, 1fr) 44px 44px`, areas `label label label` /
`control reset lock`, 4px between, 6px block padding) and the control still fills its column with
the boxes at the row's end. Measured: 1440 x 900 body 385 - one line, every control x 1137..1310
(173), the reset box at 1318, the lock at 1370, the pitch 45 (44 + the hairline); 1280 x 720 body
336 - stacked, the control 922..1150 (224), the reset at 1158, the lock at 1210, the pitch 74; 393
x 852 body 341 - stacked, the control 30..263, the reset at 271, the lock at 323. FINDING, not
fixed: the compact band's inspector renders at 388 (the wide clamp), not layout.ts's 300 -
`+layout.svelte` sets `--inspector-min` / `--inspector-max` inline and the band's media query
cannot override an inline style; the rack fits either way. So the switch is at the band: one line
at 1440 and above (and in the compact band from 1421, where the body reaches 380), stacked from
1024 to 1420 and on the phone, one line again where the inspector sits under the surface (768 to
1023, the body 716+). ROW RHYTHM: one hairline between rows (the rack's, the rows block's, the
swatch block's), no other gap; the section word 8px above its rows, 24 under the divider, the
divider 24 under the last row, the first head 16 under the lede (the body's 8 + the head's 8). THE
ACTION BUTTONS: Randomize / Reset settings / Undo randomize and the pinned Save copy / Share
snapshot / Export for Grid Editor (and the Sandbox's Duplicate / Delete element) are
`repeat(auto-fit, minmax(168px, 1fr))` at 8px, every button 44 (`block-size: 44px` on the region's;
the route's `min-block-size`); 168 not 160 because Export for Grid Editor is 182 on one line and a
164 cell wrapped it at 1280 - so 2 + 1 equal cells at 1440 (188 each) and a column of three under
a body of 344 (1280, the phone); CopyLink's control fills its cell (`inline-size: 100%`, was
fit-content). The picker's palette row (Knob.svelte with `caption={false}`) is `bare`: no label
cell, the swatches from the block's start 8px apart, the two empty cells at its end.

**Screenshots** (the scratchpad, not the tree; vite preview on 4174, never 5173, stopped by port
after): before - `shots/before/before-{arc,orbit,chorus,morph,sandbox}-{1440x900,1280x720,393x852}.png`
at `26bdab3`: at 1440 the chips right-aligned at two widths (Azure 95, Amber 93), the brightness
field 160 at the column's end, the segmented 133, the select 177 wide at another x, the MIDI
steppers full width, the reset boxes at three different distances, the actions 2 + 1 with Share
snapshot narrower; at 1280 and 393 the stacked rows with the controls at five widths hugging the
left and the boxes floating right - the user's screenshot. After - `shots/after/after-*.png` at
`a65a95f`: every control one column, one x, one right edge, 44 tall; the boxes on two lines; one
pitch per section; and `shots/after6x/*` for the segmented edge cases (STAGE's None / Ctrl / Alt /
Shift four across and F13 / 1 / F1 / Keypad 1 two by two, WHEELS' spring two by two, QUADRANT's
fill three lines, CULL's legend four across, JOYSTICK's bend 2 + 1), `shots/picker/*` for the opened
picker (SNAKE's palette row from the left; AURORA's rails). What I saw on the first after cut and
fixed before committing: BRIGHTNE / SS (the label broke mid-word at 80), the export button wrapped
to two lines at 1280 and 393, MODIFIER 3 + 1 and SPRING SPEED 3 + 1 (the auto-fit floor from a
character estimate, then a 44 floor, then the 1px overlap - three cuts before the counted columns).

**Proved.** `e2e/rack-grid.e2e.ts` new (chromium, two titles): ARC's rack at 1440 x 900 and 1280 x
720 - the seven rows in order, every control's x and x + width within 1px of the first row's and
44 tall, every reset box at one x and 44 x 44 and 8px past its control, every lock at one x 8px
past the reset box and ending at the row's edge, the lock rows the four knob rows, the pitch
between neighbouring controls in a section one value (45 at 1440, 74 at 1280 - measured on the
controls because the hairline sits on the row below, inside a swatch row's box and on a knob row's
root), the three action cells equal and 44, nothing sideways on four ids. `tune-ui.spec.ts`: the
rows test reads the four-column rule, the 8px gap, the 44 floor, the control's width and floor,
the 380 switch with the two box columns kept in the stacked rule, in all four files; the chip
`inline-size: 100%` / `block-size: 44px` and the square 42; the marker keyed on `class:changed
class:bare={!caption}`. `instrument.spec.ts`: five mono uses across five files (the brightness
field's rides through Stepper.svelte). `tuning-webkit.e2e.ts` reads 385 / 336 / 380. Counts
carried + delta: quick 96 / 1028 + 1 todo -> **96 / 1028 + 1 todo** (run A `vitest run --project
server --maxWorkers=2` on the tree before the commit, 96 files, 1028 passed, 1 todo, exit 0; run B
the gate's quick term at `a65a95f`, `check-counts` 96 / 1028 / 1 todo, exit 0); check 677 -> **677**;
lint clean; e2e 96 / 111 -> **98 / 113** (+2 titles, +2 runs, one file); utilities **44 -> 44** (0
disappeared, 0 appeared; `measured`, `bare`, `stretch` spell none); catalog **27**; testids 351 ->
**350** (-1: the literal `brightness-field-input` rides through Stepper.svelte's `{testid}-input`,
the value unchanged for the e2e; `brightness-field-stepper` / `-down` / `-up` exist in the DOM
through the same template); copy exports equal (`d73d361a…`); OG 27 files / 159,169 B / `9becd682…`
and the four fixtures unmoved; `src/` 10 modified, 0 added, 0 deleted, 0 renamed against `26bdab3`.
**The gate's terms** (`--before change-16b` at `26bdab3`; `--after change-16b --against change-16b
--check 677` at `a65a95f`): **the wire set `654e202e…`, the full `1c4acf19…` and the sandbox set
`3bdb5974…` ALL EQUAL**; the census `cd28c1e3…` -> `d22fbc7e…` (3163 -> 3163 literals, 7494 ->
7490 occurrences: `brightness-field-input` 1 -> 0, `.word` 0 -> 1, the three key names and
`input` / `numeric` / `text` down one each with the brightness field's own input); the copy exports
equal; the testids `f29b0454…` -> `f057796d…` (351 -> 350); the SCOPED CSS `a373572a…` ->
`763cc8d4…` and the raw `0c4948f8…` -> `2baa3fb9…` (Knob.svelte's row / control / options /
measured / option / word / swatches / select rules, Swatch.svelte's row / control / chip / square /
readout, MidiField.svelte's and BrightnessField.svelte's row / control, Stepper.svelte's stepper /
box / field / input / unit / ladder, TuningRegion.svelte's actions / action, Inspector.svelte's
body / group-title / divider / actions, CopyLink.svelte's share / control); utilities 44 -> 44; the
titles `5e64e0ca…` -> `05122e0b…` (1029 vitest titles incl. todo; 111 -> 113 playwright runs); the
JS `db4331c6…` -> `c3035823…` (73 files); comment lines: every touched header at the ten-line rule
(`comment-lines.mjs --todo` prints nothing for the eight files). The script exits 1 at the census
by design; the name-status term reads 10 modified; the refuse-list stat names `Knob.svelte` (130 +
/ 42 -, the grid; its circles none since change 16). **Chunks** (a fresh detached wrangler dev on
4173 each, stopped through PowerShell, HTTP 000 after each; 5173 untouched; the build stamped
`a65a95f`'s tree): c3 by three files (`tuning`, `tuning-webkit`, `rack-grid`) **24 passed** (its
first run read 22 passed with the two new titles red on the test itself - the pitch measured on
the rows' tops, which the hairline moves - and a second run of the file alone red on the actions
locator counting the pinned three; both fixed before the commit); c5 by its four files **11
passed**; c1 by its two files **33 passed** (the brightness walk in `install.e2e.ts` on the
stepper shape); c4 by its five files **23 passed** (the Sandbox's brightness walk, its history).
c2 not run: no browse title reads the rack.

**Departures from the brief:** `--tune-label-w` 96 at every breakpoint, not 136 / 112 / 96 (the
arithmetic above: 136 and 112 leave no control column at 385 and 336); the row's start padding 4,
not 8 (the same arithmetic - the changed rule is 2px inside it); the stepper's unit 11px and 8px
from the box, not 12 (`250 points`); the segmented cells 3px of padding, not 12, and counted
columns instead of a bare `repeat(n, 1fr)` (which spilled Pentatonic, Keypad 1, Colour and fill
and 640 ms over their cells at 173 and 224 - the count keeps every cell equal and every word
whole); the action cells' floor 168, not 160 (Export for Grid Editor at 1280); the chip's square
42 inside the chip's hairline, not a 44 with its own; Knob.svelte gained a `$effect` with a
ResizeObserver (the columns follow the column's width; TuningRegion.svelte still carries none);
Stepper.svelte commits the shown text on blur even when nothing was typed (so the Sandbox's
history boundary always hears Enter and blur); the brightness field lost its own input and the
`brightness-field-input` literal (the census); `Inspector.svelte` (shell) and `CopyLink.svelte`
moved for the rhythm and the equal cells; `e2e/rack-grid.e2e.ts` is `.e2e.ts` (the Playwright
match), not `.spec.ts`; the gate's before record was taken twice on the worktree (the first with
no quick summary beside the preview server and a chunk, deleted before the copy); the compact
inspector's 388 is a finding left alone (`+layout.svelte`, the shell); STATE / ROADMAP /
REQUIREMENTS untouched; CAT-04 stays `[ ]`; not deployed; the worktree removed, the main
`node_modules` intact (262 entries).

Change 16c, recorded 2026-09-21 on the 16b screenshots: the playground rack is on the grid, but
the Sandbox's inspector is now two registers in one panel - Identity, Behavior and MIDI output
still the 10B shape (a sentence-case label above a full-width field, helper prose between the
rows, native selects and checkboxes) while Appearance below them is the new four-column grid.
The whole Sandbox inspector goes onto the same grid as the playground rack: every field a
`label | control | reset | lock`-shaped row (the lock column empty in the Sandbox except for the
element's own Locked toggle, which becomes a lock box on the name row), the same 44 px controls
(typed fields as steppers where the value has rungs, the min / max / CC / channel / note fields as
plain typed fields at 44, selects and segmented controls as the rack's, checkboxes as segmented
On / Off), helpers as titles and sr-only text, the section heads as the rack's, the two action
buttons as equal cells. Proved by screenshots the same way.

**Done - change 16c, 2026-09-21 (the Sandbox's inspector on the rack's grid; the user asleep,
nothing asked).** One source commit before the docs, no push, no device, no deploy: `88f9921`
feat(sandbox) - `RegionInspector.svelte` rewritten on the grid, `Stepper.svelte` with three props,
`BrightnessField.svelte`'s helper out of the flow, `sandbox/copy.ts` +3, `sandbox-ui.spec.ts`'s
pins, `sandbox.e2e.ts` re-aimed, `rack-grid.e2e.ts` +2 titles; then this paragraph with
`docs/TESTING.md`'s "2026-09-21 change 16c" and the gate records `gate/change-16c.*` (before, at
`08f1061`, on the main tree - clean at HEAD, no worktree) and `gate/change-16c-after.*` (at
`88f9921`).

**Each field's control, before -> after.** ELEMENT NAME: a sentence-case label over a full-width
16px field -> the eyebrow label in the label column, a typed field filling the control column at
44 (the house face at the field's 15px, a boundary hairline, 12px in; quiet under Play), and the
LOCKED checkbox with its helper paragraph -> the rack's lock box in the row's lock column (a real
`<button aria-pressed>` named Locked, `field-locked` on the box, the shackle seated when every
member is locked, `aria-pressed="mixed"` when they differ, the helper its title and description;
over a set, which has no name row, the box rides the Type row). TYPE: a label over a bare word ->
a row whose control column holds the kind word in the field's face at the field's 12px inset,
the ink, NO boundary - a fact reads as a fact; a disabled field would have read as a field that
refused (the Bible's "not editable" without looking broken; `field-kind` on the word as before).
ORIENTATION, MODE (fader, pad), SPEED, OUTPUT: native selects -> the rack's segmented control of
two words (real radios in labels under a radiogroup, joined boxes of equal width, the chosen one
on the action colour by edge and word, `data-value` on the group; two columns counted from the
words, never measured - every Sandbox word row has two). SPRING, TOGGLE: native checkboxes -> a
segmented Off / On (`SWITCH_OFF` / `SWITCH_ON`), never a checkbox. KNOB MODE (four words),
TOUCHES (1..5), GROUP (None + 8): native selects -> the rack's select (appearance none, the
hairline, the two-edge arrow, full width at 44; Touches keeps its snap-back on a refusal). CC
NUMBER, CC NUMBER (Y), CHANNEL, MIN, MAX, SPRING VALUE, NOTE: full-width 16px text fields in a
2 x 2 grid -> `Stepper.svelte` filling the control column over the field's closed range (0..127,
1..16; the note 0..127 shown as its name): every keystroke through the editor's `editNumber` as
before (a refusal keeps the text, `aria-invalid` and its line under the row, the error ink on the
stepper's boundary), Enter and blur through `commitField`; the boxes and the arrows step by one
from the MODEL's value (a refused text steps from the model, as MidiField does) - each step a
keystroke to `editNumber`, coalesced under the field's key until blur or Enter, the brightness
field's shape; the ladder's mark alone (128 rungs; the channel's 16 ticks drawn). HELPERS
(Mode, Speed, Touches, Spring, Toggle, Group, Note, Min / Max - Max reads Min's description -,
Locked, the brightness's, the recent colours'): prose paragraphs between the rows -> the label's
`title` and an sr-only span the control is described by; the Arrange, Shared controllers and
New elements helpers keep their lines (not field helpers; the brief kept their content). MIXED:
a stepper shows `Mixed` as its placeholder over an empty value (the sans face, quiet), a select
its blank disabled option, a segmented control neither word with `data-mixed` on the group, the
lock `aria-pressed="mixed"`; a value applies to every member as before. THE RESET COLUMN: empty
on every row - the Sandbox has no per-field default - except Color (the swatch row's, back to
the palette's default, which it had) and Brightness (255), both on one x. SECTIONS: Identity
(new, titled - `IDENTITY`), Behavior, MIDI output, Appearance, on Inspector.svelte's rhythm (8
under the word, 24 round the divider, the first head 16 under the lede); Arrange (a set) and
Shared controllers (nothing selected) the same heads; the Arrange row four boxes to a line. THE
ACTIONS: Duplicate / Delete element fill Inspector.svelte's `repeat(auto-fit, minmax(168px,
1fr))` cells at 44 (2 at 1440, a column at 1280 and the phone, the rack's rule). The eyebrow,
the headline and the units chip as before. GONE from RegionInspector.svelte: the 2 x 2 grid and
its ResizeObserver over `NUMERIC_GRID_REFLOW` (layout.ts keeps the number), the `check` and
`mixedOption` snippets, the `identity` lead (Identity is a section), the `data-field` attribute,
the visible helper paragraphs, the `lock` class on the Play line (`hold` now; `.lock` is the box).

**Ids moved and walks re-aimed.** No DOM value renamed: `field-cc`, `field-channel`, `field-min`,
`field-max`, `field-note`, `field-spring-value`, `field-name` stay on their inputs (Stepper.svelte's
new `inputTestid` prop; its boxes are `field-cc-down` / `-up`, its root `field-cc-stepper`);
`field-mode`, `field-speed`, `field-spring`, `field-toggle`, `field-output`, `field-orientation`
sit on the radiogroup (a select or a checkbox before); `field-group`, `field-touches` and the
knob's `field-mode` on the select as before; `field-locked` on the lock box; `field-kind`,
`orientation-problem`, `touches-problem`, `field-*-message`, `colour-mixed`, `recent-colours`,
`arrange-*`, `reset-defaults`, `duplicate-element`, `delete-element` as before. Re-aimed in
`sandbox.e2e.ts`: the options walk's `field-mode.selectOption("relative")`,
`field-speed.selectOption("full")`, `field-spring.check()`, `field-toggle.check()` and
`field-output.selectOption("note")` -> the word clicked through its label
(`getByTestId(id).getByText(word).click()`), and the draft's read-back `toHaveValue` /
`toBeChecked` on those four -> `toHaveAttribute("data-value", …)`; the selection walk's
`field-locked` `toBeChecked()` -> `aria-pressed="true"` and `uncheck()` -> `click()`; the
geometry walk's `field-orientation` `toHaveValue("vertical")` -> `data-value`. Unchanged: every
`fill` and `press("Enter")` on the typed fields, the knob's `field-mode` and `field-group` /
`field-touches` `selectOption`, the brightness walk, the Mixed placeholder read on
`field-channel`. Copy exports +3 (`IDENTITY`, `SWITCH_OFF`, `SWITCH_ON`); testids 350 -> 341 as
literals (ten now ride through snippet arguments or the stepper's prop, `field-locked` a literal).

**Screenshots** (the scratchpad, not the tree; vite preview on 4174, never 5173, stopped by port
after): before - `shots/16c-before/{1440x900,1280x720,393x852}-{fader,button,knob,xy,multi,none}.png`
and `-tall.png` (the same width on a 2400-tall viewport, so the whole body is one image) at
`08f1061`: the two registers - Identity, Behavior and MIDI output in 10B's shape over
Appearance's grid. After - `shots/16c-after/*` at `88f9921`: every row on the grid, every
control one x and one right edge, 44 tall, the lock at the name row's end, the section heads on
one rhythm. Measured (the same figures as the rack's, on every state): 1440 x 900 - the body 385,
one line, the control 1137..1310, the reset at 1318, the lock at 1370, the pitch 45 (123 across a
divider); 1280 x 720 - the body 336, stacked, 922..1150, the reset at 1158, the lock at 1210, the
pitch 74; 393 x 852 - stacked, 30..263, the reset at 271, the lock at 323. What I saw on the
first after cut and fixed before committing: the Arrange row over a set wrapped 7 + 1 at 385
(eight 44 boxes at 8px are 408; it did before, too) - a grid of four to a line now, the
alignments over the centres and the spacings. Seen and left: over a set whose colours differ the
"Mixed" line under the Color row (13A's `colour-mixed`) makes that one pitch 76 - it is the
marker, and the chip alone would show the first member's colour as everyone's. The first shot
run's knob state was nothing selected - the script's hotkey went to a focused radio, not the
plate (the script's fault, fixed in the script; the e2e walks focus the plate first).

**Proved.** `sandbox-ui.spec.ts` 28 / 28 (seven pins moved with the shape, every behavioural
assertion kept; docs/TESTING.md names each); `tune-ui.spec.ts`, `instrument.spec.ts`,
`identity.spec.ts`, `radius.spec.ts` untouched and green; `e2e/rack-grid.e2e.ts` 2 -> 4 titles
(the Sandbox with a fader at 1440 and 1280: eleven rows by label, one x and one right edge, 44
tall, the reset boxes on Color and Brightness alone at one x, the lock on the name row alone
ending at the row's edge, seven pairs one pitch, two equal action cells, nothing sideways);
`sandbox.e2e.ts` 12 titles green on the new shape at the first run. Counts carried + delta:
quick 96 / 1028 + 1 todo -> **96 / 1028 + 1 todo** (run A on the tree before the commit, run B the
gate's quick term at `88f9921`, `check-counts` 96 / 1028 / 1 todo, exit 0 both); check 677 ->
**677**; lint clean; e2e 98 / 113 -> **100 / 115**; utilities **44 -> 44** (0 disappeared, 0
appeared); catalog **27**; testids 350 -> **341**; copy exports +3; OG 27 files / 159,169 B /
`9becd682…` and the four fixtures unmoved; `src/` 5 modified, 0 added, 0 deleted, 0 renamed
against `08f1061`. **The gate's terms** (`--before change-16c` at `08f1061`; `--after change-16c
--against change-16c --check 677` at `88f9921`): **the wire set `654e202e…`, the full `1c4acf19…`
and the sandbox set `3bdb5974…` ALL EQUAL**; the census `d22fbc7e…` -> `7daf68b2…` (3163 -> 3157
literals); the copy exports `d73d361a…` -> `9d3640cb…`; the testids `f057796d…` -> `b89c69d9…`;
the SCOPED CSS `763cc8d4…` -> `b2a15f74…`, the raw `2baa3fb9…` -> `d104bbf0…`; utilities 44 -> 44;
the titles `05122e0b…` -> `afef833c…` (1029 vitest incl. todo; 113 -> 115 playwright runs); the JS
`c3035823…` -> `f619c486…` (73 files); RegionInspector.svelte's header 11 -> 10 against the rule
(it was over before); the script exits 1 at the census by design; the refuse-list stat empty.
**Chunks** (a fresh detached wrangler dev on 4173 each, stopped through PowerShell, HTTP 000
after each; 5173 untouched): c3 by three files **26 passed** (its first run 2 failed / 24 passed
on the test itself - the reset expectation named Brightness alone where the swatch row carries
one; fixed before the commit); c4 by its five files **23 passed** at the first run; c5 by its four
files **11 passed**. c1 and c2 not run: no title of theirs reads the Sandbox's inspector.

**Departures from the brief:** the reset cell is filled on Color as well as Brightness (the
swatch row had one, back to the palette's default; the brief said Brightness alone); Identity is
a titled section (+1 copy export `IDENTITY`) so the four sections share one head rhythm - the
Bible's page 3 had the name above the first head; over a set the lock box rides the Type row (a
set has no name row); the brightness and the recent colours helpers went out of the flow too
(the brief listed the brightness's; the recent colours' was prose between two rows of
Appearance); the Arrange row is four boxes to a line (not in the brief; 7 + 1 at 385 was ragged);
`Stepper.svelte` gained three props (`inputTestid`, `placeholder`, `mixed`) and a placeholder
rule so the Sandbox's ids and its Mixed state ride the shared shape (renaming the seven typed
ids to `-input` would have moved sixty test lines for no DOM gain); the Sandbox's segmented
controls count their two columns and never measure (Knob.svelte's ResizeObserver is for four
words); a step box sends the next value through `editNumber` as a keystroke, coalesced until blur
or Enter (the brightness field's rule; the brief named Enter / blur for the typed path only);
`border-radius: 0` stays on the name field, the select, the boxes and the buttons where the user
agent would round (the house pattern; radius.spec.ts reads `[1-9]`), no radius above zero anywhere;
the `data-field` attribute on the typed inputs is gone (no suite read it); the gate's before record
was taken on the main tree at HEAD (clean) rather than a worktree; STATE / ROADMAP / REQUIREMENTS
untouched; CAT-04 stays `[ ]`; not deployed.

17. In-depth MIDI configuration and MIDI RX, every sending element (2026-09-23)
--------------------------------------------------------------------------------

> we need more in depth MIDI config options. every element that send MIDI across HANGAR needs to
> have an easy way to setup to send not just different messages but different channels etc.
> research how BOTOR (and the Grid Editor) handles MIDI configuration and use that as reference.
> also implement MIDI RX.  Do this for ALL playground configs AND for all elements in sandbox

Facts read before asking. The Grid Editor's MIDI block (`grid-editor/src/renderer/config-blocks/
Midi.svelte`): Channel, Command (the status byte - note on 144, note off 128, CC 176, program
change 192, channel pressure 208, poly aftertouch 160, pitch bend 224), Parameter 1, Parameter 2,
and a Mode - Default 7-bit, CC 14-bit, NRPN, NRPN 14-bit - carried as `gms`'s fifth argument
(the firmware's `simplemidi.lua` splits CC 14-bit into CC n + n+32 and NRPN into 99 / 98 / 6 / 38).
Its RX is two checkboxes, "Sync value" and "Sync LED intensity", written as
`self:gmrr(-1,ch,cmd,p1,{value,led},mode)`; `gmrr` (simplemidi.lua:81) installs a per-event
`midirx_cb` matching channel, command and parameter 1 on host traffic (header INSTR 13). BOTOR
(`grid-editor/src/renderer/main/zona/_pad.ts`, the vendored compiler): `gmrr` does not work on the
touch element ("there is no MIDI RX event on a touch element", `_pad.ts:2822`), so BOTOR assigns
`self.midirx_cb=function(s,h,v)` from the Setup directly and, for its motor faders, moves a
fader's target when the host sends its CC (`motorRx`, `_pad.ts:1978`) - value sync with the
lights following. HANGAR today: most cards carry one Channel knob (often four rungs) and a fixed
message kind per send; the Sandbox has CC / Note per button and CC elsewhere, one channel each.
Questions put to the user; answers recorded below.

Questions asked 2026-09-23: (1) what RX does - sync value / sync LEDs / DAW sets colours;
(2) the message types for a continuous send - the Editor's full set, or common only;
(3) the playground's layout - per output, or one channel per card; (4) a card that cannot fit -
system slots, or fewer options.

> (1) Sync value, Sync LEDs, DAW sets colours  (2) Common only  (3) Per output  (4) Use system slots

Recorded: 
- Every MIDI output, playground and Sandbox, has Type, Channel (1..16, all sixteen) and Number.
  Continuous outputs (faders, XY axes, knobs, the playground's gesture sends): CC, Pitch bend,
  Channel pressure (Number unused for the last two). Trigger outputs (notes, buttons, steps,
  bites): Note or CC (a Program change where a trigger has no off makes sense - the executor's
  call, recorded). Min / Max where a value is continuous (the Sandbox's already).
- The playground's MIDI section lists one block per output, named by what it is (Ring 1, X axis,
  Bite), each with its own Type / Channel / Number, and a "same channel for all" control on top.
- RX, on by default on every element that sends: a host message matching an output's type,
  channel and number (i) sets the element's value - the next touch continues from it, a relative
  fader from it, a toggle takes its state - and (ii) moves its lights to show it. Plus (iii) DAW
  colours: a received value on a colour controller recolours the element (the convention chosen
  by the executor - a hue wheel or a palette index - recorded). The touch element takes RX through
  `self.midirx_cb` assigned in the Setup (BOTOR's route; `gmrr` does not reach a touch element).
- Budget: the system slots carry the shared MIDI send / receive code wherever a card needs the
  room (`library.ts` may change for this change, and every card's system records move with it -
  stated in the gate); every card gets the full set.
- Built in two serial parts on Opus: 17A the shared pieces (the send and receive helpers, the
  knob model and stamp for per-output MIDI, the tuning panel's MIDI block) and the whole Sandbox;
  17B the twenty-seven playground cards, one commit per card.

**Done - change 17A, 2026-09-23 (the shared pieces, the whole Sandbox, ARC; nothing asked, every
decision recorded).** Eight commits before the docs, no push, no device, no deploy: `6b3fe11`
feat(sandbox) the runtime; `d5e51fe` feat(sandbox) the inspector; `bd3dad3` feat(tune) the
playground's MIDI outputs and ARC's LFO; `3eec7fa` fix(sandbox) the Color input's switch an entry
of its own, an integer ladder a run in any order; `576cccf` test the two e2e walks and
`QUICK_TESTS` 1036; `d1b2a1e` fix three comments Tailwind read as `row-1`; `b40ce7a`
docs(audition) row 40; `bc3f400` fix(census) the census reads an output knob by its role; then
this paragraph with `docs/MIDI.md` (new: 17B's manual), `docs/TESTING.md`'s "2026-09-23 change
17A", the dated sections in `docs/entries/sandbox-runtime.md` and `docs/entries/arc.md`, ARC's rows
in `docs/TUNING-REVIEW.md`, and the gate records `gate/change-17a.*` (before, at `db5a3c5`, the main
tree clean) and `gate/change-17a-after.*` (at `b40ce7a`).

**The firmware, read.** Voice MIDI reaches Lua by default (`init.lua`'s `grxm(MIDIVOICE,
FORWARD|HANDLE_EXTERNAL)`; no `grxm` of ours needed, REALTIME stays off); `decode.lua`'s
`pass_midi` calls every element's `midirx_cb(self,{instr,sx,sy},{ch,cmd,p1,p2})` - instr 13 the
host, 14 a neighbour's sends passing through; `gmrr` does not reach a touch element, so the
callback is assigned by hand (BOTOR's route); and nothing clears `midirx_cb` between landings, so
every callback guards itself on the touch callback it was made beside and is inert after another
landing. Whether host MIDI reaches the TOUCH element's callback on a module at all is unbenched -
row 10's open question, and audition row 40's.

**The send.** Types: CC 176, Pitch bend 224, Channel pressure 208 on a continuous output; Note 144
and CC on a trigger; Program change 192 only on a trigger with no off (`once`) - no Sandbox button
has none, so the Sandbox offers Note and CC. Wire per type: CC `number, value`; Pitch bend `0,
value` (7-bit: 64 is exactly the centre, 8192 - chosen over `v*129` so a spring fader or an LFO
rests on no bend; 127 is 16256); Channel pressure `value, 0`; Note `number, velocity`, off `128,
number, 0`. In the Sandbox the helper is `D`, by the row's channel word: `function
D(s,r,n,c,v,h)v=r[12]+(r[13]-r[12])*v//127 if v~=r[n]then r[n]=v h=h or r[8]local t=h//16%4 if s
then s:gms(h%16,176+t*16,t==2 and v or c,t==2 and 0 or v)end end end` (174 characters, was 100;
`s` nil stores without sending - the receive's no-echo). On a card the send is inline:
`local o,t=<value>,@TYPE s.l=o s:gms(@CH,t,t==208 and o or t>223 and 0 or @CC,t==208 and 0 or o)`.

**The row encoding.** The eighth column is the CHANNEL WORD: wire channel + 16 x the type's code
(CC 0, Channel pressure 2, Pitch bend 3, Note -2) + 128 when the element does not receive. A
receiving controller is the bare channel, byte-identical to before; the dearest word is 175 (one
digit more). The XY pad's Y word is column 15 when it differs; a pitch bend's controller column is
0. The button's note left the flag word (a note button with nothing else loses its forced
`,0,127,2` tail, -8, for one character). Measured against a flag bit (+10 on an otherwise-default
row) and a column of its own (+2 to +4 on every row).

**The receive** (`Y`, 539 characters; assigned `self.midirx_cb=Y` at the end of the touch Timer,
every run, `=nil` when nothing receives): host traffic only, and only while this surface's `O` is
the element's touch callback; a note-off folds into a note-on at 0; the message becomes a word
(`channel + status - 176`) matched whole against each receiving row's word (both axes of a pad),
and for a CC or a note the number too; the value (a pressure's first byte, anything else's second)
maps back through min..max. Per kind: a fader's held fine position and bar (a relative fader
continues from it, a spring fader shows it until its next release); a button's on-flag and light
(on above 0 and not its min); a one-touch XY pad's axis and the crosshair at the held pair; an
absolute knob's position and arc. A relative knob and a multitouch pad do not receive (no kept
position; transient slots) and show no Receive row. Nothing is echoed: the received value becomes
the last value sent. On ARC the received value is the LFO's CENTRE - the offset fader moves to
`s.u=-((w-127)*512//127)`, exact at depth 0 at seven measured values - and the callback, made by the
Timer once per install (ARC's Setup is 806 of 908) keyed on `s.k=s.touch_cb`, ignores its own last
value `s.l` (the echo guard for a card that sends at 50 Hz).

**The colour input** (a surface setting, off by default, in the no-selection panel: Receive colors,
Channel, First CC): a CC on its channel numbered `first + n - 1` recolours element n (the surface's
order, a blank included) on layers 1 and 2 - 0 its own colour back, 1..126 a hue wheel in six
sectors (1 red, 22 yellow, 43 green, 64 cyan, 85 blue, 106 magenta), 127 white, dimmed by the
surface's brightness. `Z` 337 characters (392 dimmed) plus 41 in `Y`. A palette index was measured
out: 128 RGB triples are about ten times the wheel's text.

**The playground's model (17B's contract, `docs/MIDI.md` section 4).** An entry declares
`outputs: [{id, name, kind: "continuous" | "trigger", once?, tokens: {type, channel, number?,
receive?}}]`, each token naming an ordinary knob built from `tune/midi.ts`'s ladders: Type the
status bytes (`CONTINUOUS_STATUSES` 176 224 208, `TRIGGER_STATUSES` 144 176, `ONCE_STATUSES` + 192),
Channel `CHANNEL_VALUES` 0..15, Number `numberValues(kept)` - 0..127 with the card's old rungs first
in their old order so a saved copy keeps its value - and Receive `RECEIVE_VALUES` 0 / 13 (the header
the callback answers, so Off and On cost the same Lua), On by default. The stamp needs nothing new:
each knob is an index; a 128-rung Number is a wide knob, two base-32 characters, so a grown card's
older links land unreadable (the known pattern). New knobs are appended, so saved copies' older
indices hold. The panel draws Same channel for all (a select: Per output, or the shared channel,
writing every output's channel), then a block per output under its name - Type (a select at three
words, a segmented pair at two), Channel, Number (gone under Pitch bend or Channel pressure), Receive
- then any unassigned MIDI knob. `outputProblems` (catalog.spec) refuses a bad declaration.

**The library and the system slots.** `library.ts` untouched: a shared matcher and sender (about 90
+ 150 characters) fit neither library half (255/0 had 66 free, 255/6 35) and 255/4 is a card's
page-next; inline per card costs ~45 more on the send and ~160 on the receive. No catalog card's
system record moved; the Sandbox's own trimmed 255/0 moved (460 -> 363: its head now carries the
contact tables `S={}F={}`, `E` no longer landed - the release is the expiry, `E=R`).

**Budgets, before -> after.** Sandbox page 3, every element receiving: 255/6 847 -> 893, 255/0 908
-> 908, 255/4 834 -> 852, Timer 783 -> 906, Setup 489 -> 900 (the Setup is the fifth bin under five
slots, first fit decreasing then an exact search; 73 characters left, room for ONE more dearest
fader); every Receive off 893 / 908 / 907 / 716 / 497. The dearest sixteen 892 -> 892 at five slots;
the CAP FLOOR 11 -> 11 dearest faders from an empty surface, 15 -> 14 from twelve. Every kind
combination fits five slots receiving; beside a multitouch pad the same three are over as at change
11; with the colour input on, the three combinations carrying a fader, a button, a pad and a knob
are over. ARC Setup 803 / 806 unmoved, Timer 410 / 437 -> 714 / 743; the lua-entries sweep 1,804 ->
2,001 combinations, worst 906 of 908 unmoved; the sweep file **4 / 19**.

**Screenshots** (the scratchpad; vite preview on 4174, stopped by port through PowerShell; 5173
untouched): `shots/sandbox/`, `shots/sandbox2/` and `shots/arc/` at 1440 x 900, 1280 x 720 and
393 x 852, each with a `-tall` cut. Fixed on seeing them: ARC's Type as a segmented row stacked
1 + 1 + 1 (130 tall) - a Type of three words is a select; an XY pad's shared Min / Max / Receive
under the Y axis read as the Y axis's - moved above the axis blocks. Left: "Channel pressure" sits
against the select's arrow at 173px; ARC's channels read 0..15 (the card's numbering, X-08) where
the Sandbox's read 1..16.

**Counts, carried + delta:** quick 96 / 1028 + 1 todo -> **96 / 1036 + 1 todo** (+8: runtime 17 to
20, sandbox-ui 29, tune-ui 15, lua-smoke two), green twice at `--maxWorkers=2`; check 677 -> **678**;
lint clean; e2e 100 / 115 -> **102 / 117**; audition rows 39 -> **40**; utilities **44 -> 44**;
catalog **27**; testids 341 -> **345**; copy exports +18 -2; OG and the four fixtures unmoved; `src/`
31 modified, 1 added (`tune/midi.ts`). **The gate:** the wire set `654e202e…` -> `6acf32ae…`, the
full `1c4acf19…` -> `c43fa145…`, the sandbox set `3bdb5974…` -> `05388336…` - moved by design (ARC's
Timer records and its new knobs; `S/page3`; +6 Sandbox fixtures); SCOPED CSS `b2a15f74…` ->
`b8281684…` by name (the two `.subhead` rules, `.rows > .subhead:first-child`); the census, the copy
exports, the testids, the titles and the JS moved as `docs/TESTING.md` states; the script exits 1 at
the wire by design. **Chunks** by files on the final build: c3 **27**, c4 **24**, c5 **11** passed.

**Departures from the brief:** the colour input is OFF by default (a controller always listening
on a channel a DAW uses for its own feedback would recolour unasked); a relative knob has no Type
and no Receive, a multitouch pad no Receive; the Sandbox's receive assignment is in the touch TIMER,
every run, not the Setup (the Setup had no room at page 3's cost), and ARC's likewise from its Timer
once per install; ARC's received value is the centre, not the LFO's output; the Sandbox's cost
representative is now a channel pressure with Receive off (the dearest word); `scripts/knob-census.mjs`
fixed (not in the brief); STATE / ROADMAP / REQUIREMENTS untouched; CAT-04 stays `[ ]`.

**What I would have asked:** (1) the colour input off by default - right? (2) Same channel for all
and the card channels read 0..15 while the Sandbox reads 1..16: renumber the cards' channels 1..16
in 17B? (3) Pitch bend as 7-bit with 64 the exact centre, or full 14-bit range with the centre off
by 64? (4) the preset (compiler) cards assign no callback, so a previous landing's stays live -
17B appending `self.midirx_cb=nil` moves every preset card's wire: accept? (5) page 3 now has room
for one more dearest fader: acceptable, or should Receive default off on a surface near the budget?
(6) ARC echo: a DAW that echoes controller feedback while the LFO runs - is the `s.l` guard enough,
or should RX on a free-running output default Off? (7) the hue wheel rather than a palette - right?
(8) a button's received "on" is above 0 and not its min - right for a button whose min is not 0?

17A's questions, decided 2026-09-23 on the recorded reading (the user away): the cards' channels
read 1..16 as the Sandbox's do (the wire keeps 0..15; a display change); pitch bend stays 7-bit
(every value is 7-bit); every card assigns its own receive callback, and a card with nothing to
receive assigns `self.midirx_cb=nil`, so a previous landing's callback never survives a Store;
the `s.l` echo guard stands; the colour input stays off by default. The playground's remaining
cards are built in two serial parts: 17B the nineteen hand-authored Lua cards (CHORUS, CONSOLE,
CULL, GHOST, LUMEN, MORPH, ORBIT, POMODORO, QUADRANT, RADAR POINTS, RADAR, SNAKE, SONAR, STAGE,
STEPS, STRIP, TRACKPAD COMET, TRACKPAD, WHEELS), one commit per card; 17C the seven ported
presets on the shelf's cards (AURORA, PINWHEEL, STARFIELD, JOYSTICK, NINE PADS, FOUR FADERS,
DIAL), compiled from the untouchable vendor tree - wrapped on HANGAR's side or rebuilt as Lua
cards the way RADAR was (12b), the executor's measured call. Keystroke (HID) sends are not MIDI
and stay as they are.

**Done - change 17B, 2026-09-23 (the nineteen hand-authored cards; nothing asked, every decision recorded).**
One commit per card, the shared pieces the cards needed, no push, no device, no deploy: `f4b2005` feat(tune) a card's
channel reads 1..16 (the wire keeps 0..15; ARC's display moves with it, its entry does not); `b5159f8` CHORUS;
`33fc889` feat(sim) the touch element's `tim` - the pull-in; `a209365` CONSOLE; `6660a98` CULL; `ac0286c` GHOST;
`680bf0d` LUMEN; `a81d423` MORPH; `6515047` ORBIT; `8d5cdbe` POMODORO; `25c4258` QUADRANT; `4b33e46` RADAR POINTS;
`0c4caa1` RADAR; `e595a12` SNAKE; `5ba6c30` SONAR; `f43f2d5` STAGE; `c25d2bc` STEPS; `525bfb3` RADAR POINTS's receive;
`a87869d` STRIP; `aaa33bf` TRACKPAD COMET; `5e5f244` TRACKPAD; `a5e222c` WHEELS; `29488c7` test(sweep, gate) the output
knobs walked, not cross-producted; `f49236c` test(e2e) SNAKE's widget walk; then this paragraph with
`docs/TESTING.md`'s "2026-09-23 change 17B", `docs/MIDI.md` sections 6 and 7, the nineteen tables of
`docs/TUNING-REVIEW.md`, each card's dated section in `docs/entries/` (TRACKPAD COMET's file new), audition rows 41-44
and the gate records `gate/change-17b.*` (before, at `5da1fed`, the tree clean) and `gate/change-17b-after.*`.

| Card | Outputs (kind) | Types offered | Receive, decided | Setup / Timer (corner) | Latch |
| --- | --- | --- | --- | --- | --- |
| CHORUS | Chord (trigger; no Number - the chord's) | Note, CC | none, `nil` (a note names no one pad) | 779 / 602 -> 798 / 602 | fault fixed |
| CONSOLE | Faders (continuous, one bank; first CC, PB / pressure per fader from the first channel) | CC, PB, pressure | the fader's level and bar | 807 / 0 -> 890 / 240 (pull-in) | fault fixed (bodies); cap swipe by design |
| CULL | none (keystrokes) | - | `nil` | 565 / 0 -> 584 / 0 | already latched |
| GHOST | X axis, Y axis (continuous) | CC, PB, pressure | none, `nil` (a recording's replay) | 730 / 484 -> 749 / 560 | already latched |
| LUMEN | Hue, Depth (continuous; sysex unchanged) | CC, PB, pressure | the held pair moves the cursor | 733 / 0 -> 882 / 461 (pull-in) | one control |
| MORPH | Top left, Top right, Bottom left, Bottom right (continuous) | CC, PB, pressure | the corner's value and block, held to the next touch | 860 / 0 -> 786 / 535 (pull-in) | already latched |
| ORBIT | Ring 1..4 (trigger; Number the ring's note) | Note, CC | a note arms the ring's step at the playhead, never clears | 853 / 411 -> 869 / 777 | swipe by design |
| POMODORO | Transport, Alarm (trigger) | Note, CC | none, `nil` (announcements; an echo would undo a tap) | 743 / 659 -> 766 / 669 | already latched |
| QUADRANT | Top left .. Bottom right (trigger) | Note, CC | the pad lit on, dark off | 838 / 0 -> 537 / 726 (pull-in) | already latched |
| RADAR POINTS | Points (trigger; no Number) | Note, CC | a note arms one point on the ring just crossed | 892 / 380 -> 897 / 758 | swipe by design |
| RADAR | X axis, Y axis (continuous) | CC, PB, pressure | the held pair draws the comet | 763 / 50 -> 856 / 412 (pull-in) | already latched |
| SNAKE | Bite, Death (trigger) | Note, CC | none, `nil` (game events) | 877 / 739 -> 896 / 768 | one control (the steer) |
| SONAR | Sequence (trigger; no Number) | Note, CC | a note arms one cell of its ring on the sweep line | 572 / 289 -> 591 / 668 | swipe by design |
| STAGE | none (keystrokes) | - | `nil` | 653 / 136 -> 672 / 136 | already latched |
| STEPS | Track 1..8 (trigger) | Note, CC | a note arms the track's step at the playhead | 727 / 361 -> 746 / 826 | swipe by design |
| STRIP | Fader, Crossfader (continuous) | CC, PB, pressure | the value and its light | 875 / 0 -> 849 / 476 (pull-in) | already latched |
| TRACKPAD COMET | none (mouse) | - | `nil`, from the Timer | 903 / 427 -> 903 / 443 | one control |
| TRACKPAD | none (mouse) | - | `nil`, from the Timer | 903 / 510 -> 903 / 526 | one control |
| WHEELS | Pitch wheel (14-bit under PB), Mod wheel (continuous) | CC, PB, pressure | pitch shown and held (no spring), mod bar | 895 / 343 -> 900 / 895 (pull-in) | already latched |

**The records.** Every one of the nineteen cards' `E/<id>/` records moved - the Lua text did (the type's expressions,
the tables, the receive or the `nil`) - while each card's defaults send exactly what they sent before, message for
message, and each card's `lua-smoke.spec.ts` case holds it. frames.json and the OG images did not move: no rest frame
moved (a still card's pull-in arms nothing; an armed one-shot Timer, tried on CONSOLE first, moved frames.json's tick-0
flag and would have turned the listing's `static` into `animated` - refused). No system slot and no library helper:
`library.ts` untouched (LUMEN inlined `A`, which now has no caller - recorded for the next library change); the room
came from each card's Timer, made once per install (ARC's route) or pulled in by `self:tim()`, which the Sandbox's
first probe lit and the host now models (`lua-host.ts` SELF_PRELUDE `tim`, HOST_SELF_METHODS + `tim`;
`host-surface.spec.ts`'s refusal of an entry's `self:tim()` retired). The stamp: CONSOLE, LUMEN, POMODORO (it landed
`older`), QUADRANT, SNAKE, SONAR and STRIP join the grown racks whose captured links land `unreadable`; STEPS's captured
default vector is no longer the defaults (the drum channel moved from index 2 of four to 9 of sixteen); CULL's and
STAGE's still land `restored`. ORBIT's library fixture re-exported against its 25-knob rack. Screenshots: ORBIT,
MORPH and STEPS at 1440 / 1280 / 393 (`shots/b17/` in the scratchpad) - nothing ragged to fix; STEPS's MIDI section is
eight blocks tall, and a note-kind Number reads as a note name under CC, both left.

**Counts, carried + delta:** quick 96 / 1036 + 1 todo -> **96 / 1055 + 1 todo** for 17B (+19, one case a card);
the tree at `94a219b` reads **96 / 1062 + 1 todo, green** at `--maxWorkers=2` (with change 18's +4 and 18b's +3; a
first run met 18b's in-flight files, 8 red there, every 17B file green); `QUICK_TESTS` 1059 -> **1062**. Check 678 ->
**678**; lint clean; e2e 102 / 117 -> **103 / 118** (titles / runs; 17B +0 - one title renamed - change 18 +1); audition rows 40 -> **44**; utilities **44**;
catalog **27**; testids **345**; SCOPED CSS `b8281684…` equal; copy exports unmoved by 17B. The sweep **4 / 19** (the
lua-entries sweep 2,144 -> 6,549 combinations, all inside 908). **The gate**: the wire set `6acf32ae…` -> `9d5de31f…`,
moved by the nineteen cards' `E/<id>/` records and change 18's `S/page3` alone (ARC, the presets, the library and
the defaults unmoved); the sandbox set by change 18 alone. **Chunks** by files: c3 **27**, c1 **33**, c4 **25**, c2 **21**
+ one webkit wasm-load console error green alone, c5 **10** + the artifacts title on a build one commit behind HEAD.

**Departures from the brief:** (1) the channel display change is its own commit (`f4b2005`) before the cards, and moves
ARC's panel with every card's; (2) the pull-in - a host change (`33fc889`) and two retired gate assertions - where the
brief ordered Setup, Timer, system slots: a still card's armed Timer moves the listing, the system slots are shared;
(3) RADAR POINTS took two commits (no receive, then SONAR's one-cell rule so the four sequencers agree); (4) three test
fixes outside the card commits - the sweep's Pass A and the gate's hash-wire past reach (`29488c7`), SNAKE's e2e walk
found by c3 (`f49236c`); (5) the knob floor of three counts every knob (LUMEN keeps two outside its outputs);
`brightness.spec.ts` test 3's timeout 60 s; (6) WHEELS' pitch wheel keeps fourteen bits under a pitch bend where
`docs/MIDI.md`'s pitch bend is 7-bit; (7) four-rung channel ladders became the sixteen in order - a saved copy at
index 2 or 3 reopens on channel 3 or 4 (Same channel for all needs the sixteen in order); (8) CONSOLE's cap row keeps
its swipe (11-07's ask) while its bodies latch; CHORUS's slide no longer re-chords (the latch rule over the entry's
old "a slide is legato"); (9) the gate's after-record ran with change 18b's uncommitted Sandbox edits in the tree
(another executor's), so its quick term and sandbox set carry them. CAT-04 stays `[ ]`.

**What I would have asked:** (1) CHORUS: a slide between chord pads re-chorded ("legato by construction"); the latch
rule stopped it - right, or is a chord glide wanted? (2) Receiving nothing on CHORUS, GHOST, POMODORO and SNAKE -
right, or should a received note light a pad, move GHOST's comet, start or pause POMODORO? (3) The sequencers' receive
arms the step at the playhead (the step just played); should a note played just before a step arm the next one
instead (audition rows 42-44 ask the bench)? (4) CONSOLE's nine faders are one output (a first CC, the Mackie layout
under pitch bend) while MORPH, QUADRANT and STEPS got one output per element - is a bank right for a mixer? (5) STEPS's
eight blocks make a long MIDI section - collapse the blocks, or keep them flat? (6) A note-kind Number reads as a note
name under CC - word it by the Type? (7) WHEELS' pitch wheel: fourteen bits under a pitch bend - right beside the 7-bit
rule? (8) The library's `A` has no caller now - drop it at the next library change (every card's system records
move)?

**Done - change 17C, 2026-09-23 (the seven ported presets and the two panel fixes; nothing asked, every decision
recorded).** One commit per card, the panel fixes their own, no push, no device, no deploy: `cfccb51` feat(tune) the
wrap (`src/lib/catalog/entries/ported-midi.ts`), the stamp, the model, the gate and the sweeps, AURORA the first;
`3499868` PINWHEEL; `3a29228` STARFIELD; `e51b5f0` JOYSTICK; `a8feff5` DIAL (and the ladder restored on a wrapped
card); `cac5061` FOUR FADERS rebuilt by hand; `4ef97cb` NINE PADS rebuilt by hand; `9b08a4a` feat(tune) the Number by
its Type and the folding blocks; `3d3d9da` test(ui) the sixth mono use; `6f35816` fix(tune) the summary spans the
reset column; `ebfbfac` chore(gate) `QUICK_FILES` 97, `QUICK_TESTS` 1072; then this paragraph with `docs/MIDI.md`
section 8, `docs/TESTING.md`'s "2026-09-23 change 17C", the seven tables of `docs/TUNING-REVIEW.md`,
`docs/entries/faders.md` and `docs/entries/ninepads.md` (new), audition rows 45-47 and the cost rows, and the gate
records `gate/change-17c.*` (before, at `e5a4578`) and `gate/change-17c-after.*` (at `ebfbfac`).

| Card | Route | Outputs (kind) | Types | Receive, decided | Latch | Setup / Timer (corner), before -> after | Ring |
| --- | --- | --- | --- | --- | --- | --- | --- |
| AURORA | (a) wrapped | X axis, Y axis (continuous) | CC, PB, pressure | the library comet drawn at the held pair | already (the first finger's claim) | 375 / 55 -> 784 / 55 | stays |
| PINWHEEL | (a) wrapped | X axis, Y axis (continuous) | CC, PB, pressure | the comet at the held pair in the first finger's colour | already | 422 / 55 -> 841 / 55 | stays |
| STARFIELD | (a) wrapped | X axis, Y axis (continuous) | CC, PB, pressure | the comet at the held pair | already | 368 / 55 -> 777 / 55 | stays |
| JOYSTICK | (a) wrapped | X axis (PB), Y axis (CC 17) (continuous); Bend and Send superseded | CC, PB, pressure | the parked dot moved to the held pair's cell; a rest follows its Type | already | 501 / 24 -> 622 / 445 (the receive in the Timer, pulled in) | stays |
| DIAL | (a) wrapped | Dial (continuous); Send and Channel superseded | CC, PB, pressure | absolute: the level the next turn continues from; relative: `nil` | already | 652 / 55 -> 904 / 55 | stays |
| FOUR FADERS | (b) a Lua card | Fader 1..4 (continuous) | CC, PB, pressure | the fader's bar | FIXED: a contact keeps its fader | 528 / 24 -> 765 / 300 (pulled in) | leaves |
| NINE PADS | (b) a Lua card | Pads (trigger, a bank from a base note) | Note, CC | a pad's note lights it, its off darkens it | FIXED: a contact keeps its pad | 651 / 163 -> 752 / 764 (pulled in) | leaves |

**Why each route.** (a) where the only change is what the sends are, plus a receive: the compiled sends are exact
literals once the shelf's Send and Channel are superseded (16 / 17 on channel 0 on every state), so the rewrite names
each whole call, counts it and throws on a miss (`ported-midi.spec.ts` walks every compiler knob state); the card stays
`padsim` and on the ring. (b) where the fix IS the latch: the vendored PadSim runs the `PadState`'s control-under-the-
finger rule, so a wrapped preview would still slide (FOUR FADERS - the fault the user saw "in playground") or
re-trigger (NINE PADS) while the module latched. **What the browser shows for a wrapped card**: the compiled preset's
picture under every gesture - the module's, since the rewrite touches no LED call; no MIDI monitor (a compiler preview
keeps no log, so nothing on the page claims a message); no receive (no MIDI reaches any preview). Honest, because the
five were already latched: no gesture makes the preview and the module disagree. **The ring after**: five - Aurora,
Pinwheel, Starfield, Joystick, Dial (FOUR FADERS held position 6, NINE PADS 3); the hero is still AURORA.

**The records.** The defaults send what the presets sent: the five wrapped cards exactly, message for message, and the
same frames (`lua-smoke.spec.ts` runs each beside the shelf preset in one VM); FOUR FADERS and NINE PADS exactly under
every gesture that stays on a control, and differently on a slide (the latch) - and NINE PADS on a fast tap, which now
plays (the shelf sent nothing: a recorded BOTOR finding a hand-authored card can fix). `frames.json` and the OG images
did not move (no rest frame moved; NINE PADS' watchdog Timer is armed only while a pad is held). The wire moved by the
seven cards' records alone: `P/aurora`, `P/pinwheel`, `P/starfield`, `P/joystick`, `P/dial` moved and grew (the output
knobs walked), `P/faders` and `P/ninepads` out, `E/faders` and `E/ninepads` in. **Stamps**: AURORA's, PINWHEEL's and
STARFIELD's older links all still land (the vendored stamp is written while the outputs are at their defaults, HANGAR's
format `w` once one moves); a JOYSTICK or DIAL link with its Send, Channel or Bend off the shelf's lands `unreadable`;
FOUR FADERS' and NINE PADS' older links and `#z.pfaders` / `#z.pninepads` land `unreadable` (RADAR's pattern).
`library.ts` untouched; no system slot; the room came from the Setup, then the Timer pulled in (JOYSTICK, FOUR FADERS,
NINE PADS). The tightest is DIAL's Setup, 904 of 908 absolute and receiving at its dearest literals.

**The two panel fixes.** (1) An output's Number is worded by its output's Type: a note name under Note, the number
under CC or Program change, hidden under Pitch bend and Channel pressure (ORBIT's and STEPS' notes under CC read 36, not
C2). (2) Each output block's head is a full-width button on the rack's grid - the name in the label column, the summary
(`Note · Ch 10 · C2 · Receive`) in the house mono from the control column across the reset column, a chevron of straight
lines in the last column, 44 px, no radius - that opens and closes the block (click, Enter, Space; `aria-expanded`);
every block folded on arrival past four outputs (STEPS), open at four or fewer (ORBIT, FOUR FADERS); Same channel for
all unchanged above. **Screenshots** (`shots/c17/` in the scratchpad: STEPS, ORBIT, FOUR FADERS and NINE PADS at 1440,
1280 and 393, each with a `-midi` cut; wrangler dev on 4174, stopped by port through PowerShell, 5173 untouched): seen
- the eight STEPS heads folded, the four ORBIT and FADERS blocks open, NINE PADS' Number reading C2, the stacked head at
393; fixed on seeing them - at 1440 the summary was cut to `Rec…` in the control column alone, so it spans the reset
column (`6f35816`).

**Counts, carried + delta** (carried = the tree at my start, `e5a4578`): quick 96 / 1062 + 1 todo -> **97 / 1072 + 1
todo** (+1 file, +10), green twice at `--maxWorkers=2` and in the gate; `QUICK_TESTS` 1062 -> **1072**, `QUICK_FILES`
96 -> **97**. Check 678 -> **682**; lint clean; e2e 103 / 118 -> **104 / 119** (titles / runs); audition rows 44 ->
**47**; utilities **44 -> 44**; catalog **27** (five presets, twenty-two Lua); testids 345 -> **347** (`midi-fold`,
`midi-summary`); copy exports +5 (`outputSummary` and its four words); SCOPED CSS `b8281684…` -> `d514ae96…` by name
(`.subhead` out; `.output`, `.fold`, `.fold-name`, `.fold-summary`, `.chevron`, `.fold-rows`, `.fold-rows[hidden]`,
`.fold-rows > *` and the head's container rule in); the sweep **4 / 19**; `src/` 28 modified, 4 added. **The gate**: the
wire set `9d5de31f…` -> `e9534dba…`, the full `5fcce9e9…` -> `98a51842…` (`--full` completes now, in seconds, since
17B's `29488c7`), the Sandbox set `51e5da18…` equal; the script exits 1 at the wire by design. **Chunks** by files on the
gate's build: c3 **28** (a first run lost its server mid-run and passed whole alone), c4 **25**, c2 **22**, c1 **33**.

**Departures from the brief:** (1) the wrap's machinery rode in AURORA's commit, and DIAL's restored the ladder
`cfccb51` had skipped (DIAL is the specs' over-budget fixture); (2) three small commits after the panel commit - the
mono list, the summary span found on the screenshots, the gate counts; (3) JOYSTICK's Bend left the rack, superseded by
the two Types; (4) NINE PADS' fast tap now plays (a wire change at the defaults, a fix); (5) NINE PADS' latch overrules
the compiler's own "legato for free", on CHORUS's precedent and section 18's rule; (6) the NINE PADS and FOUR FADERS
colour / grid knobs - NINE PADS' grid colour is a five-colour palette where the lattice was (RADAR's divergence); (7)
`view.ts`, `brightness.ts` and `e2e/rack-grid.e2e.ts` touched outside the card files. STATE / ROADMAP / REQUIREMENTS
untouched; CAT-04 stays `[ ]`.

**What I would have asked:** (1) NINE PADS: latched against the compiler's "legato for free" - right, or a Glide option
(a slide plays each pad it crosses) beside it? (2) FOUR FADERS as four outputs rather than CONSOLE's one bank - right?
(3) JOYSTICK: the Bend knob retired for the two axis Types - right? (4) DIAL relative receives nothing (a relative dial
keeps no value) - right, or should a received value set the absolute level it would switch to? (5) AURORA, PINWHEEL and
STARFIELD draw a received pair as the finger's comet - right, or a steady dot? (6) The ring is five - should FOUR
FADERS or NINE PADS rejoin once a Lua card may sit on it (the 271 KB VM on the front page's first paint)? (7) NINE PADS'
grid colour is a palette of five where the lattice was - widen it back to the picker? (8) A folded block's summary
reads `Receive off` when Off - the word you want?

18. Latch - an element keeps the finger that landed on it (2026-09-23)
--------------------------------------------------------------------------------

> in sandbox mode add "Latch mode" for each element that means that even if your finger leaves
> the element's area it keeps controlling it. Dont know how to correctly articulate it. Imagine
> that you are sliding a fader and you touch another fader by accident then that second fader
> should not react to the touch but keep controlling the first fader do you understand?

Asked: which case - other touches ignored, a second touch driving the first, one finger slid
onto the next fader on the module, or the preview only - noting that the Sandbox already pins a
contact to the element it landed on for the whole gesture (runtime.ts `O`).

> i saw it on the faders config in playground

Recorded: the fault is on the playground - FOUR FADERS (the ported preset) picks the fader under
the finger on every sample, so a slide past one fader's edge drives the next; every playground
card with side-by-side controls is audited for the same (CONSOLE, STRIP, WHEELS, MORPH, the
TRACKPADs, the Lua cards 17B is on now, the presets 17C takes) and made to latch: a contact
keeps the control it landed on until it lifts. In the Sandbox the behaviour already latches; it
becomes a per-element option `Latch` (On by default - today's behaviour), and Off lets a finger
hand over to whatever element it moves onto - a strum across buttons, a glide from fader to
fader - so both feels are available.

**Done - change 18 (Sandbox), 2026-09-23 (the per-element Latch; no catalog entry touched; nothing asked,
every decision recorded).** Six commits before the docs, no push, no device, no deploy: `f278bd6`
feat(sandbox) the runtime; `ae7ed43` feat(sandbox) the inspector's Latch row; `aae17e4` test the e2e
Latch walk; `55cf407` test the options walk's and the rack grid's pins; `4bc2378` and `133a458`
chore(gate) `QUICK_TESTS` 1046, then 1059; then this paragraph with `docs/TESTING.md`'s "2026-09-23
change 18 (Sandbox)", the dated section "Latch" in `docs/entries/sandbox-runtime.md`, and the gate
records `gate/change-18.*` (before, at `309188b`, 17B's edits in the tree) and `gate/change-18-after.*`.

**The option.** Latch, Off / On, **On by default**, on every kind that takes touch (a blank has none);
the schema's `latchTouch` (absent is On, so an older draft reads On; not the button's `latch`, which is
its Toggle). **At On every emitted string is byte-identical to before**: every earlier fixture, the
field absent or `true`, under two, three and five slots, and the gate's sandbox set moved only by its
nine new fixtures (589 strings equal, 171 added).

**The hand-over rule.** On every live sample that is not an onset, the finger's cell is compared with
the region its contact holds. When they differ and the contact is free to pass - it holds a Latch Off
region, or it is already waiting - the old region is released through `E` exactly as a lift releases
it (a momentary button's off, a fader's bar kept, a spring's return with its value, an XY pad's
crosshair and a knob's arc cleared, a multitouch pad's slot freed), the contact waits (`S[i]=false`:
no region), and if the new cell is an element that no other contact holds, the sample is an onset
there: a button presses, an absolute fader or pad jumps to the finger, a relative one anchors, a
multitouch pad gives the lowest free slot. A waiting finger crosses empty plate and blanks holding
nothing and takes the next free element it reaches. A finger that landed on empty plate or a blank,
or on an element that is On, is never free: On keeps its finger wherever it goes; a hand-over INTO an
On element is allowed, and that element then keeps it. **The steal decision: no** - a sliding finger
never takes a region another contact holds (a multitouch pad with any finger on it included); it waits
on nothing and takes the region on its first sample after the holder lifts (a finger held perfectly
still sends no sample, so: when it next moves). A finger that LANDS on a held region still takes it.

**The encoding and its cost.** A bit in the CHANNEL WORD: a Latch Off region's word is 512 higher -
every Off word 480 and up, every On word under 192, so the entry reads `J[g][8]>479`; every reader that
runs at On takes the word apart by `%16`, `%128` or `//16%4`, which 512 leaves alone; the receive
callback `Y` reads it whole, so its rows are a variant taking `%512` first (+6). A word gains at most
two digits (0 -> 512), a Receive-off word none (175 -> 687). Measured against a keyed field `,h=1`
(four a row) and a flag-word bit (the tail forced where omitted, plus variants of `R` and the knob), every
element Off, rows + readers + the entry's read: page 3 22 / 39 / **21**, page 3 with every option
22 / 18 / **19**, eight 38 / 76 / **25**, sixteen 70 / 140 / **33** - the channel bit is the cheapest on
every surface measured. **The variant**: `O` 273 -> **399** (+126), the multitouch `O` 392 -> **518**
(+126), `Y`'s rows +6 - swapped in only when an element is Off (change 11's pattern); every other part
the same text.

**Five slots at the picker corner** (255/6, 255/0, 255/4, Timer, Setup), On -> every element Off: four
faders 830/903/558/111/482 -> 907/791/725/111/486; eight 852/907/832/231/614 -> 858/907/824/365/622;
twelve 852/907/832/231/758 -> 858/907/824/365/770; sixteen 852/907/832/231/892 -> 858/907/824/365/908 - all fit. **Page 3,
every element receiving (893/908/852/906/900, 81 free), does not fit with ANY element Off** (the
hand-over wants 126 + 6 and a word's digits; all four Off: the Timer 1,076); with every Receive off it
fits (852/908/837/842/617). The ceiling in kinds, every element Off and receiving: `vbxk`, `hbxk`,
`vhbxk` over (the three change 11 put over beside a multitouch pad); beside a multitouch pad `vhxk` too.
**The cap floor 11 -> 11** dearest faders from an empty surface (the representative now carries Latch
Off: its word 175 -> 687, three digits, the entry once), **14 -> 14** from twelve.

**The VM cases** (`runtime.spec.ts` 21): On pinned (a slide from Lane A up and across into Lane B:
Lane A 25, 50, 76, 101; Lane B nothing); Off, the same slide hands over (Lane A stops at 50, its bar
kept; Lane B 76, 101; back into Lane A, 76); a strum across four Off buttons with an empty cell before
the last (90 on, 90 off, 91 on, 91 off, 92 on, 92 off, 93 on; 93 off on the lift) and the same row On
(90 on, 90 off alone); an Off fader's finger onto empty plate released (bar kept, nothing sent while it
crosses) and then pressing the next Off button; an Off spring fader left for empty plate (101, then 64);
a finger landed on empty plate crossing a button and a fader (nothing); a slide from an Off button onto
an On button another finger holds (its off; no second press; waits; takes it after the lift; the On
button then keeps it past an Off button that hears nothing); a slide onto an Off multitouch pad another
finger holds (waits; alone takes the first slot; slid back out its cross goes and the button presses);
page 3 all Off receiving as page 3 does. `runtime.spec.ts` 22 measures, `emit.spec.ts` 10 the word and
the encodings, `sandbox-ui.spec.ts` 30 the editor and the row, the e2e walk the preview (one mouse finger
slid from Button 1 onto Button 2 in Play: the monitor shows Button 2 at 127; with Latch left On it does not).

**The inspector.** The Latch row is the last row of Behavior on every kind that takes touch, a segmented
Off / On; its helper is the label's title: "On: the finger keeps this element until it lifts. Off: a
sliding finger passes to the element it moves onto." Multi-edit writes every member as one entry, a
blank refuses it, Play refuses it; Latch is remembered per kind (13B). A set of mixed kinds with no blank
now shows Behavior with Latch alone (before: no Behavior over mixed kinds).

**Counts, carried + delta** (carried = the tree at my start, `309188b` with 17B's uncommitted edits:
quick 96 / 1037 + 1 todo, check 678, e2e 117 runs, utilities 44, testids 345): quick -> **96 / 1059 + 1
todo** (mine +4: runtime 21, 22, emit 10, sandbox-ui 30; 17B's +18 since my start), green at
`--maxWorkers=2` at `a81d423` (1,046) and in the gate's quick term (1,059, 0 failed); two runs earlier
mid-17B were red only in 17B's five in-flight files. `QUICK_TESTS` read fresh (1036, untouched by 17B)
-> 1046 (my run at `a81d423`) -> **1059** (the gate's run, the whole tree at `a5e222c`). Check 678 ->
**678**; lint clean; e2e 117 -> **118** runs; utilities **44 -> 44**; testids **345 -> 345** (the census
reads `data-testid="..."` literals; `field-latch` is the segmented snippet's argument, as
`field-receive` is); copy exports moved (mine +2: `LATCH`, `LATCH_HELPER`); SCOPED CSS **`b8281684…`
equal** (the row is the existing segmented control); raw CSS equal. **The gate**: the sandbox set
`05388336…` -> `a805fdc4…` - moved only by new fixtures; the wire set moved by 17B's cards committed
meanwhile (`481eced6…` -> `8eab8e1f…`), none of it mine; the full wire term stopped after 58 CPU-minutes
(17B's grown cards put the three-position sample past reach), so the sandbox set was hashed alone and
the script exits 1 at the wire. **Chunks** by files on the build at `133a458`: c4 **25** passed (24 + the
walk), c5 **11**, `rack-grid.e2e.ts` **4**.

**Departures from the brief:** (1) the encoding: the brief named a bit in the flag word or the channel
word - the channel word it is, measured against both and a keyed field; it needs a second variant (`Y`'s
rows, +6) beside `O`'s. (2) The row appears over a set of mixed kinds (Behavior with Latch alone), so
multi-edit covers a fader-and-button set; sandbox-ui 12 and 16's "never Latch" and "no Behavior over
mixed kinds" pins moved, and `rack-grid.e2e.ts` (not in the brief's file list) moved with the row. (3)
`setLatchTouch` refuses in Play (returns false) where `setReceive` returns true and changes nothing. (4)
`QUICK_TESTS` moved twice (1046, then 1059), both figures proved by my runs on the whole tree. (5) The
gate's full wire could not finish; the sandbox set is recorded by hand beside the after-record.
**Found and not fixed** (it moves the sandbox set at the default): `A(r,x,y)` (change 17) computes both
axes, so a one-cell-wide vertical fader (or one-cell-tall horizontal fader) divides by zero on every
sample and sends nothing - the editor allows 1 x 2; emit.spec's twelve and sixteen are 1-wide and are
only measured, never run. A guard per axis is +31 characters on `A`. CAT-04 stays `[ ]`.

**What I would have asked:** (1) Page 3 with every element receiving cannot take Latch Off at all -
accept, or make room (the 1-wide fix will cost 31 more)? (2) No steal - right, or should a sliding
finger take a held region as a landing does? (3) A finger that landed on empty plate takes nothing
even when it slides onto an Off element - right, or should it pick one up? (4) A waiting finger crosses
empty plate and keeps looking; should an Off element's finger that leaves onto empty plate instead be
done for the gesture? (5) Latch Off on a knob releases it the moment the finger slips outside its box -
offer Latch on a knob at all? (6) Fix the one-cell fader's division by zero now (it moves every surface
with a fader or a pad)?

Change 18 (Sandbox) questions, decided 2026-09-23 (the user away): no steal stands; a finger that
touched down on empty plate stays inert; an Off element's finger that leaves onto empty plate
carries on to the next Off element (the strum); page 3 with every element receiving cannot take
Latch Off - accepted, the refusal is the budget's own (no number shown). Two changes follow as
change 18b, Sandbox only: (1) the knob loses the Latch row (an Off knob lets go the moment the
finger slips outside its box, which only ever reads as a fault on a rotary gesture); (2) the fault
18 found and did not fix - a one-cell-wide vertical fader (or one-cell-tall horizontal one) never
sends on the module because 17A's `A` divides by the zero-length cross axis - is fixed: the
runtime guards the cross axis, the cost stated per surface.

17B's questions, decided 2026-09-23 (the user away; each open to the user's word): the latch rule
stands on CHORUS (a pad slide no longer re-chords - the user's latch word covers it; a CHORUS
"glide" option can come back on request); CHORUS, GHOST, POMODORO and SNAKE receive nothing -
right, nothing there has a value to take; a sequencer arms the step that just played; CONSOLE
keeps one bank output; WHEELS keeps its 14-bit pitch bend; the orphaned library `A` waits for the
next library change. Two tuning-panel fixes go into 17C: (1) a Number row is worded by its
output's Type - a note name under Note, a controller number under CC, hidden under Pitch bend and
Channel pressure; (2) output blocks fold - each block a one-line summary ("Note · Ch 1 · C1 ·
Receive") that opens on click, every block folded when a card has more than four outputs (STEPS'
eight, ORBIT's four open), so a long MIDI section reads at a glance.

**Done - change 18b (Sandbox), 2026-09-23 (the one-cell fader sends; the knob loses Latch; no catalog entry touched;
nothing asked, every decision recorded).** Two commits before the docs, no push, no device, no deploy: `708f65f`
fix(sandbox) the runtime and the model; `94a219b` feat(sandbox) the inspector and the editor; then this paragraph with
`docs/TESTING.md`'s "2026-09-23 change 18b (Sandbox)", the dated section "The one-cell fader, and the knob without
Latch" in `docs/entries/sandbox-runtime.md`, and the gate records `gate/change-18b.*` (at my start, `ee50d42`) and
`gate/change-18b-after.*`.

**The guard: a new part `V`, the axis with its divisor clamped.** `A` reads each axis through
`V(d,l)=glim(d*127//glim(l-1,1,9)//64,0,127)`: floor division twice is floor division once by the product, so every
box two cells and up reads exactly the number it did (change 17's `A` and this one run side by side in the VM over
4,896 positions: 0 differ), and a one-cell axis divides by 1 where it raised `n//0` - the fader throws that axis away.
**The cost: +11** over change 17's `A` (133 -> `A` 83 + `V` 61), measured canonical against the clamp inline in `A`
(+14, one part - and page 3 with every option on then has no placement at all), `A` handed the kind so it computes only
the axis read (+31) and `len>1 and ... or 0` per axis (+33). **Per surface: +12** (the 11 and a separator) on every
surface that carries a fader or a pad, at the corner under five slots - page 3 893/908/852/906/900 -> 893/908/905/897/868
(81 -> 69 free), four faders 2,884 -> 2,896, eight 3,436 -> 3,448, twelve 3,580 -> 3,592, sixteen 3,714 -> 3,726, each
of those every element Off 12 more likewise (the sixteen's Setup still 908); every fixture that fitted still fits, the
cap floor 11 -> 11 and 14 -> 14; a surface of buttons and knobs alone does not move. **An XY pad one cell wide:** the
editor refuses it (minimum 2 x 2), so it never reaches `V`.

**Found by the new placement, fixed (Rule 1):** `V` moved page 3's placement and first fit put the entry `O` in the
touch Timer, which re-runs every period - a new `O` each run, so `Y`'s `s.touch_cb~=O` ignored every host message
(runtime.spec test 18 went red). `packRuntime` keeps the entry out of the Timer while a receive callback is packed;
no placement that worked moves, and 19 fixture landings that fit with a receive half all keep `O` elsewhere.

**The VM cases** (`runtime.spec.ts` 23): a 1 x 6 vertical fader 127, 0, 76 (a wobble across nothing) with its bar; a
1 x 2 127, 0; a 6 x 1 horizontal 0, 127, 50 with its bar; a 2 x 1 0, 127; the pad beside them - under five slots and
three; the 1 x 6 relative at full with the spring at 100: 127 then 100; two 1 x 6 Latch Off hand over (70:25, 70:50,
74:76, 74:101). RED before the fix (the 1 x 6 sent nothing). **Every emit.spec surface RUN** (`emit.spec.ts` 11): one,
page 3, eight, twelve, sixteen, four faders, page 3 with every option, page 3 at three fingers, sixteen at typed
literals, eight / twelve / sixteen every element Off and four of the cap floor's representative at 1 x 2 - 63 faders and
pads, each sends on its channel, an absolute controller exactly its min then its max, nothing raises (on change 17's
divisor: "Twelve / Fader 1 sent nothing").

**The knob.** `takesLatch(kind)`: the fader, the button, the XY pad. The inspector shows no Latch row on a knob; **over
a set the row shows only when every member carries Latch** - a set with a knob in it has no row, and a set of mixed
kinds with a knob no Behavior (Latch was its one row): the row writes every member, so it is not shown rather than
skipping the knob. `setLatchTouch` refuses a knob alone or in a set, as a blank; the knob's remembered fields lose it. A
knob stored Off (change 18's few hours) is still a valid record and reads On: its word without the bit, its strings
byte-identical to the field absent, loaded with no row (`sandbox-ui.spec.ts` 31; the options walk in `sandbox.e2e.ts`
reads no row on the knob).

**Fixtures that moved** (the sandbox set `a805fdc4…` -> `51e5da18…`, 760 -> 874 strings, 40 -> 46 fixtures): 300 strings
across the 35 fixtures that carry a fader or a pad - `A`'s new text and `V` (runtime/page3-off also by its knob's word,
the knob now On); none gone; 114 added by six new fixtures; unmoved, the five of buttons and knobs alone (latch, notes,
knobs, strum, wait). **The catalog wire did not move:** the base set's 12,127 records, 12,117 equal, the 10 moved all
`S/page3/…` (its own Sandbox page 3).

**Counts, carried + delta** (carried = the tree at my start, `ee50d42`, src as at `a5e222c`: quick 96 / 1059 + 1 todo,
check 678, e2e 118 runs, utilities 44, testids 345): quick -> **96 / 1062 + 1 todo** (mine +3: runtime 23, emit 11,
sandbox-ui 31), green at `--maxWorkers=2` twice and in the gate's quick term at `e5a4578`; `QUICK_TESTS` not edited by
me - 17B's `0d61e04` moved it 1059 -> 1062 on the whole tree at my `94a219b`, my three. Check 678 -> **678**; lint
clean; e2e 118 -> **118** runs; utilities **44 -> 44**; testids **345 -> 345**; copy exports **equal**; SCOPED CSS
`b8281684…` **equal**. **The gate** ran whole (`--after change-18b --against change-18`): the full wire finished this
time (17B's `29488c7`), `WIRE FULL 5fcce9e9…`; it exits 1 at the wire, moved since `309188b` by 17B's cards and by 18
and 18b; the sandbox set was hashed on its own at my start and after (`change-18b.wire-sandbox.json`,
`change-18b-after.wire-sandbox.json`) and every later term compared by hand. **Chunk** by files on the gate's build at
`e5a4578`: c4 **25**.

**Departures from the brief:** (1) the guard is a part of its own, `V`, not a guard inside `A` - cheapest measured and the
only form that keeps page 3 with every option on five slots. (2) The packer rule above, found by test 18. (3) Change 18's
encoding measure moved with the knob: the channel bit is still the cheapest on eight and sixteen elements (25, 33) and no
longer on page 3 (the keyed field 18 against 20) or page 3 with every option (the flag bit 14 against 19) - not
re-encoded. (4) `scripts/gate/sandbox-fixtures.mjs` +6 fixtures (outside the brief's file list, as 18's nine were). (5)
17B's gate `--after change-17b` ran with my edits uncommitted in the tree, so its sandbox set (`6abddaa3…`) and quick
figure carry them. CAT-04 stays `[ ]`.

Change 17C, 2026-09-23 - the fault this section was opened for is fixed on the playground: FOUR FADERS, rebuilt as a
hand-authored Lua card (`cac5061`, `docs/entries/faders.md`), pins a contact's fader on its onset, so a slide from
fader 1 onto fader 2 moves fader 1's output only (lua-smoke.spec.ts's VM case, beside the shelf preset sending 16, 17
and 18 for the same slide); NINE PADS' pads latch the same way (`4ef97cb`); AURORA, PINWHEEL, STARFIELD, JOYSTICK and
DIAL were already latched (the first finger claims `s.f`). The Done paragraph "17C" under section 17; audition rows
45 and 46.

19. Every colour knob a full RGB picker (2026-09-24)
--------------------------------------------------------------------------------

> review every card's color picker and fix those that have the older one where you can only
> select certain colors an not RGB sliders.
> also suggest me more features and stuff how can we make HANGAR better

Read before building: the census lists 35 colour knobs on 22 hand-authored cards whose rungs are a
fixed palette of three to five colours (ORBIT's four rings, CHORUS, ARC's two, GHOST's two, MORPH,
SONAR, STEPS, CONSOLE, STRIP, LUMEN, STAGE, SNAKE, POMODORO, WHEELS, RADAR POINTS, the TRACKPADs,
RADAR, NINE PADS); the ported presets' colour knobs are already the RGB444 lattice with the full
picker. Recorded: every palette colour knob becomes the full picker - the RGB444 lattice the
presets use, the card's old palette kept as the picker's quick-pick row and its old default kept
exactly (as an extra rung if it is off the lattice, so no rest frame moves), every cost
re-measured at the lattice's corner, older links to those cards landing at their defaults (the
stamp's known pattern for a grown knob).

**Done - change 19, 2026-09-24 (every colour knob a full RGB picker; nothing asked, every decision recorded).** One
commit per card, the machinery in ORBIT's, no push, no device, no deploy: `c5d92d9` the machinery and ORBIT; `1f2ccd4`
CHORUS; `8b9ca90` ARC; `089df4d` GHOST; `467ad02` MORPH; `5889a5d` SONAR; `9e0a7a4` STEPS; `e2d459d` CONSOLE; `471af83`
STRIP; `5e2ded1` LUMEN; `3679700` STAGE; `9d2b47b` fix(stamp) STAGE's captured `x` stamp; `557e6b7` SNAKE; `fe37598`
refactor(snake) its TRAPS line; `5d04477` POMODORO; `3416a49` WHEELS; `d20dad7` RADAR POINTS; `fe5bb5d` TRACKPAD;
`3ae723b` TRACKPAD COMET; `d19c1ae` RADAR; `83a7398` NINE PADS; `31134cd` test(e2e) SNAKE's walk; `1096c2b` fix(e2e)
ORBIT's library fixture; then this paragraph with `docs/TESTING.md`'s "2026-09-24 change 19", `docs/TUNING-REVIEW.md`
(35 rows and a change-19 paragraph) and the gate records `gate/change-19.*` (before, at `d7748ab`) and
`gate/change-19-after.*` (at `cfcfd3e`, on a clean worktree with its own `npm ci`, removed after). The census's 35 knobs
are on NINETEEN cards (22 is the Lua cards; CULL, QUADRANT and FOUR FADERS carry no colour).

| Card | Knobs converted | Default, on / off the lattice | Setup / Timer: old palette's corner -> picker corner |
| --- | --- | --- | --- |
| ORBIT | ring1Colour..ring4Colour | 0,200,255 off; 0,255,120 off; 120,0,255 off; 255,255,255 on | 869 / 777 -> 869 / 777 |
| CHORUS | bloomColour | 255,200,80 off | 798 / 602 -> 798 / 602 |
| ARC | swirlColour, heartColour | 0,110,255 off; 255,255,120 off | 806 / 743 -> 806 / 743 |
| GHOST | recordColour, ghostColour | 0,255,180 off; 255,80,255 off | 749 / 560 -> 749 / 560 |
| MORPH | trailColour | 180,255,255 off | 786 / 535 -> 786 / 535 |
| SONAR | sweepColour | 120,255,255 off | 591 / 668 -> 591 / 668 |
| STEPS | armed, sweep | 0,40,60 off; 0,200,255 off | 743 / 826 -> 746 / 826 |
| CONSOLE | level, rail, mute | 0,200,255 off; 60,60,60 off; 255,40,0 off | 882 / 240 -> 890 / 240 |
| STRIP | bar, vernier, rail | 0,200,255 off; 255,180,60 off; 0,25,50 off | 839 / 476 -> 849 / 476 |
| LUMEN | cursor | 255,255,255 on | 882 / 461 -> 882 / 461 |
| STAGE | live, zone | 255,40,0 off; 40,40,50 off | 665 / 136 -> 672 / 136 |
| SNAKE | body, food | 0,255,120 off; 255,140,0 off | 890 / 764 -> 896 / 768 |
| POMODORO | ring, break | 255,120,0 off; 0,140,255 off | 758 / 659 -> 766 / 669 |
| WHEELS | pitch, mod, divider | 0,180,255 off; 255,150,0 off; 90,90,110 off | 896 / 895 -> 900 / 895 |
| RADAR POINTS | sweepColour | 120,255,255 off | 897 / 758 -> 897 / 758 |
| TRACKPAD | colour | 214,255,78 off | 903 / 526 -> 903 / 526 |
| TRACKPAD COMET | colour, head | 214,255,78 off (both) | 903 / 443 -> 903 / 443 |
| RADAR | colour | 255,68,0 on | 856 / 412 -> 856 / 412 |
| NINE PADS | colour | 0,68,204 on | 752 / 764 -> 752 / 764 |

**How each knob is built, and the defaults kept.** `src/lib/catalog/lattice.ts` (new) `paletteLattice`: the card's old
colours FIRST, in their old order, then every RGB444 cell no old colour occupies - 4,096 rungs, one per cell. An old
colour off the multiples of 17 stands IN its cell (`0,200,255` where `0,204,255` would be), so 31 of the 35 defaults,
off the lattice, are kept byte-exact, and so are all the other old colours; the stamp's three-character colour field
still names one rung. Every index is where it was - the defaults (`entry.defaults` unmoved), a saved copy's, every
spec's, the residue probe's "first value of every knob" - so the wire at the defaults is byte-identical (lua-smoke's
per-card sha256 against `0f8c474`) and no rest frame, frames.json record or OG image moved (the gate: equal). **How the
picker learns a per-knob palette:** the knob's `palette` field (`LuaKnob.palette`, the old colours, which are also its
first rungs; an entry writes `...onLattice([...])`) rides `luaKnobs` to the model, which gives the view `palette` (how
many lead) and `cells` (each index's cell - a Lua card's index is not its cell, a preset's is); `ColourPicker.svelte`
reads the knob's cell through `cells` for its three rails, hands a rail's cell back as a knob index, announces the
literal the pad receives, and draws the first `palette` rungs under the rails as the quick-pick row (the shipped
`Knob.svelte` swatch row, `colour-quick`), checked while the knob stands on one; its three circles are the only radii
(moved to :833 / :860 / :875). The chip reads the three channels. Randomize rolls a lattice knob over its 4,096 rungs
(one per cell, so over the lattice) as it rolls a preset's; CHORUS stays out. Brightness: every site stays declared in
`brightness.ts`; a lattice literal is scaled as a palette one was (lua-smoke reads 221,85,153 at 170 as 147,56,102 on
a layer, on every knob).

**The sweep's sampling.** A lattice knob is never enumerated through the minifier: lua-entries test 5 walks it at its
old colours and the 27 cells of 0 / 17 / 255 (every literal length, the corner `255,255,255` among them), test 6 measures
those 27 and its own colours; brightness.spec and `hash-wire` walk the same sample; the codec alone walks all 4,096
(the round-trip sweep's Pass D, 143,360 rungs in 0.4 s). The sweep 4 / 19 green in 124 s. **The budget movers:** none
of the gated figures - the sweep had charged every Lua colour knob at 255,255,255 since 10-08 - but the reachable
maximum grew on STEPS, CONSOLE (882 -> 890, 18 free), STRIP, STAGE, SNAKE, POMODORO and WHEELS (900 / 895, the
tightest); all inside, no function moved to a Timer, no system slot. **Stamps:** a link minted before on each card
still parses and lands `older` (stamp.spec, nineteen captured payloads); the wild fixture's STAGE `x` stamp lands
`older` too. **The library fixture** (change 21A's c4 finding, reproduced: the import landed `older`, `waitForURL`
timed out): ORBIT's `orbit-copy.hangar.json` re-exported through `transfer.ts`, every index kept, the four colour
entries 4096 - 1 passed.

**Screenshots** (`shots/c19/` in the scratchpad: ORBIT, CONSOLE and NINE PADS at 1440 and 393 with a chip opened, and
ORBIT's quick row at 1440 with white picked for ring 2): seen - the rails at the cell, `0 + 200 + 255`, the quick row
of five / four / five with the default outlined, a quick pick landing on its index; nothing fixed. At 393 a row of five
wraps 4 + 1 (`Knob.svelte`'s bare row keeps two empty 44 columns; on the refuse list, and the palette row wrapped the
same before); at 1440 ORBIT's quick row sits under the pinned actions until the inspector scrolls.

**Counts, carried + delta** (carried = the tree at my start, `d7748ab`): quick 97 / 1072 + 1 todo -> 100 / 1120 + 1
todo on the whole tree at `cfcfd3e` (change 19's **+22**; `QUICK_TESTS` 1120 committed by change 20 on the same tree,
the figure my three green runs read); check 682 -> 691 (19's +1); lint clean; e2e runs 119 -> 122 (19's +0); utilities
**44 -> 44**; testids 347 -> 357 (19's +1, `colour-quick`); copy exports and SCOPED CSS unmoved by 19 (moved by 20 and
21A); frames.json, OG and the four fixtures **equal**. **The gate:** the catalog wire 0 moved, 0 removed, 1,834 added -
every one a lattice knob's sampled cell walked alone; every default, corner, old single-knob and cross-product record
byte-identical; the Sandbox set moved by 21A's 304 added fixtures alone. **Chunks** on the gate's build: c3 26 / 2 red
(rack-grid's two Sandbox-inspector titles, 21A's; the widget walk, red once at its first step under load, 1 passed
alone), c1 33 / 2 red twice (a different device title each run, each passing alone), library 1.

**Departures from the brief:** (1) every old colour kept, first, standing in its cell - not the default alone as an
extra rung (a cell names one rung in the stamp; old indices kept); (2) the wire moved by added single-knob records, the
cross-product held at the old rungs; (3) STAGE committed red and fixed in `9d2b47b`; (4) the library fixture from 21A's
finding; (5) the gate and the runs on a clean worktree, three executors sharing the tree; (6) files outside the brief's
list, named in `docs/TESTING.md`. CAT-04 stays `[ ]`.

**What I would have asked:** (1) old colours standing in their cells (their cells' own lattice literal not offered on
that card) - right, or the default alone as an extra rung? (2) the quick row wrapping 4 + 1 on a phone - let it span the
picker's width (a `Knob.svelte` change)? (3) should a preset's picker get a quick row too (its shelf colour and a few
more)? (4) a caption over the quick row ("Card colours")? (5) ORBIT's four-way selector stacks in the head at 1440 - a
select there?

**Suggestions, since you asked how HANGAR could be better:** a typed colour field beside the rails (r,g,b or hex,
snapped to RGB444); an eyedropper - click a lit cell of the preview to take its colour; a "recent colours" row shared
across cards; a harmonise action that recolours every colour knob of a card from one pick; a "same colour as" link
between two knobs (ORBIT's rings); a before / after toggle on the preview while tuning; and a gallery of saved copies on
the front door.

20. Live mirror - the browser shows what the connected ZONA is doing (2026-09-24)
--------------------------------------------------------------------------------

Suggested with nine others after change 19; the user's word:

> do 3. first

(3: "Live mirror. When a ZONA is connected, the browser shows what the pad is actually lighting and
where your fingers are.") Facts read before briefing: the grid protocol carries LEDPREVIEW (class
0x042), EVENT / EVENTPREVIEW / EVENTVIEW (0x050 / 0x051 / 0x053) and MIDI (0x000); the Grid
Editor consumes LEDPREVIEW and EVENTPREVIEW / EVENTVIEW (`grid-editor/src/renderer/serialport/
message-stream.store.ts:319-373`, `instructions.ts:259-280`) for its own LED preview and element
highlighting; `rx_mode` type 3 is EVENTVIEW routing. Recorded: a Mirror mode on the card workspace
and the Sandbox - when a ZONA is connected, the plate can show the module's own LED state
(from LEDPREVIEW) and the live touch points (from the event traffic) in place of the simulator,
with the MIDI the module actually sent in the monitor beside it; read-only, switched on by a
click, the simulator one click back. Built against the protocol and the fake ZONA; the hardware
answer is the user's bench.

Done, 2026-09-24 (change 20; `docs/MIRROR.md`, `docs/TESTING.md` "2026-09-24 change 20"). **Mirror
ZONA** - one outlined toggle on the name row beside the mode switch, on the Playground workspace and
in the Sandbox's Play, present only while a ZONA is connected - puts the module's own lights on the
plate (the same canvas, painter and colours as the simulator; the records are the frame buffer's
final values, the number the simulator computes) with `Mirroring ZONA · page N` under it, and the
MIDI the module sent in the monitor beside it, labelled ZONA; a second click, leaving Play or the
page, or a disconnect hands both back to the simulator. **What the protocol could not give: where
the fingers are.** EVENT skips the touch element and EVENTVIEW / EVENTPREVIEW carry no coordinate,
so no frame carries a ZONA finger; the plate says so under the status line, and there are no rings.
`rx_mode` type 3 turned out to be receive routing, so nothing is switched by Lua. **What it sends,
the whole write log:** the TYPE 255 heartbeat HANGAR already sent after every write, every 100 ms
while on (LEDPREVIEW reaches a host only while it counts as an editor), and one LEDPREVIEW FETCH on
the click (a full 81-LED report; a new ninth descriptor); off is silence, and the module's own 2 s
timeout ends editor mode. Never while the install store owns the port; never a config, store, page
switch or discard (the e2e decodes every chunk the page wrote from the click on and finds heartbeats
and one FETCH). Commits `8f4e0a5` (decode), `4981589` (engine), `ba0b649` (UI), `88f4041` (fix: the
mirror reached no plate before or without the simulator's engine), `ef05335` (e2e), `8308a17`
(MIRROR.md, runbook row P, audition rows 48-51), `cfcfd3e` (gate constants), and this record.
**Counts, carried + delta** (the tree at my start, `d7748ab`): quick 97 / 1072 + 1 todo -> **100 /
1120 + 1 todo** on the whole tree at `31134cd`, green twice at `--maxWorkers=2` (mine +3 files, +23
tests; the other +25 are changes 19 and 21A's); check 682 -> **691** (mine +8); lint clean; e2e mine
+2 titles / +2 runs; audition rows 47 -> **51**; utilities **44 -> 44**; testids +3 mine; copy
modules 8 -> **9** (`mirror/copy.ts`, nine exports); the wire: no record of mine moved (the
after-gate's only moves are change 19's 36 added `E/orbit` records; the Sandbox set equal).
**Chunks:** no clean run was possible beside the other two executors (0.02-0.52 GB free); the
mirror's two e2e titles passed in all four runs whose server stayed up; c1 24 / 35 and c4 25 / 26 at
`1096c2b`'s build, every other failure an attempt-2 retry, a 30 s timeout, ORBIT's brightness ratio
or the Latch walk's known monitor line, and the session file alone 16 / 16 - a quiet-machine chunk
run is owed (TESTING.md lists every run). Audition rows 48-51 and runbook row P are the hardware
answers: do the lights arrive and match the pad (serpentine and all), is there really no finger on
the wire, does the MIDI echo arrive, and is off really silence. CAT-04 stays `[ ]`.

21. One element, several MIDI messages - a touch note on the XY pad (2026-09-24)
--------------------------------------------------------------------------------

> andrew huang is testing HANGAR and ZONA right now and he asked about the following:
>  if touch on an XY pad could correspond with a note on/off in addition to sending the XY values
> - let me know if that's possible!
>
> can we implement that one element could send multiple MIDI messages?

Recorded 2026-09-24: an element's MIDI output becomes a list. Beside its own outputs (a fader's
value, an XY pad's X and Y, a button's press) an element may carry extra messages, up to three,
each with a trigger: **Touch** (on when a finger lands on the element, off when it lifts - a gate:
Note on / off, or CC max / min), **Value** (follows the element's value like its own output - CC,
Pitch bend, Channel pressure, on its own channel and number). A Touch note's velocity is fixed
(default 100) or taken from the X or Y position at the landing; its number is typed like every
note. Receive stays on the element's own outputs only. Sandbox first (21A, now); the playground's
XY cards (AURORA, PINWHEEL, STARFIELD, JOYSTICK, RADAR) take a Touch note output after change 19
lands (21B).

> in the MIDI output type Channel pressure equals note on off?

Answered: no - channel pressure (208) is one continuous pressure value for the channel, no note;
note on / off (144 / 128) starts and stops a numbered note.

> then we need to add note on off inside midi output for each element no?

Asked what Type = Note does on a continuous element (a fader, a knob, an XY axis): the value picks
the pitch, a gate with the value as velocity, both as a choice, or keep it under "+ Add message".

> Both, as a choice

Recorded 2026-09-24 (folded into 21A): every continuous element's own output Type list gains
**Note**, with a **Mode: Pitch / Gate** choice. Pitch - a ribbon: a touch plays the note the value
picks (Min..Max as the note range, a Scale select - Chromatic plus the common scales - quantising
it, the Number unused), sliding to the next note sends the old note's off then the new note's on,
lifting sends the off; velocity fixed (default 100). Gate - a touch plays the fixed Number, the
value at the landing as its velocity (through Min..Max), lifting sends the off; the value does not
move the note while held. Buttons keep Note / CC as they have. The "+ Add message" Touch note stays
for a note beside the element's own output (an XY pad's X and Y CCs plus a gate).

**Done - change 21A, 2026-09-24 (the Sandbox: extra messages, and a Note on every continuous output; nothing asked,
every decision recorded).** Six commits before the docs, no push, no device, no deploy: `92bf1c1` feat(sandbox) the
model; `51983ae` feat(sandbox) the runtime and the emitter; `fce13fd` fix(sandbox) a Pitch re-notes only onto another
note; `b94209a` feat(sandbox) the inspector; `7376ad7` test(sandbox) the VM, the encoding and the budget, the panel, the
walk; `021b8b1` fix(sandbox) an extra's summary on a line of its own; then this paragraph with `docs/MIDI.md` sections 9
and 10, `docs/TESTING.md`'s "2026-09-24 change 21A", the dated section in `docs/entries/sandbox-runtime.md`, audition
row 52 and the gate records `gate/change-21a.*` (before, at `0c4bab8`) and `gate/change-21a-after.*` (at `021b8b1`).

**The model.** A region's `extras` - up to three `{ trigger, type, channel 1..16, number 0..127, velocity?, source? }`.
TOUCH (every kind that takes touch): on at the landing - an onset, a 9, a Latch Off hand-over's arrival - a Note at its
velocity (Fixed 1..127, default 100, or From X / From Y: the landing's position on the element's box through the
calibrated axes, `p*126//127+1`, never 0) or a CC at 127; off on every path `R` covers - the lift, the sweep's expiry, a
9, the same id pressed again, another finger on the region, a hand-over's departure - the note-off or the CC at 0.
VALUE (a fader, an XY pad by its `source` X or Y, an absolute knob): the element's own scaled value on its own type
(CC, Pitch bend, Channel pressure), channel and number, sent when the element's own output is. **A multitouch pad's
Touch note is a gate for the pad: on at the first finger down, off at the last finger up** (decided); its Value
follows the first slot. Extras never receive and are never echoed. The order: the element's own messages first at the
landing, the Touch offs first at the release. A button's Touch note is a second note under its press; a blank takes
none; a Value on a button or a relative knob is kept and not sent. **The addition, folded in:** a fader's, an absolute
knob's and a one-touch pad's either axis's Type gains **Note** - **Pitch** (Min..Max the range, a Scale of nine -
Chromatic, Major, Minor, Dorian, Mixolydian, Lydian, Phrygian, Major and Minor pentatonic, the catalog's own words -
rooted on Min, the value quantised DOWN onto it; a slide sends the old note's off then the new one's on, never two held;
Velocity fixed, default 100) or **Gate** (the fixed Note at the landing's value as its velocity, never 0; a move changes
nothing). A Note does not receive (a note is an event, not a held value); not on a multitouch pad or a relative knob.

**The encoding and its cost.** A row's keyed field `m={{w,n,v},...}` - the word as the channel word spells a type, the
number, and a Touch note's velocity or 128 / 129 for X / Y, a Touch CC's 127, a Value's column negated (the sign tells
the triggers apart). Measured against a table of its own keyed by region index: the row field is cheaper everywhere
measured, data and readers - page 3 with one Touch note 18 against 49, three extras on each of page 3's four 171 against
263, a Touch note on each of sixteen's buttons 144 against 196. A Pitch is the channel word's code -3 (Gate the button's
-2) with the receive-off bit always set; its velocity rides the number column, its scale `[24]={...}` (`[25]` for Y).
**The variant**: `W` (146 with a Touch extra, 220 with From X / Y, 237 with the multitouch gate, 296 with a Note, 585
every piece), the entry `O` +24 (its call), `R` +6, `D` 174 -> 299 (a Value) / 399 (a Note) / 524 (both) - swapped in only
when a region carries an extra or a Note: **every one of the gate's 46 earlier Sandbox fixtures is byte-identical** (with
the new fields present and inert too), and the gate's Sandbox set kept all 874 of its records, 304 added by sixteen new
fixtures.

**Five slots at the picker corner** (255/6, 255/0, 255/4, Timer, Setup): page 3 receiving 893/908/905/897/877 (69 free)
-> with a Touch note on its pad 858/908/854/1147/908 - **over**; with every Receive off 893/908/907/728/502 ->
858/908/882/904/590, **fits**; the fader a C major ribbon beside it 858/908/858/1131/907, over. **The cap floor with every
option on** (change 21A's Pitch on Major and three dearest extras beside the representative's every other option): **4**
1 x 2 faders from an empty surface; `cost.ts`'s representative unmoved (11, and 14 from twelve). **No longer fit**, every
element receiving: with a Touch note on every element the three combinations change 11 put over beside a multitouch pad
(`vbxk`, `hbxk`, `vhbxk`); with every continuous element a Pitch note seven (`vbk`, `hbk`, `vhbk`, `bxk`, `vbxk`, `hbxk`,
`vhbxk`). Andrew Huang's case - a pad with its X, Y and a Touch note - fits alone and on page 3 with every Receive
off; page 3 with every element receiving is over.

**The VM cases** (`runtime.spec.ts` 24): the pad's `176:21:63 176:22:63 144:48:100`, the move's CCs, `128:48:0`; From Y
127 / 63 / 1; the off exactly once on a lift, the sweep, a 9, the same id again, another finger, a hand-over's departure
onto an Off button and its arrival from it; the multitouch gate (and a lost last finger's sweep); a Value CC 74 on channel
2 value for value with its fader; a Touch CC 127 / 0; three extras in order; page 3 with and without the note sending the
same but the note; the C major ribbon `60 62 64 67 69 72` each off before the next on; the Gate at 76 and at 1; both
modes on every release path; the notes' hand-over; a spring's silent return; a relative ribbon; a pad's X ribbon; a
knob's (which found the re-note of the sounding note: `fce13fd`). `emit.spec.ts` 12 the encoding, the variant, the
byte-identity and the budgets; `sandbox-ui.spec.ts` 32 the editor, the schema and the panel; the e2e walk: the pad's
Touch note From Y reads `Note on C4 ch 1 → 127` and its off in the Play monitor, a C major fader slid over three rows
reads E3, G3 and A3 on and off.

**The inspector.** Under a Note: `Note mode` (Pitch / Gate; `Mode` is Behavior's word), Pitch's Scale and Velocity or
Gate's Note in place of the number, no Receive, Min / Max's helper naming the note range. After the element's own rows a
block per extra on 17C's folding head - `Message n`, a chevron, the summary on a line of its own (`Touch · Note · Ch 1 ·
C4 · Vel from Y`, `Value · CC · Ch 1 · 60 · from Y`), the remove box (a cross of two straight lines, 44 px) in the lock
column - with Trigger, Type, Channel, Number, Velocity and Source; then "+ Add message", disabled at three. One element at
a time (13A's multi-edit does not reach extras); not remembered (13B - nor a continuous Note, its Mode, Scale or
Velocity); the Grid Editor profile file (13C) carries them in the landing's strings. **Screenshots** (`shots/c21b/` in the
scratchpad: the pad with a Touch note From Y and a Value CC from Y, open, folded and whole, and a fader on Note / Pitch /
Major, at 1440 x 900 and 393 x 852; wrangler dev on 4174, stopped by port through PowerShell, 5173 untouched): seen - the
blocks, the three-word Velocity row, the remove boxes in the lock column, the Note rows; fixed on seeing them - at 1440 and
393 the summary was cut to `Touch · Note · Ch 1 · C…` in the control column (`021b8b1`).

**Counts, carried + delta** (carried = the tree at my start, `0c4bab8`, changes 19's and 20's edits in flight - the
before-record's quick term printed no summary at 0.9 GB free): quick **100 / 1120 + 1 todo**, green twice at
`--maxWorkers=2` and in the gate (mine +3: runtime 24, emit 12, sandbox-ui 32); `QUICK_TESTS` not edited by me - change
20's `cfcfd3e` set 1120 on the whole tree at `31134cd`, which carried my tests. Check 685 -> **691** (mine +0); lint
clean; e2e titles +1 (122 runs); audition rows 51 -> **52**; utilities **44 -> 44**; testids 348 -> 357 (mine +6);
`sandbox/copy.ts` exports 203 -> 231 (+28); SCOPED CSS `d514ae96…` -> `d9255ffa…`, mine by name (`.extra`,
`.extra-head`, `.fold`, `.fold-name`, `.fold-summary`, `.chevron`, `.fold-rows`, `.add` and their states); catalog 27;
OG and the four fixtures unmoved. **The gate**: the wire set `e9534dba…` -> `529219e8…` by change 19's colour knobs
alone (`E/<card>/`; no Sandbox page 3, preset, library or defaults record), the Sandbox set `51e5da18…` -> `61085205…`
by new fixtures alone; the script exits 1 at the wire by design. **Chunk** c4 by files on the gate's build: 24 passed,
every Sandbox title green; red, `first-experience`'s hero-motion title (green alone) and `library`'s ORBIT-copy import
(ORBIT's rack moved under change 19's `c5d92d9`, both runs).

**Departures from the brief:** (1) the addition (Note, Pitch / Gate) folded in as relayed; (2) a Note does not receive;
(3) a Touch CC is 127 / 0, not the element's Min / Max; (4) Pitch quantises down onto the scale, not into equal zones
per scale note; (5) no Note and no per-finger notes on a multitouch pad; (6) the Note mode's label is `Note mode`; (7)
two fixes after the feature commits (the re-note, the summary line); (8) `audition.spec.ts`'s count and the route's one
line are outside the brief's file list, as 17C's and 18's were; (9) `cost.ts`'s representative is unchanged (the
floor with every 21A option on is measured beside it); (10) not fixed: the plate's CC-number label on a continuous Note
shows its stored number (a Gate's note as a number, a Pitch's unused controller) - `SurfaceEditor.svelte` is the plate,
change 20's area; the shared-controller pass ignores extras. STATE / ROADMAP / REQUIREMENTS untouched; CAT-04 stays
`[ ]`.

**What I would have asked:** (1) a multitouch pad's Touch note - one gate for the pad, or a note per finger? (2) Pitch -
the value quantised down onto the scale (a semitone's width per step, so C and D get two, E and B one), or the ribbon cut
into equal zones per scale note? (3) should a received note place a Pitch ribbon's finger (a DAW's note lighting the
ribbon)? (4) a Touch CC at 127 / 0, or the element's Min / Max? (5) page 3 with every element receiving cannot take a
Touch note - should Receive default off near the budget, or say so on the meter? (6) the plate's number under a Note -
the note name, the scale, or nothing? (7) should extras' controllers join the shared-controller pass? (8) remember a
Note (and its Mode, Scale, Velocity) for the next element of the kind, as every other Type is?

**Done - 21 fix-up, 2026-09-24 (the consolidation after 19, 20 and 21A: the whole e2e suite by chunks, the inspector's
grid with extra messages, the plate's numeral under a Note, ORBIT's picture on one frame; nothing asked, every decision
recorded).** Four commits before the docs, no push, no device, no deploy: `eb062fd` fix(sandbox) an extra message's
block on the inspector's strict grid; `9aaa413` fix(sandbox) the plate's numeral under a Note output; `4581e12` test(e2e)
ORBIT's brightness picture compared on one frame; `81b31b0` chore(gate) `QUICK_TESTS` 1121; then this paragraph with
`docs/TESTING.md`'s "2026-09-24 fix-up after 19 / 20 / 21A" (every run listed) and the gate records `gate/fixup-21.*`
(before, at `cf54963`, clean) and `gate/fixup-21-after.*` (at `81b31b0`, clean).

**The machine was quiet of executors, not of memory**: 3 to 252 MB available all pass (37.6 GB committed of 43.3, the
processes' own 11.4) - two chunk runs lost their server and are discarded. **The chunks** at `cf54963`'s build: c3 26 / 2
red (rack-grid's two Sandbox titles, expected), c4 25 / 1 red (library's page crashed - `session closed`; the Latch
walk, first-experience and library's ORBIT import all green there or alone), c5 11; reruns c1 **35 / 35**, c2 21 / 1
(`browse.e2e.ts:343`, green alone). On the fixed tree: c3 **28 / 28**, c4 **26 / 26**, c5 11, c2 21 / 1 (`:343` again,
green alone), c1 30 / 5 and 33 / 2 - every c1 red an exact count off by one retry (the fake's acknowledgement later than
`executeMs` 250 on a starved machine) or a 30 s timeout, each title green alone (598, 735, 1430 three of three; 705 and
1016 three of four) but the `webkit-phone` half of `install:2000` (Clear; the fake holds each acknowledgement 200 ms
on purpose, 50 ms under the timeout): 2 green, 3 red alone - owed a run on a machine with memory.

**The reds, cause and fix.** (1) **rack-grid's Sandbox titles**: 21A's "+ Add message" was a `.row` with no `.control`.
Now a full-width action row (`.add-row`); the extra's head states the row's four columns (label | control | reset |
lock at the row's 4px start) - the fold button across the first three, the name in the label column, the chevron in
the reset column, the summary on its own line inside the head, the remove box in the lock column; under 380px the rows'
narrow columns. The spec learns an extra's block as a group of its own (as 17C's `.output`), and its Sandbox title adds
a Value CC to its fader and measures the block: the add row across the row, the extra's rows on the control column at
the section pitch, the remove box on the lock column, the chevron on the reset column - real rows' invariant unchanged.
(2) **The Latch walk**: no change broke it. Its 2026-09-23 reds were change 18 drafting the walk itself (a different
title, before `aae17e4`); the committed walk was green that day, and red once since, at 0.1 GB free in change 20's c4.
Three green runs alone here; nothing changed. (3) **ORBIT's brightness ratio**: change 19 did not move the picture (all
1,273 of ORBIT's wire records before 19 byte-identical now; `brightness.ts` and the vendor tree untouched) and the test
reads no single colour. Both reds read 63 - one layer at half: the samples after the recompile caught no head over a
marker, the phase the title's own comment warns of. Fixed in the test, the 0.42..0.58 band kept: the page asks for less
motion, so both sides are the preview's tick-64 frame; 189 -> 95 (0.503) three times. (4) library and first-experience:
green.

**The plate's numeral under a Note** (CC numbers on): a Gate reads the note it plays by name (`D#4`, not 63); a Pitch
reads its range, Min to Max as notes (`C3–C5`; `C-1–G9` unset) - the range over the Min note because a ribbon is its
range, and the longest, `C#-1–G#9`, fits a fader cell; Pitch bend and Channel pressure keep change 17's `PB` / `CP`, no
number; CC as before; the XY pad axis by axis (`C-1–G9 E2`). `sandbox-ui.spec.ts` 33. **Screenshots** (`fx/shots/` in
the scratchpad, 4174, stopped by port, 5173 untouched): the pad with a Touch note and a Value extra open and folded, and
the plate with a Gate and a Pitch fader, at 1440 and 393 - seen, nothing fixed.

**Counts, carried + delta** (carried = `cf54963`): quick 100 / 1120 + 1 todo -> **100 / 1121 + 1 todo** (+1), green
twice at `--maxWorkers=2`; `QUICK_TESTS` **1121**; check **691 -> 691**; lint clean; the sweep **4 / 19** (128 s); e2e
titles / runs **122 -> 122**; utilities **44 -> 44**; testids **357 -> 357**; `sandbox/copy.ts` exports 231 -> **232**
(`plateNoteRange`); SCOPED CSS `d9255ffa…` -> `c673a834…` by name (`.extra-head`, `.extra-head > .fold`, `.extra-head
> .remove`, `.fold`'s inset, the 380px rule, `.add-row`, `.add`); no radius. **The gate: the wire byte-identical** (set
`529219e8…`, full `f673bf93…`, Sandbox set `61085205…`) - no Lua moved; it exits 1 at the literal census by design
(`"field"` 17 -> 16, `"row"` 26 -> 25, `"gate"` 4 -> 5, `"add-row"` +1); by hand the rest: copy exports by
`plateNoteRange` alone, testids equal, fixtures and OG equal, titles +1, `src/` 4 modified, the refuse list empty.

**Departures from the brief:** (1) two server-lost runs discarded and the webkit half of `install:2000` short of three
green alone, both the machine's memory; (2) Pitch bend / Channel pressure keep `PB` / `CP` on the plate rather than
nothing - no number is drawn, which is what the brief's "(no number)" asks, and the word is change 17's label; one line
in `numberOf` takes it off; (3) the Latch walk unchanged - nothing broke it; (4) ORBIT fixed in the test, since the
picture did not move; (5) an extra's summary stays on the head's second line (inside the head) and its chevron in the
reset column, the lock column being the remove box's. STATE / ROADMAP / REQUIREMENTS untouched; CAT-04 stays `[ ]`.

**What I would have asked:** (1) Pitch on the plate - the range (`C3–C5`), or the Min note alone? (2) `PB` / `CP` on the
plate - keep the word, or draw nothing? (3) the webkit Clear title's 200 ms hold sits 50 ms under `executeMs` - widen
that margin in the test (a 100 ms hold still shows CLEARING...), or leave it strict?

22. SERIOUS: the utility button must always change pages (2026-09-27)
--------------------------------------------------------------------------------

> Found a serious bug: Sandbox configs can remove the code under utility which makes it
> impossible for the real life hardware to change pages with pushing the utility button. you
> should never remove that function

Confirmed against the tree: the firmware's default for the system element's utility event (255/4)
is `--[[@cb]]gpl(gpn())` (grid-fw `grid_ui_system.h:38`, GRID_ACTIONSTRING_SYSTEM_MAPMODE - the
button goes to the next page). Since 13-17 the Sandbox has packed its runtime into 255/4 as a
third slot (pulled in by the Setup's `ele[#ele]:map()`), so every Sandbox store replaced the page
switch; the button then re-ran the runtime's definitions instead of changing page. RULE, from
now on and for every landing HANGAR makes (Sandbox, every catalog card, Clear, the Grid Editor
profile export): the utility event always changes page when the button is pressed. The runtime
may still live in 255/4 only behind a guard that the Setup's pull-in sets - a real press runs
`gpl(gpn())` and nothing else - and a spec proves the rule over every string HANGAR can write
there. A module already stored from the Sandbox keeps the broken button until the page is
stored again or cleared from HANGAR (the Target page select still reaches every page).

**Done - change 22, 2026-09-27 (the utility button always changes page; nothing asked, every decision recorded).** Six
commits before the docs, no push, no device, no deploy: `f10b49c` fix(sandbox) 255/4's runtime behind a guard;
`5050fbd` fix(share) a card's Grid Editor profile carries the page-next; `5382d76` fix(dev) the install probe's starting
utility string; `ba2cdd3` test(device) `utility-button.spec.ts`; `3d23128` chore(gate) `QUICK_FILES` 101,
`QUICK_TESTS` 1126; `1b49e83` test(e2e) the Sandbox store walk; then this paragraph with `docs/TESTING.md`'s
"2026-09-27 change 22", the dated sections in `docs/entries/sandbox-runtime.md` and `docs/INSTALL-RUNBOOK.md` (the
recovery), audition row 53 and the gate records `gate/change-22.*` (before, at `3990c94`, clean) and
`gate/change-22-after.*` (at `3d23128` plus `1b49e83`'s e2e file, in a clean worktree).

**The audit, every writer of 255/4, before -> after.** A Sandbox surface on five slots (Store, Apply): **wrong** -
`--[[@cb]]I=I or{}<parts>`, or `--[[@cb]]I=I or{}` alone, never the page switch -> `--[[@cb]]if self then
gpl(gpn())else <parts> end`, or `--[[@cb]]gpl(gpn())` verbatim when the packer gives it no part. On three slots: the
same. On two: `""` (the store substituted the default on the wire, the profile file carried `""`) -> the default
verbatim. The Setup's pull-in `ele[#ele]:map()` -> `ele[#ele].map()`, written only when 255/4 carries parts. Every
catalog card (27): **already right** on the wire - the tuner publishes `""` and the store wrote
`SYSTEM_DEFAULT_UTILITY`; unchanged (the rule is now `systemSlotString`, which the store and the profile both call).
Clear and the defaults leg of every Store: **already right**, `SYSTEM_DEFAULT_UTILITY`; unchanged. The Grid Editor
profile file: **wrong** for a card (`""` at 255/4 - the Editor would load a button that does nothing) and for a surface
(the unguarded runtime) -> the page-next / the guarded landing. The `/dev/install/` probe's starting string: **wrong**
if clicked (`--[[@cb]]--[[utility]]`) -> `--[[@cb]]gpl(gpn())--[[utility]]`. Put back and the skeleton's no-op cycle
write the module's own string back - the owner's, not HANGAR's; unchanged. `sequence.ts`'s slot list is right and
untouched; `library.ts` and `library-trim.ts` write nothing to 255/4.

**The guard.** The firmware registers a body as `ele[n].map = function (self) ... end` (`grid_ui.c:374`) and fires a
press as `eve(element)` (`events.lua`), so on a press `self` is the system element; the pull-in's dot call passes
none. 255/4 is `--[[@cb]]if self then gpl(gpn())else <parts> end` - the marker kept, a fixed point of the pinned
minifier - so a press runs `gpl(gpn())` and nothing else (no part defined again, `Y`'s `s.touch_cb~=O` safe) and the
pull-in defines the parts. **Cost: 32 characters in 255/4, none in the Setup.** Measured beside it: `if not self then
... else gpl(gpn())end` 36 / 0; the brief's global flag - no single capital is free (the library's twenty-one, the
runtime's `S F I R O Q D K W V Y Z A E`, the emitter's `J M`), so `UB`: Setup `UB=1 ele[#ele]:map()UB=nil`, 255/4 `if
UB then <parts> else gpl(gpn())end` - 30 / 12, and it put sixteen elements every one Off over the cap (Setup 920). The
guard alone left one element of every kind receiving (`vhbxk`) with no placement at any search depth, so the branch
table's head `I=I or{}` now stands only in a slot that defines a branch: nine back in every branchless slot, `vhbxk`
fits again (4,518 of 4,540).

**The five slots at the picker corner, before -> after** (255/6 / 255/0 / 255/4 / Timer / Setup; all 62 fixtures in
`docs/TESTING.md`'s table): page 3 893/908/905/897/877 -> **893/902/903/897/901, fits** (44 free, 69 before; 255/4
carries `R A I[4]`); page 3 receiving nothing 893/908/907/728/502 -> 893/908/880/779/502; eight 852/907/894/181/614 ->
852/907/896/195/614; sixteen 852/907/894/181/892 -> 852/907/896/195/892; sixteen every one Off 858/907/908/293/908 ->
858/907/848/369/908; two elements 892/903/508/111/412 -> 906/903/510/111/412; the knobs fixture 893/890/17/113/492 ->
893/882/19/113/477 (255/4 the default verbatim, no pull-in). **No longer fit:** page 3 with every option on
(906/908/905/897/902 -> refused on the Timer, 950), page 3 with every type (906/908/905/897/900 -> over), and one
element of every kind receiving at emit.spec's dearest literals (`vhbxk`); with every continuous element a Pitch note
`vhbx` is over and `vbk`, `hbk` fit again. The Setups did not move: the cap floor (11, 14 from twelve) and the floor
with every option on (4) unmoved. 55 of the gate's 62 fixtures fit before, 53 after.

**The proof.** `utility-button.spec.ts`, five tests: 307 strings (47 distinct) - the 62 gate fixtures at two, three
and five slots and at the corner (248: 184 guarded, 64 the default), the 27 cards at their defaults and at their
corner through the store's substitution (54), Clear and the Store's defaults leg, the profile file for a surface and a
card, the probe's start, the default - each run in wasmoon inside the firmware's wrapper and pressed as the firmware
presses: `gpn` then `gpl` once, no other call, no global added or replaced, no error; each guarded one also pulled in
as the Setup does (every part the packer put there defined, no page call) and pressed after that (the same page switch,
every definition left as it was). A negative check presses the runtime without its guard and finds no page switch.
runtime.spec's and emit.spec's VM tests unchanged and green; their `map` stand-in names its parameter `self` now, as the
firmware does, and so does `preview.ts`'s (the live preview's).

**Counts, carried + delta** (carried = `3990c94`, clean): quick 100 / 1121 + 1 todo -> **101 / 1126 + 1 todo** (+5,
the new spec), green twice at `--maxWorkers=2` on the worktree; `QUICK_TESTS` **1126**; check 691 -> **692**; lint
clean; the sweep **4 / 19** (168 s); e2e c4 by files **26 / 26**, c1 by files **35 / 35** (install and session needed
no edit - neither pins a Sandbox landing's frames; the probe's string is read at run time), a first c4 run discarded
(the worktree had no `.dev.vars`: every request 401); titles 1122 -> 1127 vitest, 122 playwright runs; audition rows
52 -> **53**; utilities **44 -> 44**; SCOPED CSS **`c673a834…` equal**; testids 357 equal; copy exports equal;
fixtures and OG equal. **The gate:** the catalog wire did not move - 7 of 17,890 records moved, every one `S/page3/`
(set `529219e8…` -> `032a53f3…`); the Sandbox set `61085205…` -> `651664f8…`, 470 of 1,178 byte-identical (every
two-slot record), every 255/4 and Setup at three and five slots moved, 255/6, 255/0 and the Timer where a head or a part
moved; exit 1 at the wire by design.

**A second executor ran change 24 in the same tree** (its `--before change-24` at 18:38Z read this change's
uncommitted edits; its `2210d8f` landed between `3d23128` and `1b49e83`); every commit here is `--only` on this
change's paths; every count, run and gate term above was taken on a detached worktree at `3d23128` with its own
`npm ci`, removed and pruned after (the five-slot figures were measured in the tree before the other's first edit). `QUICK_TESTS` 1126 is this change's tree's figure; `2210d8f`'s tests are not counted in it.

**Departures from the brief:** (1) the guard is `self`, not a global flag - measured cheaper and it leaves the Setup
alone; (2) the branch table's head only where a slot defines a branch, to keep `vhbxk` on five slots; (3) the profile's
three system slots through the store's one rule (the brief named 255/4); (4) the install probe's starting string, not
in the brief's list; (5) `preview.ts`'s stand-in parameter; (6) the figures on a worktree, and one c4 run discarded;
(7) `audition.spec.ts`'s count. STATE / ROADMAP / REQUIREMENTS untouched; CAT-04 stays `[ ]`.

**What I would have asked:** (1) the `self` guard or the global flag the brief shaped - the flag costs ten more and
put sixteen elements every one Off over; (2) is losing page 3 with every option on, and one of every kind at the
dearest literals, acceptable for the button, or should the packer look for more (the Timer's head, the trimmed
halves)? (3) should Put back refuse to restore a pre-fix Sandbox 255/4 (a module's own string, but a broken one)?

23. Store and Clear over a page HANGAR did not write (2026-09-27)
--------------------------------------------------------------------------------

Andrew Huang's report (email, 2026-09-26), relayed by the user: sending a Playground config to page
2 with "Store on ZONA" always fails with "HANGAR couldn't read what Page 3 holds, and it won't
write over something it hasn't copied. Nothing was written." - page 2 held a configuration made in
the Grid Editor; pages 1, 3 and 4 work; the page named in the message changes between attempts.

> Andrew reported a bug on his page 2 which held a config made in the Grid Editor not Hangar.
> Hangar SHOULD be able to rewrite (store into it) and clear configs made in Grid Editor as well.

Read before briefing: the block is `snapshotFailedBlock` (install-copy.ts:357), raised by
`#snapshot` (install.svelte.ts:541) when `fetchAll` times out or `canWriteBack` refuses the read
(D-03: an empty string is what a fetch of a non-active page produces); the page it names is
`#page()`, not the Target page the user chose - hence "Page 3" for a Store to page 2. Recorded:
(1) Store and Clear never refuse because the page could not be read - the snapshot is a
best-effort copy (Put back was retired in 13.1), and a Store / Clear over a page HANGAR could not
copy goes ahead, the previous configuration replaced; (2) every message names the page the user
chose; (3) the cause is found and fixed where it is HANGAR's (reading a page that is not the
active one, a timeout on a Grid Editor configuration's strings) so the copy succeeds whenever the
module can give it. Queued after change 22 (both edit the install path).

### The strings change 23 retired, verbatim (D-05's register)

In `src/lib/device/install-copy.ts` (the ledger block "THE UNCOPIED PAGE'S STRINGS ARE RETIRED BY
NAME, 2026-09-27"); the sample page is wire 1, read as `Page 2`. No string was written in their
place: a page HANGAR could not copy is written like any other, and the failed copy is said nowhere.

- `SNAPSHOT_FAILED_TITLE` (the block's title, I.5.7 as reworded at 13.1-06):
  ~~`Nothing copied yet`~~
- `snapshotFailedBlock` (its detail and its one step):
  ~~`HANGAR couldn’t read what Page 2 holds, and it won’t write over something it hasn’t copied. Nothing was written.`~~
  ~~`Click Store on ZONA to read it again`~~
- `CLEAR_REASONS["no-snapshot"]` (Clear's reason while no copy was in hand):
  ~~`Needs a copy of what is on your ZONA first.`~~

**Done - change 23, 2026-09-27 (Store and Clear over a page HANGAR did not write; Andrew Huang unblocked; nothing
asked, every decision recorded).** Six commits before the docs, no push, no device, no deploy: `0560efe`
fix(protocol) the restore rule in `systemSlotString` and the skeleton's `writeBack`; `73dc12b` fix(install) the copy
best-effort, `snapshot-failed` retired, the Target named, the fake, the strings ledger above; `6b75c4c` test(device)
the restore writers in `utility-button.spec.ts`; `a918fc0` test(e2e) the Grid Editor page on the fake; `71efd55`
chore(gate) `QUICK_TESTS` 1138; `6bd577a` docs(audition) row 55; then this paragraph with `docs/TESTING.md`'s
"2026-09-27 change 23", the dated section of `docs/INSTALL-RUNBOOK.md` and the gate records `gate/change-23.*`
(before, at `6ee5072`) and `gate/change-23-after.*` (at `71efd55`), each on a detached worktree with its own `npm
ci`, removed and pruned after.

**The diagnosis - which cause Andrew hit.** The firmware stores and answers a configuration string as it was
written, any byte (`grid_decode.c:1262-1290`; the fetch returns the registered source, `grid_ui.c:420-455`). The Grid
Editor sends `compressScript(toLua())` verbatim and refuses only at 909 characters (`instructions.ts:163-173`), and
the pinned minifier keeps a `--` comment with its line break (`--[[@cb]] -- note\nlocal x = 2\nprint(x)`
minifies to `--[[@cb]]\n-- note\nlocal x=2 print(x)`), so a Grid Editor page routinely holds line breaks, and tabs. HANGAR's copy
ran the write-back guard, `canWriteBack`, whose printable rule (`^[\x20-\x7e]*$`) refused that page on every read ->
`snapshot-failed`; the Store click read again first and wrote nothing unless a copy landed, so "Store on ZONA always
fails" on exactly that page and never on pages 1, 3 and 4 (HANGAR's own or factory strings, printable). **The page
number moved** because the block named `#page()` = `snapshotPage`, the page last COPIED: arriving on page 2 from page
3 it said "Page 3", from page 1 "Page 1"; and from `snapshot-failed` no further switch re-read (not a writable
phase), so the name stayed stale until a click. Reproduced on the fake against the code before the fix (a throwaway
spec in the before-worktree, never committed): H4 the line break -> `snapshot-failed`, "Page 3", three Store clicks
0 CONFIG/EXECUTE, `canWriteBack`: "Setup has a non-printable character at index 9" - **Andrew's case**; H3 a
905-character printable page copied fine - not the length; H1 a fetch in the page load a switch starts
(`grid_ui.c:1017` reports the new page first; `grid_ui.c:466-474` refuses the recall; `grid_decode.c:1318-1360` then
sends a NACK AND the empty REPORT) failed the copy once and the click's re-read then succeeded - never "always"; H5
extra element-0 events cannot exist (a ZONA's touch element has events 0 and 6, its system element 0, 4 and 6 -
`grid_module.c:455-466`: the five slots are the whole page); a non-active page is fetched only in that race - HANGAR
fetches the page the module reports, after the module's own report confirms a switch.

**The new rule.** Store on ZONA and Clear never refuse because a page could not be read. The copy is best-effort:
`canCopy` (a string came back and it is not the refused-fetch shape) replaces `canWriteBack` for it, so a Grid Editor
page is copied as it came (for this tab and the browser's record, as before); it is tried three rounds with the
backoff (the page-load race); a page not copied lands `ready` with no copy in hand, nothing persisted, nothing spoken.
A Store over it is the eighteen frames, a Clear the twelve, the read-back proof deciding, the page replaced. **The
block is retired, not kept as a note:** `snapshot-failed` left the union (13 phases), `snapshotFailedBlock`,
`SNAPSHOT_FAILED_TITLE` and `CLEAR_REASONS["no-snapshot"]` by name (the ledger above; no string written) - the copy has
had no control on the site since 13.1-06 (D-07), so a sentence about it after a successful store would be a false
alarm of change 3's kind. Clear no longer asks for a copy (SAFE-03's third term; the next gate amends it). Nothing else
on the write path moved: the click, the defaults leg, RAM then flash, the read-back proof.

**Every message fixed** (the page named is the Target, the page chosen): `#page()` is now the page the action
ADDRESSED (the module's reported page at the leg's start - writes never leave with the target not at rest), else the
page read; `snapshotPage` is set on the copy's first line, copied or not, so it follows the Target and a page that
cannot be copied is read once per arrival, not once per heartbeat. Through `#page()`: `liveSnapshotSaved`,
`liveKept`, `liveCleared`, `liveRestored`, `liveSettled`, `keptMismatchBlock`, `partialBlock`, `nothingLandedBlock`,
`lostBlock`, `restoredUnconfirmedBlock` (spoken titles carry no page; the live lines do). In the components:
`DestinationZone.svelte`'s `page` (Store's description, every failure block) and `Clear.svelte`'s (its description
and `Clearing…` label) read `pageRequested` first, then `snapshotPage`; the bar's clause reads `snapshotPage` through
the routes, which is the Target once read. The retired block was the only one that named the page last copied.

**Where the failure was HANGAR's, fixed:** the guard (above); the page-load race (the rounds); the enumeration, which
ran only after a successful read, runs either way. Fetching "with the right page field" needed nothing: HANGAR already
fetched the reported page. The fake learned firmware's answers: a refused fetch is a NACK and then the empty REPORT
(it was the REPORT alone), `loading` counts fetches a page load refuses, `pages` holds every other page's flash and a
switch loads the page it reaches.

**Change 22's folded decision.** `systemSlotString` writes a 255/4 that starts with `PRE_GUARD_UTILITY_HEAD`
(`--[[@cb]]I=I or{}`, every Sandbox store's head before `f10b49c`) as the firmware's page-next; no landing HANGAR makes
today starts with it, so no wire moved. Writers through it now: the probe's put-back (its three system slots through
`#systemStringOr`; it also refuses a copy it cannot write - a Grid Editor line break, which `sendConfig` refuses - before
the wire) and the walking skeleton's `writeBack` (the no-op cycle; a pre-change-22 page now reads back as no no-op,
which is true). The guard is not added to the old string: that page's old Setup pulls 255/4 in as a method call,
which would take the guard's page branch; the button is kept, the old runtime is not, and the page works again once
stored from the Sandbox. A Grid Editor owner's own utility script is restored as it was. The firmware-native revert
(PAGEDISCARD) writes no string. Added to `utility-button.spec.ts` as test 6: 46 pre-change-22 strings (every gate
fixture at three and five slots with the guard taken off, and the head alone) pressed as written (no page switch),
then through the put-back's rule (all 46) and off the skeleton's write-back frames (8 of them) - the page switch,
pressed in wasmoon - a guarded 255/4 and the default written back as they came.

**Counts, carried + delta** (carried = `6ee5072`, built): quick 101 / 1129 + 1 todo -> **101 / 1138 + 1 todo** (+9:
install.spec +4, write-guard.spec +2, synthetic.spec +2, utility-button.spec +1), green twice at `--maxWorkers=2` on
the after-worktree; `QUICK_TESTS` **1138** (read fresh: 1129 was change 24's); check 692 -> **692**; lint clean; e2e
**c1 by files 36 / 36** (35 before; a first run 35 / 1 on the new title's own assertion, which had not counted the
copy's reads after the switch - tightened to name them; no server died); titles 1130 -> 1139 vitest, 122 -> 123
playwright runs; audition rows 54 -> **55**; utilities **44 -> 44**; SCOPED CSS **`c673a834…` equal**; testids 357
equal; copy exports `08194098…` -> `48ba2b07…` (three retired, none joined); fixtures and OG equal. **The gate:** every
wire term byte-identical - set `193d352f…`, full `bbb8a19b…`, Sandbox set `651664f8…`; exit 1 at the census by design
(out: the block's three strings, the Clear reason, `no-snapshot`, `snapshot-failed`; in: `canCopy`'s two reasons, the
head). The before record's quick term read exit 1 only because a fresh worktree has no build (radius.spec layer B);
run by hand once built, green. **The e2e write log** (the new title): connect and browse write nothing; the Target
change is HEARTBEAT (255) then the switch, then SERIALNUMBER/FETCH and five CONFIG/FETCH at Page 2; Store 18 frames at
Page 2 (the defaults verbatim in SLOTS order, HEARTBEAT, the card's five, HEARTBEAT, PAGESTORE, five fetches), kept
after 2 heartbeats; Clear 12; the wire 15 CONFIG/EXECUTE, 2 PAGESTORE, 1 switch, 0 discards, 4 heartbeats.

**Change 24 ran in the same tree** (its `5b56e9e`..`edff143` landed between my before record and my commits: e2e, docs
and the gate script, no `src/` file - so the wire and the src terms compare clean; its e2e titles are in the titles
term). Every commit here is `--only` on this change's paths.

**Departures from the brief:** (1) the block retired rather than kept as a note (above); (2) `device-clause.ts` and
the probe's `retrySnapshot` (re-aimed as the probe's read-again with no copy in hand; the probe route untouched) moved
because the phase left the union; (3) `write-guard.ts` gained `canCopy` (the copy's own guard; `canWriteBack` unchanged
for the write-back); (4) the put-back refuses an unwritable copy before the wire (a consequence of copying Grid Editor
pages); (5) `audition.spec.ts`'s count; (6) the gate on worktrees. STATE / ROADMAP / REQUIREMENTS untouched; CAT-04
stays `[ ]`; next gate: amend SAFE-03.

**What I would have asked:** (1) retire the block or keep a quiet note after the store - retired; (2) should a
restore also refuse a Grid Editor owner's own utility script that does not turn the page, or only HANGAR's own
pre-change-22 string - only HANGAR's here; (3) the hardware - audition row 55 is where the page-load race and the
Editor's stored bytes meet a module.

24. SNAKE's step time freely chosen (2026-09-27)
--------------------------------------------------------------------------------

Andrew Huang, same email: "I'm wondering if it would be possible to freely choose the step time
of Snake instead of only being able to use the four preset values that are offered."

Recorded: SNAKE's step time becomes a fine ladder - every 10 ms from 50 to 1000 ms (96 rungs), the
stepper types any value and snaps to the nearest 10 ms; the default stays 220 ms (the first game,
frames.json and the OG unmoved); both events re-measured at the worst literal; older links land
at the defaults (the stamp's known pattern).

> can you also put a MIDIRX or something on Snake like in sequencers so you can midi sync the
> steps inside a DAW?

Recorded 2026-09-27, folded into change 24: SNAKE takes the sequencers' MIDI clock sync (ORBIT's
idiom, change 8; STEPS, RADAR POINTS, GHOST in 12; RADAR in 12b) - `Sync: Internal / External`
and `Division` (8th / 16th / 32nd = 12 / 6 / 3 clocks a step); under External the snake steps on
the DAW's clock, Start begins a fresh game on the bar, Stop pauses the snake where it is (its last
note released), Continue resumes; the step time knob is ignored under External.

Change 22's questions, decided 2026-09-27: the `self` guard stands (a firmware press always passes
`self`, the Setup's dot call passes none; ten characters cheaper than a flag and the Setup
untouched); the four dense surfaces that no longer fit are accepted (the budget's own refusal);
a restore of a module's own pre-fix Sandbox 255/4 (Put back's path) must not bring the broken
button back - every write goes through the store's `systemSlotString` rule, folded into change 23.

**Done - change 24, 2026-09-27 (SNAKE's step time every 10 ms from 50 to 1000, a fixed death pause, and the DAW's
clock).** Outside the GSD cycle, no plan file; no push, no device, no deploy. Commits: `2210d8f` feat(catalog) - the
entry and the spec pins; `5b56e9e` test(e2e) - the widget walk; `0935333` chore(gate) - `QUICK_TESTS` 1129; `9bca427`
docs(audition) - row 54 and `ROW_COUNT` 54; then this paragraph with `docs/TESTING.md`'s "2026-09-27 change 24",
`docs/entries/snake.md`'s dated section (the two strings as they stood at `de624ee`, verbatim), `docs/TUNING-REVIEW.md`'s
SNAKE rows and the gate records `gate/change-24.*` (before, at `f6e8922`, over change 22's uncommitted edits - the
tree was shared) and `gate/change-24-after.*` (at `0935333`, on a clean worktree with its own `npm ci`, removed after;
change 23's edits were in the main tree by then). Every commit `git commit --only` on this change's paths.

**The ladder.** `@SPEED` is every 10 ms from 50 to 1000, ascending - 96 rungs, two stamp characters - with 220 the
default at index 17, so the first game, `frames.json` and the OG did not move. The stepper walks it in tens and a
typed value snaps to the nearest ten (a tie to the lower: 115 is 110). Ignored under External.

**The death pause, decided: a fixed time.** Six generations made it 6 s at 1000 ms and 0.3 s at 50; it is now six
fixed 220 ms beats at any Step time - the flash 660 ms, dark 660 ms, 1.32 s, the default's own pause, so the default's
ticks did not move. The death step and every countdown call re-arm the Timer at 220, the restart's `gtt(0,@SPEED)`
wins. Under External the pause is six clock STEPS (the re-arms are inert there), so the restart lands on the DAW's
grid and a Stop freezes it with the snake.

**The clock** (the user's addition, ORBIT's idiom): `Sync` Internal / External and `Division` 8th / 16th / 32nd (12 /
6 / 3 clocks a step, a 16th by default). `grxm(2,@SYNC and 3 or 0)` at the Setup's end; the Timer makes
`s.rtmrx_cb` with the step, the release and the restart as upvalues: Start releases the pending note and begins a
fresh game from the two-cell snake (the clock count at 0), Stop releases and freezes the snake where it is, Continue
resumes from the kept count, a clock steps every `@DIV`, active sensing does nothing; under External the Timer steps
nothing. `self.midirx_cb=nil` stays - SNAKE receives no voice MIDI; `rtmrx_cb` is the other field. **The caveat is
ORBIT's:** the callback is the Timer's, so a Start or a clock inside the first Step time after a Store is not seen and
the snake waits for the next play. Nine knobs to eleven (three to five outside the outputs).

**The costs, Setup / Timer at the RGB444 picker corner** (under the pinned `compressScript` after
`initLuaFormatter()`; the defaults in brackets): **896 / 768 -> 904 / 892** (890 / 757 -> 894 / 882) - 4 and 16 free.
The ladder alone was 897 / 769 (`1000` one character over `300` in each event); the clock as first written was 842 /
1,133, 159 over the two events together, so the Lua was tightened without moving a frame (the painter's two layers in
a loop, the steer by `c*c>r*r` and `glim`, the flash through `pairs(s.o)`, the off status computed at the release, the
new head into `s.o` before the placer, `I(s,q)` doing the countdown's black) and the Bite (growth, placer, note) moved
into the Setup's `F`. No system slot, no library helper. The budget is spent: a new knob on SNAKE needs a cut first.

**The stamp.** Every SNAKE link minted before today lands `unreadable` and opens the card at its defaults (the rack
grew and the step time is wide) - `stamp.spec.ts` holds one minted at `de624ee` (110 ms) and change 19's captured one;
a link minted now (110 ms, External, a 32nd) restores. **Frames and OG:** `frames.json` `290ff266` and the OG (27 files,
159,169 B, `9becd682…`) byte-identical through the gate; the text at the defaults moved (change 19's SNAKE capture
re-taken, `ec3322d7…` -> `dd5230ed…`), the first game did not (lua-smoke's pre-change note list holds).

**Proved.** `lua-smoke.spec.ts` +2: at 50 and 1000 ms the Timer re-arms at the period, one step per period, the first
game on the same steps times the period, the pause 1.32 s at either end; on the clock - routing, the Timer stepping
nothing, Start / a step every Division / Stop's and Start's releases / Continue / active sensing, the whole first game
and its pause in steps, Division 12 and 3. `stamp.spec.ts` +1. **Counts, carried + delta** (carried = change 22's
committed figures): quick 101 / 1126 + 1 todo -> **101 / 1129 + 1 todo** (+3), green twice at `--maxWorkers=2`; check
**692**, 0 errors; lint clean; sweep **4 / 19** green; e2e runs **122 -> 122** (one title renamed), c3 by its three
files **28 passed** on a fresh detached wrangler dev on 4173, HTTP 000 after (it waited for change 22's e2e on the same
port; no server died; 5173 untouched); utilities **44 -> 44**; testids, copy exports, scoped and raw CSS, the four
fixtures and the OG equal; audition rows 53 -> **54**. **The gate:** the wire set `c8ecbfed…` -> `193d352f…`, 17,863
-> 18,057: 17,142 identical, 721 moved (714 `E/snake/`, 7 `S/` - change 22's), 0 removed, 194 added (all `E/snake/`);
the full `6d5f5a3c…` -> `bbb8a19b…` (1 removed / 195 added: SNAKE's cross-product renamed, 1,152 -> 6,912 states);
the Sandbox set moved by change 22 alone (393 of 1,178). Nothing outside `E/snake/` is this change's.

**Departures from the brief:** the step routine and the clock callback live in the Timer (the Setup had 12 free), so
the first-period caveat; `if not @SYNC then f()end` for ORBIT's `if @SYNC then return end f(s)` (four characters); the
Lua tightened and the Bite moved to the Setup to make room; five files beyond the brief's list moved by necessity
(`stamp-roundtrip.sweep.spec.ts`, `knobs.lua.spec.ts`, `tune-ui.spec.ts`, `e2e/tuning.e2e.ts`, `audition.spec.ts`);
`view.spec.ts` and `tune-ui.spec.ts` keep the old descending ladder as their own fixture of the value-order rule, and
`view.ts`'s comment still names it; the audition's cost table row for SNAKE (871 / 732) predates 17B and 19 and was
left, row 54 carries today's; the after-run on a clean worktree. CAT-04 stays `[ ]`; STATE / ROADMAP / REQUIREMENTS
untouched. Not deployed.

**Questions for the user.** (a) The pause is 1.32 s at every Step time under Internal and six steps under External;
say if External should pause in time too (it would restart off the DAW's grid). (b) The callback is made by the
Timer's first call, so a DAW already playing at a Store needs a Stop and a play; say if that matters on the bench (the
Setup has 4 free - it would need another cut). (c) A later Start seeds the food walk with the steps counted so far, so
each Start plays a different game; say if every Start should replay the shelf's first game instead. (d) Changes 1
to 23's questions still stand.

25. The centre panel fits the screen, never scrolls (2026-09-28)
--------------------------------------------------------------------------------

> One change across all HANGAR: the middle panel (whether in Sandbox or Playground) where the ZONA
> lives should not be scrollable but fit the screen always

Recorded: on the card workspace and in the Sandbox the centre column (the plate, the card or
surface name row and its lede, the Configure / Play / Mirror switches, the plate's status and
coordinate lines, the MIDI monitor bar) always fits the shell's height with no scroll: the plate
is sized from the space left after its fixed rows (the smaller of the column's width and the
remaining height, so it stays square), and every row around it keeps its size. The side panels
(the rail, the inspector) keep their own scrolling. On a phone-width stacked layout, where the
page itself scrolls, the plate fits the viewport's width and height the same way.

**Done - change 25, 2026-09-28 (the centre panel fits the screen, never scrolls).** Outside the GSD cycle, no plan
file; no push, no device, no deploy. Commits: `7d63547` feat(shell) - the `fit` shape and the numbers; `7cf1af0`
feat(playground) - the card; `e2d226d` feat(sandbox) - the Sandbox; `6b8c71d` fix(sandbox) - the plate's menu in the
top layer; `82a7e04` test(e2e) - `e2e/layout.e2e.ts`; `f7b4159` chore(gate) - the records `gate/change-25.*` (at
`663651d`) and `gate/change-25-after.*` (at `82a7e04`), both on the clean tree; then this paragraph with
`docs/TESTING.md`'s "2026-09-28 change 25". Every commit `git commit --only` on this change's paths.

**The sizing rule** (written once in `layout.ts`, THE CENTRE FITS THE SCREEN). The card workspace and the Sandbox
declare `fit` (as data, so the prerendered frame has it, and in the fill); the shell's centre is then a flex column of
the frame's height with `overflow: hidden`. The rows keep their height; the plate's region takes the rest as a size
container, and the plate is square at the smallest of the region's width, its height less the plate's own caption
rows, and its cap - exact CSS, `min()` over container units, no ResizeObserver; neither the canvas (9 x 9, upscaled
pixelated) nor the Sandbox's SVG (a viewBox) needs a repaint. On a card: `min(width, height - 16 - 18, 600)`, the
stage's ceiling the plate its width allows plus the caption, so a plate held by its width leaves the slack under the
last row. In the Sandbox, the editor alone: `min(width, height - 75.7, 571)` (the caption 12 + 18, the status line 8 +
two lines). **One move beyond the brief's letter:** the Play monitor, and the empty surface's instruction and
starters, stand BESIDE the plate on the desktop - the editor `min(height - 75.7, 571, width - 160 - 24)` wide, the
aside 160 to 320, never taller than the region, scrolling inside itself. Under the plate the monitor alone was about
180px of the 416px column at 1280 x 720 and left a Play plate of a few pixels. The open MIDI monitor on a card takes
two parts of the height to the plate's three, its panel scrolling. The caps stay (600 card, 571 Sandbox).

**The clamps.** The card's name and the surface's name: one line with an ellipsis, the full name as the title (and
in the breadcrumb and eyebrow); they give way to 120px before the switches wrap. The card's lede: two lines. The
Sandbox's sub-line (the same sentence on every surface): one line; its mode, Mirror status and Mirror note lines: two.
The card's Mirror note and quiet line: two. The Sandbox's status line: two, at a fixed height. Every clamped line
carries its full text as the title. The caption under the plate is one fixed 18px line, as wide as the plate but never
under 310px while the column has it; the readout (coordinates, the Mirror status, the element count) stays whole at
the right and the matrix label gives way first, then steps aside below 310. The side columns keep their own scrolling.

**The plate's menu** (a fix the change needed): its ten rows are 450px, taller than the plate's column at 1280 x 720,
and a column that no longer scrolls cut it off. It is a manual popover in the top layer, placed in viewport pixels at
the same point with the same flip past 0.55 and held 8px inside the viewport: whole at every size measured.

**The stacked layout** (below 1024): the page scrolls, as before; the centre is a plain block and the plate is also
held under the viewport's height less twice the centre's 24px padding and its caption rows (34 on a card, 75.7 in the
Sandbox), so scrolled to, the whole plate and its caption are on screen. At 393 x 852 the plate is 361 (width-bound,
unchanged) and runs from 8 to 369 of 852 when scrolled to; at 844 x 390 it is held by the height (proved in e2e).

**Measured, the plate's edge in px** (the served build, chromium; the centre's scrollHeight equals its clientHeight in
all 45 desktop views, the document equals the viewport, no two rows overlap):

    viewport    ORBIT  ORBIT,open  AURORA  PINWHEEL+Mirror | Sandbox empty  Edit  Play+lines  Play+Mirror
    1920x1080     518        324     580              481 |           556   556         556          508
    1440x900      314        201     376              277 |           376   376         376          328
    1366x768      182        122     244              145 |           244   244         244          196
    1280x720      134         93     196               97 |           196   196         196          129
    1024x768      182        122     244               62 |           127   157         127           46
    393x852       361        361     361              361 |           361   361         361          361

Before, every desktop centre scrolled (ORBIT at 1280 x 720 930 / 464; the Sandbox in Play with lines 1034 / 464) with
the plate at its cap. The short screens pay: at 1280 x 720 ORBIT's plate is 134 (the two-line lede, the monitor bar
and the fidelity line take the rest) and at 1024 x 768 with Mirror the Sandbox's is 46 (the header holds five lines
and three rows of switches in a 311px column). The levers left are the user's to pull: the eyebrow repeats the
breadcrumb, the footer is 121px, the lede could hold one line. Screenshots, before and after, at 1920 x 1080, 1440 x
900, 1280 x 720, 1366 x 768, 1024 x 768 and 393 x 852 (ORBIT shut and open, AURORA, PINWHEEL with Mirror through the
fake ZONA, the Sandbox empty, in Edit, with its menu, in Play with and without lines, in Play with Mirror): outside
the tree, in the session's scratchpad.

**Proved.** `e2e/layout.e2e.ts`, five titles: at 1280 x 720 and 1920 x 1080 on both routes, the centre and the
document within their height, the plate square, inside the column and within 1px of the rule's edge for its measured
region, no row over another, the Play monitor 24px beside the plate and inside the region, the menu inside the
viewport; and the stacked plate at 844 x 390. No existing pin moved. **Counts, carried + delta** (carried =
`663651d`'s): quick 101 / 1138 + 1 todo -> **101 / 1138 + 1 todo** (+0), green twice at `--maxWorkers=2`;
`QUICK_TESTS` stays 1138; check **692**, 0 errors, 0 warnings; lint clean; e2e runs 123 -> **128** (+5); by files on
a fresh wrangler dev on 4173: c3 with `layout.e2e.ts` **33**, c4 **26**, c5 **11**, c1 **36**, all passed; c2, run
for 13.1's frame title (green), 20 / 2 twice, the reds the recorded `/playground/` hydration family, each green alone;
no server died; 5173 untouched. **The gate:** the wire set `193d352f…`, full `bbb8a19b…` and Sandbox set `651664f8…`
byte-identical; copy exports and the 357 testids equal; **utilities 44 -> 44**, none appeared or disappeared; the
census moved by six literals (`stage`, `menu`, `menu root`, `manual`, `showPopover`, `${}px`) and the script exits
there by design; scoped CSS `c673a834…` -> `37263da0…`; OG and fixtures equal; `src/` 11 modified, 0 added.

**Departures from the brief:** the aside beside the plate (above); the menu into the top layer; the caption's 310
floor and the label stepping aside; the Sandbox's sub-line at one line; the names' 120px floor; the open monitor's
3 : 2 share; `MidiMonitor.svelte`, `PlayMonitor.svelte` and `ContextMenu.svelte` moved beyond the named files; the
Sandbox's handles and delete icon, drawn in the SVG's user units, scale with the plate as they always did and are
small on the short screens. CAT-04 stays `[ ]`; STATE / ROADMAP / REQUIREMENTS untouched.

Change 25b, recorded 2026-09-28: at 1280x720 the fitted plate is 134 px (ORBIT) - the chrome eats
the height (header 75, breadcrumb bar 60, footer 121). Asked which space to take back:

> Footer off the app pages, Drop the duplicate breadcrumb

Recorded: on the app pages (the card workspace and the Sandbox) the 121 px footer becomes a thin
strip (~32 px, one line: HANGAR / by intech studio, GPLv3, Third-party notices, Source, the build
id, Help & shortcuts, Device actions) - or leaves those pages entirely with its links reachable
from the header's menu; the full footer stays on the content pages (the Playground index, My
configs, the intro). The 60 px breadcrumb bar leaves the app pages (the eyebrow above the title
and the rail's "All configs" carry the place); where the bar holds a control ("Preview without
hardware", the device clause) it moves into the header. The title block (lede, the quote line,
the monitor bar) stays as it is.

**Done - change 25b, 2026-09-28 (the app pages give the bar and the footer's height to the plate).** Outside the GSD
cycle, no plan file; no push, no device, no deploy. Commits: `93f2708` feat(shell) - the header's line and the
footer's strip; `3a07d04` test(e2e); `0e8ca9f` fix(shell) - the chrome switches with the route's declared shape;
`bfaf826` chore(gate) - `QUICK_TESTS` 1139; `9f75977` fix(shell) - the class `in-header`; `87a040f` chore(gate) - the
records `gate/change-25b.*` (at `7d41bff`) and `gate/change-25b-after.*` (at `9f75977`), both on the clean tree; then
this paragraph with `docs/TESTING.md`'s "2026-09-28 change 25b". Every commit `git commit --only` on its paths.

**The switch** is change 25's `fit`, read off the route's declared data: the card workspace and the Sandbox. The
content pages (the Playground index, My configs, the intro) keep the bar and the full footer, unchanged.

**The breadcrumb bar leaves the app pages.** The eyebrow above the title (EXPLORE / SEQUENCING, SANDBOX / MY
PERFORMANCE) and, on a card, the rail's "All configs" carry the place (the Sandbox's rail has no such link; there the
current SANDBOX nav item does). What the bar held moved into the header, whole, as a line of its own right-aligned
under Clear and the connection control, in their register (13px, the quiet ink):
  - on a card: "Preview without hardware" while no module has reported a page; with one, the destination - Target
    (the page select), Store on ZONA, and the lines under them (Store's reason, the over-budget refusal, the switching
    and unverified lines, "Still writing", a failure's block); the dotted device clause ("ZONA connected", "Stored on
    ZONA · Page 3", the uncertain titles) whenever the install store has a phase to state;
  - in the Sandbox: the same, and the draft clause ("Draft saved locally", or the refusal to store) before the device
    clause on the dotted line.
Same components, same testids; only the breadcrumb itself is gone. The row does not move - the wordmark, the nav and
the two controls sit where the 76px band centres them. The line takes the row's width beneath because the row has no
room for it below the wide band (at 1280 the nav ends at 709; the Sandbox's idle pair needs about 330px, a connected
destination about 630); it costs 14px idle over a one-row header, against the bar's 59.

**The footer on the app pages is one strip, 32px plus its rule**: HANGAR / by intech studio, Help & shortcuts ·
Device actions at the left; GPLv3, Third-party notices, Source and the build id at the right; the licence row's 12px;
the build id the only part that gives way (an ellipsis: whole at 1280; cut at 1024 in these builds, beside the
dirty-build note). Every link is kept: the licence links are a GPLv3 obligation (section 6(d): the corresponding
source one click away on every page that ships the code), and `e2e/layout.e2e.ts` now asserts GPLv3, the notices and the source archive on screen on /, /playground/, a
card, the Sandbox and My configs. Help or Device actions opens its panel on a row beneath the strip. Below 1024 the
page scrolls and the footer is the full one.

**Measured, the plate's edge in px, before -> after** (the served build, chromium; every desktop centre's scrollHeight
equals its clientHeight, the document the viewport, nothing over anything):

    viewport     ORBIT      ORBIT,open  AURORA     PINWHEEL+Mirror | Sandbox empty  Edit       Play+lines  Play+Mirror
    1920x1080    518 -> 600  324 -> 404  580 -> 600  481 -> 583     |   556 -> 571    556 -> 571  556 -> 571  508 -> 571
    1440x900     314 -> 447  201 -> 281  376 -> 509  277 -> 379     |   376 -> 485    376 -> 509  376 -> 485  328 -> 430
    1366x768     182 -> 315  122 -> 202  244 -> 377  145 -> 247     |   244 -> 377    244 -> 377  244 -> 377  196 -> 298
    1280x720     134 -> 267   93 -> 173  196 -> 329   97 -> 199     |   196 -> 326    196 -> 329  196 -> 326  129 -> 231
    1024x768     182 -> 315  122 -> 202  244 -> 372   62 -> 164     |   127 -> 127    157 -> 290  127 -> 127   46 -> 127
    393x852      361 -> 361  361 -> 361  361 -> 361  361 -> 361     |   361 -> 361    361 -> 361  361 -> 361  361 -> 361

The chrome was 256px (258 with a ZONA), now 123 (156): 133px back idle, 102 connected. ORBIT at 1280 x 720 is 267, was
134. At 1024 x 768 the Sandbox's empty and Play plates stay 127 - held by the width (the aside beside the plate in a
311px column), not the height. Screenshots before and after, every view above plus the Help panel open and two content
pages: outside the tree, in the session's scratchpad.

**Found on the way and fixed** (`0e8ca9f`): read off the fill, the chrome changed one flush late when a card handed
back to /playground/, which restored its scroll against the card's chrome and landed 45px short (browse.e2e.ts:1404).
Read off the declared data, it switches as the navigation starts.

**Counts, carried + delta** (carried = `7d41bff`'s): quick 101 / 1138 + 1 todo -> **101 / 1139 + 1 todo** (+1,
shell.spec.ts test 8), green twice at `--maxWorkers=2`; `QUICK_TESTS` 1138 -> **1139**; check **692**, 0 errors, 0
warnings; lint clean; e2e runs 128 -> **131** (+3, `e2e/layout.e2e.ts`). By files on a fresh wrangler dev on 4173 at
`9f75977`: c3 **36** (the first run lost its server to a wrangler error, rerun whole), c4 **26**, c5 **11**, c1
**36**, c2 **21 / 1** (the red `:1186`, the recorded hydration family, green alone). **The gate:** the wire set
`193d352f…`, full `bbb8a19b…` and Sandbox set `651664f8…` byte-identical; copy exports equal; testids 357 -> 359
(`header-context`, `shell-header-context`); **utilities 44 -> 44**, none appeared or disappeared; the census moved by
eight literals and the script exits there by design; scoped CSS `37263da0…` -> `a3d8bd43…`; OG and fixtures equal;
`src/` 6 modified, 0 added.

**Departures from the brief:** the zones on a second header line rather than on the row; the full footer below 1024;
the strip's controls 32px at a fine pointer (44 under a coarse one, where the strip grows); panels open beneath the
strip rather than floating; the switch read off the declared data; the Sandbox has no rail "All configs"; e2e pins
moved only where the element moved (five preview-line reads and install's device-clause helper scoped to the header).
CAT-04 stays `[ ]`; STATE / ROADMAP / REQUIREMENTS untouched.
