Vite builds to dist/, deliberately not www/. www/ is still produced by
sync-www.sh for the Phase 1 APK, which stays installable throughout the
port; cutover is a one-line webDir change in capacitor.config.json.
base is relative because Capacitor serves the bundle from the APK, where
absolute asset paths would 404.
index.html omits viewport-fit=cover on purpose. That string is what
makes Capacitor's SystemBars plugin pass insets through to the page;
without it the plugin pads the decor view and the layout is always
between the status and gesture bars. Phase 1 learned this the hard way.
Fonts are self-hosted for offline use, latin subsets only: three files
at 68K, against the 133K Barlow costs today. Space Grotesk is variable,
so one file covers 400-700. Dropped latin-ext and vietnamese.
Carets are drawn as SVG, not typed. U+25BE and U+23EE sit outside every
Latin subset and would fall back to a system font mid-label.
base.css carries the settled palette and the measured vertical budget as
tokens, so a component cannot quietly exceed it:
8 + 44 + 8 + 525 + 8 + 84 + 8 + 50 = 735
Verified: typecheck clean, 103 tests, vite build emits with the fonts
copied, and www/ is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>