Phase 2: React scaffolding, self-hosted fonts, design tokens

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>
This commit is contained in:
2026-09-17 01:05:09 +03:00
parent ad847faa2e
commit 956dc57763
13 changed files with 369 additions and 5 deletions

102
src/styles/base.css Normal file
View File

@@ -0,0 +1,102 @@
/* Design tokens and reset. The visual spec lives in CLAUDE.md. */
/* Latin subsets only. Space Grotesk is variable, so one file covers 400-700. */
@font-face {
font-family: "Space Grotesk";
font-weight: 400 700;
font-style: normal;
font-display: swap;
src: url("/fonts/space-grotesk-var-latin.woff2") format("woff2");
}
@font-face {
font-family: "Space Mono";
font-weight: 400;
font-style: normal;
font-display: swap;
src: url("/fonts/space-mono-400-latin.woff2") format("woff2");
}
@font-face {
font-family: "Space Mono";
font-weight: 700;
font-style: normal;
font-display: swap;
src: url("/fonts/space-mono-700-latin.woff2") format("woff2");
}
:root {
--font: "Space Grotesk", system-ui, sans-serif;
--mono: "Space Mono", ui-monospace, monospace;
/* Direction A, manual. */
--page: #e9e9e6;
--body: #d2d2ce;
--card: #f5f5f2;
--ink: #111111;
--muted: #6e6e69;
--rule: #b5b5b0;
--key: #f5f5f2;
--pad: #1b1b1b;
--pad-ink: #ededeb;
--lcd: #c9d2c4;
/* One accent, one meaning: this control is live. */
--accent: #ff4a00;
--on-accent: #111111;
--outline: 2px solid var(--ink);
--shadow: 4px 4px 0 var(--ink);
--shadow-sm: 3px 3px 0 var(--ink);
/* The measured vertical budget at 360x735. Nothing may exceed it. */
--gap: 8px;
--strip-h: 44px;
--panel-h: 525px;
--info-h: 84px;
--nav-h: 50px;
}
@media (prefers-color-scheme: dark) {
:root {
--page: #1a1a17;
--body: #2e2e2a;
--card: #2e2e2a;
--ink: #ededeb;
--muted: #8a8a85;
--rule: #4a4a46;
--key: #3e3e3a;
--pad: #0e0e0c;
--outline: 2px solid #000000;
--shadow: 5px 5px 0 #000000;
--shadow-sm: 3px 3px 0 #000000;
}
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html,
body {
margin: 0;
overscroll-behavior: none;
}
body {
background: var(--page);
color: var(--ink);
font-family: var(--font);
font-size: 16px;
line-height: 1.35;
}
button {
font: inherit;
color: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}