diff --git a/apps/journal/app/root.tsx b/apps/journal/app/root.tsx
index dd74a92..e75dabd 100644
--- a/apps/journal/app/root.tsx
+++ b/apps/journal/app/root.tsx
@@ -41,7 +41,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
-
+
{children}
diff --git a/apps/journal/app/styles.css b/apps/journal/app/styles.css
index 2fa790e..8eff5ef 100644
--- a/apps/journal/app/styles.css
+++ b/apps/journal/app/styles.css
@@ -1,4 +1,5 @@
@import "tailwindcss";
+@import "@trails-cool/ui/theme.css";
@keyframes slide {
0% { transform: translateX(-100%); }
diff --git a/apps/journal/package.json b/apps/journal/package.json
index 50e0ea7..659169b 100644
--- a/apps/journal/package.json
+++ b/apps/journal/package.json
@@ -28,6 +28,7 @@
"@trails-cool/i18n": "workspace:*",
"@trails-cool/jobs": "workspace:*",
"@trails-cool/map-core": "workspace:*",
+ "@trails-cool/ui": "workspace:*",
"@trails-cool/sentry-config": "workspace:*",
"@trails-cool/types": "workspace:*",
"drizzle-orm": "catalog:",
diff --git a/apps/planner/app/root.tsx b/apps/planner/app/root.tsx
index b116079..4a670b2 100644
--- a/apps/planner/app/root.tsx
+++ b/apps/planner/app/root.tsx
@@ -20,7 +20,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
-
+
{children}
diff --git a/apps/planner/app/styles.css b/apps/planner/app/styles.css
index 2fa790e..8eff5ef 100644
--- a/apps/planner/app/styles.css
+++ b/apps/planner/app/styles.css
@@ -1,4 +1,5 @@
@import "tailwindcss";
+@import "@trails-cool/ui/theme.css";
@keyframes slide {
0% { transform: translateX(-100%); }
diff --git a/apps/planner/package.json b/apps/planner/package.json
index 5b46319..bf4c80f 100644
--- a/apps/planner/package.json
+++ b/apps/planner/package.json
@@ -28,6 +28,7 @@
"@trails-cool/i18n": "workspace:*",
"@trails-cool/jobs": "workspace:*",
"@trails-cool/map-core": "workspace:*",
+ "@trails-cool/ui": "workspace:*",
"@trails-cool/sentry-config": "workspace:*",
"@trails-cool/types": "workspace:*",
"codemirror": "^6.0.2",
diff --git a/openspec/changes/visual-redesign/tasks.md b/openspec/changes/visual-redesign/tasks.md
index c6ee3c7..40ce4f3 100644
--- a/openspec/changes/visual-redesign/tasks.md
+++ b/openspec/changes/visual-redesign/tasks.md
@@ -1,9 +1,9 @@
## 1. Design Tokens & Typography
-- [ ] 1.1 Add CSS custom properties (`:root` vars) for colors, shadows, borders
-- [ ] 1.2 Extend Tailwind config with project color tokens and font families
-- [ ] 1.3 Add Outfit + Geist Mono fonts (Google Fonts or self-hosted)
-- [ ] 1.4 Update base styles: body background, default text color, font-family
+- [x] 1.1 Add CSS custom properties (`:root` vars) for colors, shadows, borders
+- [x] 1.2 Extend Tailwind config with project color tokens and font families
+- [x] 1.3 Add Outfit + Geist Mono fonts (Google Fonts or self-hosted)
+- [x] 1.4 Update base styles: body background, default text color, font-family
- [ ] 1.5 Update elevation gradient colors in ColoredRoute + ElevationChart to use tokens
## 2. Topbar Redesign
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 0000000..fb62fe5
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@trails-cool/ui",
+ "version": "0.0.1",
+ "type": "module",
+ "exports": {
+ "./theme.css": "./src/theme.css"
+ },
+ "dependencies": {
+ "@fontsource-variable/geist-mono": "catalog:",
+ "@fontsource-variable/outfit": "catalog:"
+ }
+}
diff --git a/packages/ui/src/theme.css b/packages/ui/src/theme.css
new file mode 100644
index 0000000..b3135d4
--- /dev/null
+++ b/packages/ui/src/theme.css
@@ -0,0 +1,71 @@
+/*
+ * trails.cool design tokens — the single source of truth for both apps.
+ *
+ * Direction: "Trail Worn warmth + Nordic Precision lightness" — warm
+ * off-whites, one muted sage-forest accent, earthy overnight tones, Outfit
+ * for text and Geist Mono for stats. Values come from the design exploration
+ * in openspec/changes/visual-redesign/mockup.html.
+ *
+ * Consume the tokens as Tailwind utilities (bg-bg-raised, text-text-md,
+ * text-accent, shadow-sm, font-mono, …) or as raw CSS vars (var(--color-accent),
+ * var(--color-eg-lo), …) where a utility can't reach — e.g. canvas/SVG fills.
+ *
+ * Fonts are self-hosted (privacy-first — no Google Fonts CDN).
+ */
+
+@import "@fontsource-variable/outfit";
+@import "@fontsource-variable/geist-mono";
+
+@theme {
+ /* ── Surface ── */
+ --color-bg: #f5f2eb; /* warm off-white — like uncoated paper */
+ --color-bg-subtle: #edeae1; /* slightly deeper — card backgrounds */
+ --color-bg-raised: #faf8f4; /* topbar, sidebar, elevated surfaces */
+ --color-map-tint: #e4dfd2; /* map background tone */
+
+ /* ── Text ── */
+ --color-text-hi: #1a1916; /* near-black — primary */
+ --color-text-md: #5c5847; /* secondary labels */
+ --color-text-lo: #9a9484; /* tertiary, placeholders */
+ --color-text-inv: #faf8f4; /* on dark surfaces */
+
+ /* ── Accent: one muted sage-forest green ── */
+ --color-accent: #4a6b40; /* primary accent */
+ --color-accent-dim: #6a8b5e; /* softer variant */
+ --color-accent-bg: rgba(74, 107, 64, 0.08);
+ --color-accent-border: rgba(74, 107, 64, 0.2);
+
+ /* ── Overnight / stop: warm amber-brown, not orange ── */
+ --color-stop: #8b6d3a;
+ --color-stop-bg: rgba(139, 109, 58, 0.1);
+ --color-stop-border: rgba(139, 109, 58, 0.25);
+
+ /* ── Danger / no-go ── */
+ --color-nogo: rgba(160, 60, 60, 0.12);
+ --color-nogo-border: rgba(160, 60, 60, 0.3);
+
+ /* ── UI chrome ── */
+ --color-border: #ddd9d0;
+ --color-border-md: #cac6bc;
+
+ /* ── Elevation gradient (route line + chart) ── */
+ --color-eg-lo: #5a8f46;
+ --color-eg-mid: #c4a840;
+ --color-eg-hi: #c46040;
+
+ /* ── Shadows ── */
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.05);
+
+ /* ── Typography ── */
+ --font-sans: "Outfit Variable", "Outfit", ui-sans-serif, system-ui, sans-serif;
+ --font-mono: "Geist Mono Variable", ui-monospace, "SF Mono", monospace;
+}
+
+@layer base {
+ body {
+ background-color: var(--color-bg);
+ color: var(--color-text-hi);
+ font-family: var(--font-sans);
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6db6201..33e60ef 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,6 +6,12 @@ settings:
catalogs:
default:
+ '@fontsource-variable/geist-mono':
+ specifier: ^5.2.6
+ version: 5.2.8
+ '@fontsource-variable/outfit':
+ specifier: ^5.2.7
+ version: 5.2.8
'@react-router/dev':
specifier: ^7.17.0
version: 7.17.0
@@ -257,6 +263,9 @@ importers:
'@trails-cool/types':
specifier: workspace:*
version: link:../../packages/types
+ '@trails-cool/ui':
+ specifier: workspace:*
+ version: link:../../packages/ui
drizzle-orm:
specifier: 'catalog:'
version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(expo-sqlite@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.7))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.7))(react@19.2.7))(pg@8.22.0)(postgres@3.4.9)
@@ -508,6 +517,9 @@ importers:
'@trails-cool/types':
specifier: workspace:*
version: link:../../packages/types
+ '@trails-cool/ui':
+ specifier: workspace:*
+ version: link:../../packages/ui
codemirror:
specifier: ^6.0.2
version: 6.0.2
@@ -671,6 +683,15 @@ importers:
packages/types: {}
+ packages/ui:
+ dependencies:
+ '@fontsource-variable/geist-mono':
+ specifier: 'catalog:'
+ version: 5.2.8
+ '@fontsource-variable/outfit':
+ specifier: 'catalog:'
+ version: 5.2.8
+
scripts:
devDependencies:
'@types/node':
@@ -1989,6 +2010,12 @@ packages:
engines: {node: '>=20.19.0'}
hasBin: true
+ '@fontsource-variable/geist-mono@5.2.8':
+ resolution: {integrity: sha512-KI5bj+hkkRiHttYHmccotUZ80ZuZyai+RwI1d7UId0clkx/jXxlo8qYK8j54WzmpBjtMoEMPyllV7faDcj+6RA==}
+
+ '@fontsource-variable/outfit@5.2.8':
+ resolution: {integrity: sha512-4oUDCZx/Tcz6HZP423w/niqEH31Gks5IsqHV2ZZz1qKHaVIZdj2f0/S1IK2n8jl6Xo0o3N+3RjNHlV9R73ozQA==}
+
'@garmin/fitsdk@21.208.0':
resolution: {integrity: sha512-mEmPHyzqaS3bb8n+yaOmgmBo/0CUrsLsN5J4viAUN5Kh6+YiwdvPh5OAzb2Jgy2IaI23q7mGtNr/8WRJC1+HoA==}
@@ -9545,6 +9572,10 @@ snapshots:
- '@types/node'
- rxjs
+ '@fontsource-variable/geist-mono@5.2.8': {}
+
+ '@fontsource-variable/outfit@5.2.8': {}
+
'@garmin/fitsdk@21.208.0': {}
'@geoman-io/leaflet-geoman-free@2.20.0(leaflet@1.9.4)':
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 5dde4fc..4b973a7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -4,6 +4,8 @@ packages:
- "scripts"
catalog:
+ "@fontsource-variable/geist-mono": ^5.2.6
+ "@fontsource-variable/outfit": ^5.2.7
"@react-router/dev": ^7.17.0
"@react-router/node": ^7.17.0
"@react-router/serve": ^7.17.0