The dark palette lives as a single :root[data-theme='dark'] block in
tokens.css; theme.ts and the pre-paint public/theme-init.js (external file
because the prod CSP forbids inline scripts) always resolve the stored
ui.theme.mode to a concrete data-theme, so 'system' needs no @media
duplicate and follows live OS changes via matchMedia. color-scheme flips
per theme (native controls/scrollbars), paired theme-color metas track the
effective theme, and the new Appearance settings section offers the
three-way choice as native radios (device-local, like #170). Label chips
gain a chip-outline ring so arbitrary user colors stay separated on the
dark canvas; useEffectiveTheme() is exported for the later pond-scoped
theming stage (ADR 0018).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
Self-hosted Google Fonts with per-pond selection (ADR 0016), the GDPR
"zero external requests" posture (security.md, CSP `font-src 'self'`).
- Catalog: a curated 15-family OFL/Apache list in shared (family, weights,
category, license, google-webfonts-helper id). `deploy/fonts/build-fonts.mjs`
validates every entry has license info (fails the build otherwise),
downloads the WOFF2 weights into apps/web/public/fonts/ (gitignored), and
generates the @font-face stylesheet — run at image build time from the web
Dockerfile (with retries), never from a visitor's browser.
- Application: PondFontScope sets --font-heading/body/mono (+ weights) from
pond.settings.fonts on the editor + read view; the existing global CSS
already reads those custom properties, so headings/body/code re-resolve to
the pond's fonts. A pond with no settings arrives with the defaulted values
(Roboto 400 / Roboto 200 / Fira Code), so the vision defaults always render.
- Admin UI: pond-settings 'Appearance' section — three slots (family + weight)
with a live preview, Pond-Admin-gated (fonts added to updatePondInputSchema
and merged in PondsService.update); a font catalog attribution page (/fonts)
listing families and licenses. New `font` i18n namespace (de+en).
- CSP: strict Content-Security-Policy in nginx.conf (default-src 'self';
font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; …) —
the app's scripts are all external files, inline styles cover CSS variables.
- Tests: shared catalog-integrity unit test (the invariant the build enforces);
e2e fonts pack — no request leaves the origin when rendering a pond (the GDPR
network assertion), a font choice applies to a page and persists, and a pond
without settings renders the defaults.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
apps/web becomes a Vite + React application: React Router with home
and 404 routes, base layout (top bar, collapsible sidebar remembered
per user via localStorage, main area), CSS design tokens including the
three font slots from ADR 0016, TanStack Query, and a typed fetch
helper showing live API health on the home page. All UI strings go
through a t() stub that issue #5 replaces with i18next. The Vite dev
server proxies /api to the api dev port (3001).
Closes#4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>