diff --git a/apps/desktop/src/renderer/src/main.tsx b/apps/desktop/src/renderer/src/main.tsx
index a4e610e1..87caad2e 100644
--- a/apps/desktop/src/renderer/src/main.tsx
+++ b/apps/desktop/src/renderer/src/main.tsx
@@ -1,6 +1,7 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
+import "@multica/ui/fonts"
import "@multica/ui/globals.css"
ReactDOM.createRoot(document.getElementById('root')!).render(
diff --git a/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx
index 824333c1..c779cf22 100644
--- a/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx
+++ b/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx
@@ -27,7 +27,7 @@ export default function WelcomeStep({ onStart }: WelcomeStepProps) {
{/* Brand Title */}
-
+
Welcome to Multica
diff --git a/apps/web/app/header.tsx b/apps/web/app/header.tsx
index dee519d6..6c77f1b1 100644
--- a/apps/web/app/header.tsx
+++ b/apps/web/app/header.tsx
@@ -7,7 +7,7 @@ export function Header() {

-
+
Multica
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 020bbdc6..69ea8aae 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,28 +1,10 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono, Inter, Playfair_Display } from "next/font/google";
+import "@multica/ui/fonts";
import "@multica/ui/globals.css";
import { ThemeProvider } from "@multica/ui/components/theme-provider";
import { Toaster } from "@multica/ui/components/ui/sonner";
import { ServiceWorkerRegister } from "./sw-register";
-const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
-
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});
-
-const playfair = Playfair_Display({
- variable: "--font-brand",
- subsets: ["latin"],
- weight: ["400"],
-});
-
export const metadata: Metadata = {
title: "Multica",
description: "Distributed AI agent framework",
@@ -42,10 +24,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
-
+
+
-
+
Multica
diff --git a/packages/ui/src/styles/fonts.ts b/packages/ui/src/styles/fonts.ts
new file mode 100644
index 00000000..a9694b2b
--- /dev/null
+++ b/packages/ui/src/styles/fonts.ts
@@ -0,0 +1,17 @@
+// Unified font imports for Web and Desktop
+// Using fontsource for consistent cross-platform font loading
+
+// Geist Sans - Primary UI font
+import '@fontsource/geist-sans/400.css'
+import '@fontsource/geist-sans/500.css'
+import '@fontsource/geist-sans/600.css'
+import '@fontsource/geist-sans/700.css'
+
+// Geist Mono - Code font
+import '@fontsource/geist-mono/400.css'
+import '@fontsource/geist-mono/500.css'
+import '@fontsource/geist-mono/600.css'
+import '@fontsource/geist-mono/700.css'
+
+// Playfair Display - Brand font
+import '@fontsource-variable/playfair-display'
diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css
index aa693af3..98081a60 100644
--- a/packages/ui/src/styles/globals.css
+++ b/packages/ui/src/styles/globals.css
@@ -1,3 +1,86 @@
+/**
+ * =============================================================================
+ * MULTICA DESIGN SYSTEM
+ * =============================================================================
+ *
+ * Design Philosophy:
+ * - Restrained & Professional: This is a work tool, not a consumer app
+ * - Clear Communication: UI should inform, not decorate
+ * - Trust Through Transparency: Users need to understand what the AI is doing
+ *
+ * Key Principles:
+ * 1. RESTRAINT over decoration — avoid flashy colors, excessive animations
+ * 2. CLARITY over cleverness — obvious > subtle, explicit > implicit
+ * 3. CONSISTENCY over novelty — use established patterns (Shadcn/UI)
+ * 4. DENSITY over sprawl — respect user's screen real estate
+ *
+ * =============================================================================
+ * TYPOGRAPHY
+ * =============================================================================
+ *
+ * Font Stack (loaded via @fontsource, works across Web + Desktop):
+ *
+ * | Font | Variable | Usage |
+ * |---------------------------|---------------|------------------------------|
+ * | Geist Sans | font-sans | Primary UI text, body copy |
+ * | Geist Mono | font-mono | Code, technical values |
+ * | Playfair Display Variable | font-brand | Brand name "Multica" only |
+ *
+ * Why Geist?
+ * - Created by Vercel, optimized for UI/developer tools
+ * - Excellent legibility at small sizes (12-14px)
+ * - Neutral, professional appearance — not "AI-ish" or trendy
+ * - Variable font = smaller bundle, flexible weights
+ *
+ * Why Playfair Display for brand?
+ * - Contrast with Geist creates clear hierarchy
+ * - Serif adds warmth/personality to otherwise minimal UI
+ * - Used ONLY for "Multica" text — nowhere else
+ *
+ * =============================================================================
+ * COLOR SYSTEM
+ * =============================================================================
+ *
+ * Approach: Neutral grays + semantic colors only
+ *
+ * Base Palette (OKLCH for perceptual uniformity):
+ * - background/foreground: Near-black and near-white, minimal chroma
+ * - muted: Subtle gray for secondary elements
+ * - primary: Near-black (light mode), near-white (dark mode) — NOT a brand color
+ *
+ * Why no brand color?
+ * - Purple/blue "AI colors" feel generic and dated
+ * - Work tools should recede, not demand attention
+ * - Color is reserved for STATE (running/success/error), not decoration
+ *
+ * Semantic Colors:
+ * - destructive: Red — delete, danger, errors
+ * - tool-running: Blue — active/in-progress state
+ * - tool-success: Green — completed successfully
+ * - tool-error: Red/orange — failed state
+ *
+ * Dark Mode:
+ * - True dark (#0f0f10), not gray
+ * - Slightly elevated surfaces for cards/popovers
+ * - Borders use subtle transparency (oklch with alpha)
+ *
+ * =============================================================================
+ * COMPONENT LIBRARY
+ * =============================================================================
+ *
+ * Built on: Shadcn/UI (https://ui.shadcn.com)
+ * - Copy-paste components, not a dependency
+ * - Radix UI primitives for accessibility
+ * - Tailwind CSS v4 for styling
+ *
+ * Customizations:
+ * - Slightly smaller radius (0.625rem base)
+ * - Muted color accents (no vibrant primary)
+ * - Custom scrollbar styling
+ *
+ * =============================================================================
+ */
+
@import "tailwindcss";
@source "../../../apps/**/*.{ts,tsx}";
@source "../**/*.{ts,tsx}";
@@ -10,8 +93,9 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
- --font-sans: var(--font-sans);
- --font-mono: var(--font-geist-mono);
+ --font-sans: 'Geist Sans', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'Geist Mono', ui-monospace, monospace;
+ --font-brand: 'Playfair Display Variable', serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -51,6 +135,21 @@
}
:root {
+ /* =========================================================================
+ * FONTS — unified across Web (Next.js) and Desktop (Electron/Vite)
+ * Loaded via @fontsource packages in packages/ui/src/styles/fonts.ts
+ * ========================================================================= */
+ --font-sans: 'Geist Sans', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'Geist Mono', ui-monospace, monospace;
+ --font-brand: 'Playfair Display Variable', serif;
+
+ /* =========================================================================
+ * COLORS — Light mode
+ * Using OKLCH for perceptual uniformity across the palette
+ * Hue ~286 (cool gray with slight purple undertone) for neutrals
+ * ========================================================================= */
+
+ /* Base: pure white bg, near-black text */
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
@@ -83,15 +182,23 @@
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
+ /* Scrollbar: subtle, non-distracting */
--scrollbar-thumb: oklch(0.82 0.003 286);
--scrollbar-thumb-hover: oklch(0.705 0.015 286.067);
--scrollbar-track: transparent;
- --tool-running: oklch(0.6 0.18 250);
- --tool-success: oklch(0.72 0.12 145);
- --tool-error: oklch(0.65 0.2 25);
+
+ /* Tool execution states — these ARE allowed to use color for clarity */
+ --tool-running: oklch(0.6 0.18 250); /* Blue: active/in-progress */
+ --tool-success: oklch(0.72 0.12 145); /* Green: completed */
+ --tool-error: oklch(0.65 0.2 25); /* Red: failed */
}
+/* =========================================================================
+ * COLORS — Dark mode
+ * True dark theme (not gray), elevated surfaces for depth
+ * ========================================================================= */
.dark {
+ /* Base: near-black bg, near-white text */
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9f1db6aa..c136db71 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -80,13 +80,13 @@ importers:
dependencies:
'@mariozechner/pi-agent-core':
specifier: 'catalog:'
- version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
+ version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
'@mariozechner/pi-ai':
specifier: 'catalog:'
- version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
+ version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
'@mariozechner/pi-coding-agent':
specifier: 'catalog:'
- version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
+ version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
'@mozilla/readability':
specifier: ^0.6.0
version: 0.6.0
@@ -597,13 +597,13 @@ importers:
dependencies:
'@mariozechner/pi-agent-core':
specifier: 'catalog:'
- version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
+ version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai':
specifier: 'catalog:'
- version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
+ version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-coding-agent':
specifier: 'catalog:'
- version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
+ version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@multica/types':
specifier: workspace:*
version: link:../types
@@ -731,6 +731,15 @@ importers:
'@base-ui/react':
specifier: ^1.1.0
version: 1.1.0(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@fontsource-variable/playfair-display':
+ specifier: ^5.2.8
+ version: 5.2.8
+ '@fontsource/geist-mono':
+ specifier: ^5.2.7
+ version: 5.2.7
+ '@fontsource/geist-sans':
+ specifier: ^5.2.5
+ version: 5.2.5
'@hugeicons/core-free-icons':
specifier: 'catalog:'
version: 3.1.1
@@ -2234,6 +2243,15 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+ '@fontsource-variable/playfair-display@5.2.8':
+ resolution: {integrity: sha512-ZzVIXPOrL85yyOvZYoBzUszIJM+xKkHqni4IYn2CVLaGQQdJR8sBeC8yFNgjxSJ7ludTwta8qpULeOFuk5X75A==}
+
+ '@fontsource/geist-mono@5.2.7':
+ resolution: {integrity: sha512-xVPVFISJg/K0VVd+aQN0Y7X/sw9hUcJPyDWFJ5GpyU3bHELhoRsJkPSRSHXW32mOi0xZCUQDOaPj1sqIFJ1FGg==}
+
+ '@fontsource/geist-sans@5.2.5':
+ resolution: {integrity: sha512-anllOHyJbElRs9fV15TeDRqAeb1IKm4bSknPl6ZMoyPTx1BBy7logudcUwpNjmQLkzn4Q0JGQLRCUKJYoyST6A==}
+
'@google/genai@1.40.0':
resolution: {integrity: sha512-fhIww8smT0QYRX78qWOiz/nIQhHMF5wXOrlXvj33HBrz3vKDBb+wibLcEmTA+L9dmPD4KmfNr7UF3LDQVTXNjA==}
engines: {node: '>=20.0.0'}
@@ -12029,6 +12047,12 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
+ '@fontsource-variable/playfair-display@5.2.8': {}
+
+ '@fontsource/geist-mono@5.2.7': {}
+
+ '@fontsource/geist-sans@5.2.5': {}
+
'@google/genai@1.40.0(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))':
dependencies:
google-auth-library: 10.5.0
@@ -15933,7 +15957,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -15964,7 +15988,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3