diff --git a/bun.lock b/bun.lock index 944cf0c..aa2d464 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,9 @@ "": { "name": "ttrpg-manager", "dependencies": { + "@fontsource-variable/hanken-grotesk": "^5.2.8", + "@fontsource-variable/spline-sans-mono": "^5.2.8", + "@fontsource/spectral": "^5.2.8", "@tanstack/react-router": "^1.95.0", "@tanstack/react-virtual": "^3.14.2", "clsx": "^2.1.1", @@ -345,6 +348,12 @@ "@fastify/websocket": ["@fastify/websocket@11.2.0", "", { "dependencies": { "duplexify": "^4.1.3", "fastify-plugin": "^5.0.0", "ws": "^8.16.0" } }, "sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w=="], + "@fontsource-variable/hanken-grotesk": ["@fontsource-variable/hanken-grotesk@5.2.8", "", {}, "sha512-K7hu09ZKReBc7EifRLeM4K94NZBrxFEg0nGcISmVwlFQOJo4EmYkLXTWEwr5JcNW4z2hr5zy8vLS2sxC8JDmrQ=="], + + "@fontsource-variable/spline-sans-mono": ["@fontsource-variable/spline-sans-mono@5.2.8", "", {}, "sha512-Z1yYiEtP+8Gll5BJaR9dICK/ZTxoTWH+ZBH6rOTMWajPSOxwl7IZplkwX2J1QhMxqulhhdMrkRx6SkkLM+jLQw=="], + + "@fontsource/spectral": ["@fontsource/spectral@5.2.8", "", {}, "sha512-0CqZ8/z8A38BdeSz5t2NSd5tCO1R9P1k7SF2VpRLMbZTMSSR6+2Dk0ZBMfEQK/IrBLQS1HgHLMqpdTwRpnKpGQ=="], + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], diff --git a/package.json b/package.json index 4edd976..4c34288 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,9 @@ "data:build": "tsx scripts/build-srd.ts" }, "dependencies": { + "@fontsource-variable/hanken-grotesk": "^5.2.8", + "@fontsource-variable/spline-sans-mono": "^5.2.8", + "@fontsource/spectral": "^5.2.8", "@tanstack/react-router": "^1.95.0", "@tanstack/react-virtual": "^3.14.2", "clsx": "^2.1.1", diff --git a/src/main.tsx b/src/main.tsx index a4cedb9..3c1c805 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,15 @@ import { createRoot } from 'react-dom/client'; import { RouterProvider } from '@tanstack/react-router'; import { router } from './router'; import { useUiStore, applyTheme } from './stores/uiStore'; +// "The Living Codex" type system (self-hosted, offline-safe): Spectral (editorial +// serif headings), Hanken Grotesk (UI), Spline Sans Mono (stats/dice). +import '@fontsource/spectral/400.css'; +import '@fontsource/spectral/500.css'; +import '@fontsource/spectral/600.css'; +import '@fontsource/spectral/700.css'; +import '@fontsource/spectral/400-italic.css'; +import '@fontsource-variable/hanken-grotesk/index.css'; +import '@fontsource-variable/spline-sans-mono/index.css'; import './styles/globals.css'; // Apply persisted theme before first paint. diff --git a/src/styles/globals.css b/src/styles/globals.css index 1dce3ff..58269bf 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -3,56 +3,106 @@ /* Class-based dark mode: toggling `.dark` on swaps the token values below. */ @custom-variant dark (&:where(.dark, .dark *)); -/* ---- Semantic design tokens (light defaults) ---- */ +/* ============================================================================ + THE LIVING CODEX — design tokens. Parchment + gilt (light) · candlelit (dark). + Every --app-* name components already use is kept; values are swapped and a few + new tokens added. Retheme the entire accent by changing --app-accent-hue + (73 gilt · 38 ember · 12 oxblood · 285 arcane · 178 verdigris · 150 forest). + ============================================================================ */ + +/* ---- Light: aged parchment ---- */ :root { - --app-surface: #f5f2ec; /* page background */ - --app-panel: #fffdf9; /* cards / panels */ - --app-elevated: #ece6da; /* raised controls */ - --app-line: #d8cfbe; /* borders */ - --app-ink: #232018; /* primary text */ - --app-muted: #6b6457; /* secondary text */ - --app-accent: #9a6b1f; /* brand (deep gold) */ - --app-accent-soft: #c79a4a; - --app-accent-ink: #fffdf9; /* text on accent */ - --app-danger: #b3261e; - --app-success: #2e7d32; - --app-warning: #b06f00; - --app-info: #1565c0; + --app-surface: oklch(0.945 0.018 84); + --app-surface-2: oklch(0.915 0.020 82); /* sunken wells */ + --app-panel: oklch(0.972 0.013 88); + --app-panel-2: oklch(0.955 0.015 86); + --app-elevated: oklch(0.900 0.022 80); + --app-line: oklch(0.855 0.025 78); + --app-line-strong:oklch(0.760 0.030 76); + --app-ink: oklch(0.280 0.022 64); + --app-ink-soft: oklch(0.400 0.020 66); /* body copy */ + --app-muted: oklch(0.520 0.020 70); + --app-faint: oklch(0.620 0.018 74); /* tertiary */ + + --app-accent: oklch(0.585 0.098 var(--app-accent-hue)); + --app-accent-deep: oklch(0.500 0.090 calc(var(--app-accent-hue) - 5)); + --app-accent-soft: oklch(0.680 0.110 calc(var(--app-accent-hue) + 5)); + --app-accent-ink: oklch(0.985 0.010 88); + --app-accent-glow: oklch(0.585 0.098 var(--app-accent-hue) / 0.16); + --app-accent-hue: 73; /* change this ONE number to retheme the accent */ + + --app-verdigris: oklch(0.540 0.058 178); + --app-danger: oklch(0.520 0.150 32); + --app-danger-glow:oklch(0.520 0.150 32 / 0.16); + --app-success: oklch(0.560 0.085 150); + --app-warning: oklch(0.560 0.110 70); + --app-info: oklch(0.500 0.095 285); + + --app-paper-mul: 1; /* set 0 to disable the paper grain */ + --paper-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E"); + + color-scheme: light; } +/* ---- Dark: candlelit ink ---- */ .dark { - --app-surface: #0f0f17; - --app-panel: #181826; - --app-elevated: #21212f; - --app-line: #2e2e40; - --app-ink: #e9e7ef; - --app-muted: #9a98ab; - --app-accent: #d4af37; /* gold */ - --app-accent-soft: #b08d2e; - --app-accent-ink: #15151f; - --app-danger: #ef5350; - --app-success: #66bb6a; - --app-warning: #ffb74d; - --app-info: #64b5f6; + --app-surface: oklch(0.185 0.012 66); + --app-surface-2: oklch(0.150 0.012 66); + --app-panel: oklch(0.225 0.014 64); + --app-panel-2: oklch(0.255 0.015 62); + --app-elevated: oklch(0.290 0.016 60); + --app-line: oklch(0.330 0.018 58); + --app-line-strong:oklch(0.420 0.022 56); + --app-ink: oklch(0.925 0.016 82); + --app-ink-soft: oklch(0.820 0.016 80); + --app-muted: oklch(0.660 0.018 76); + --app-faint: oklch(0.520 0.018 72); + + --app-accent: oklch(0.795 0.115 var(--app-accent-hue)); + --app-accent-deep: oklch(0.700 0.105 calc(var(--app-accent-hue) - 4)); + --app-accent-soft: oklch(0.860 0.120 calc(var(--app-accent-hue) + 3)); + --app-accent-ink: oklch(0.180 0.020 60); + --app-accent-glow: oklch(0.795 0.115 var(--app-accent-hue) / 0.18); + --app-accent-hue: 82; + + --app-verdigris: oklch(0.700 0.070 178); + --app-danger: oklch(0.640 0.155 36); + --app-danger-glow:oklch(0.640 0.155 36 / 0.20); + --app-success: oklch(0.700 0.095 152); + --app-warning: oklch(0.780 0.120 75); + --app-info: oklch(0.700 0.105 288); + + color-scheme: dark; } -/* Expose tokens as Tailwind color utilities: bg-surface, text-ink, border-line, etc. */ +/* Expose tokens as Tailwind utilities: bg-surface, text-faint, border-line-strong, bg-accent-glow… */ @theme inline { --color-surface: var(--app-surface); + --color-surface-2: var(--app-surface-2); --color-panel: var(--app-panel); + --color-panel-2: var(--app-panel-2); --color-elevated: var(--app-elevated); --color-line: var(--app-line); + --color-line-strong: var(--app-line-strong); --color-ink: var(--app-ink); + --color-ink-soft: var(--app-ink-soft); --color-muted: var(--app-muted); + --color-faint: var(--app-faint); --color-accent: var(--app-accent); + --color-accent-deep: var(--app-accent-deep); --color-accent-soft: var(--app-accent-soft); --color-accent-ink: var(--app-accent-ink); + --color-accent-glow: var(--app-accent-glow); + --color-verdigris: var(--app-verdigris); --color-danger: var(--app-danger); + --color-danger-glow: var(--app-danger-glow); --color-success: var(--app-success); --color-warning: var(--app-warning); --color-info: var(--app-info); - --font-display: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif; + --font-display: 'Spectral', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif; + --font-sans: 'Hanken Grotesk Variable', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'Spline Sans Mono Variable', ui-monospace, 'SF Mono', Menlo, monospace; } html, @@ -65,10 +115,30 @@ body { margin: 0; background: var(--app-surface); color: var(--app-ink); - font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + font-family: var(--font-sans); -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } +/* ---- Pressed-paper grain. Put class="paper-grain" on any positioned panel. ---- */ +.paper-grain { position: relative; } +.paper-grain::before { + content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none; + background-image: var(--paper-url); + background-size: 240px 240px; + opacity: calc(0.30 * var(--app-paper-mul)); +} +.dark .paper-grain::before { opacity: calc(0.42 * var(--app-paper-mul)); } +.paper-grain > * { position: relative; z-index: 1; } + +/* ---- Gilt hairline rule ---- */ +.gilt-rule { height: 1px; border: 0; opacity: .5; + background: linear-gradient(90deg, transparent, var(--app-accent) 18%, var(--app-accent) 82%, transparent); } + +/* ---- Small-caps section label ---- */ +.smallcaps { font-family: var(--font-sans); text-transform: uppercase; + letter-spacing: .16em; font-size: 11px; font-weight: 600; color: var(--app-muted); } + /* Reduced-motion friendliness */ @media (prefers-reduced-motion: reduce) { *,