Vite + React + TypeScript + Vitest. No router, state library or UI kit.
The 159-row combo table is extracted from reference/index.html by
scripts/extract-reference-data.mjs rather than retyped: it balances
brackets past string literals to slice out the pure-data declarations and
evaluates them in a vm with no DOM. raw.ts and labels.ts are generated.
The extracted count matches the 159 the Phase 1 app reported on device.
Entry.id keeps the reference's format verbatim (shift>p1@S#FIX VEL),
built from the unsplit context string, so drill progress already on the
phone migrates instead of being orphaned.
The domain is pure where the reference was not: boxes go in and new
boxes come out, and the random source is injected. That is what made
card selection testable.
Two bugs found by doing so:
- pickCard returned the card just answered when random() gave exactly 0.
A zero-weight card does not move the accumulator, so "r <= 0" matched
it. The reference has this; at one draw in 2^53 it is invisible in use,
but a pinned random source hits it every time. Fixed to "r < 0".
- boxDistribution incremented through an index that
noUncheckedIndexedAccess types as possibly undefined.
65 tests. Several assert numbers observed on the device: 159 entries,
sample-mode layers of 3 / 35 / 2, a 16-card SHIFT+pads deck.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two chip rows sat flush: the reference spaces them with
'#strip .chips + .chips', but the .lbl between them is display:none on
phones and still in the DOM, so the sibling selector never matched.
Measured gap was 0px, and both selected chips are light-filled and
left-aligned, so they merged into one shape.
- collapse the 7 context chips into one 44px control opening the existing
sheet, whose rows now show CTXHINT (never surfaced on phones before)
- chip targets 34.9px -> 44px; they scroll, so a mis-hit scrolled instead
of tapping
- right-edge fade only on rows that actually overflow (703px and 952px of
content in a 360px viewport, scrollbars hidden)
Applied in native/ so reference/ stays frozen; CLAUDE.md records what the
React port must carry.
scripts/device.sh makes the screen-off renderer denial unmissable: it
verifies the phone is awake, waits for the page to parse rather than for
the activity, and reads the pid after start (adb install -r kills the old
process, which produced one false 'page never loaded').
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wraps reference/sp404-layers as a sideloadable APK without editing the
reference, which stays the frozen spec for the Phase 2 React port.
- sync-www.sh builds www/ from reference/ + native/, asserting each patch
anchor so a reference change fails loudly rather than shipping broken
- drops the service worker (would serve a stale app after an APK update)
- haptics via a navigator.vibrate override, so buzz() needs no change
- localStorage mirrored into Preferences; sync load()/save() untouched
- hardware back: sheet -> Explore tab -> exit
- edge-to-edge handled in MainActivity: Android 16 + targetSdk 36 removes
the opt-out, and a WebView never fills env(safe-area-inset-*), so insets
are pushed into CSS custom properties instead
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>