feat(nexus): design system phase 1 tokens and inter font

First phase of the DESIGN.md (ClickHouse-inspired) migration. Rewrites
the foundation CSS variables and theme machinery; downstream phases
(status/role dictionaries, raw utility sweep) still pending.

index.css
  - Full rewrite of @theme inline block. Dark (.dark) and light (:root)
    token sets per MIGRATION-PLAN sections 3 and 5:
    * Dark: pure black canvas (#000000), Neon Volt primary (#faff69),
      Forest Green secondary (#166534), charcoal border (rgba(65,65,65,
      0.8)), near-black cards (#141414), silver muted (#a0a0a0).
    * Light: near-white canvas (#fafafa), Forest primary, Volt
      downgraded to dark olive (#4f5100) for border/active use only,
      silver inverted to #6b6b6b. Accessibility fallback, not brand.
  - Added --warning (#f59e0b / #b45309), --success, and direct brand
    token refs (--volt, --volt-pale, --volt-border, --forest, --near-
    black, --hover-gray, --silver, --charcoal-border) exposed as
    Tailwind utilities via --color-* mirrors.
  - Added --destructive: #ef4444 (#dc2626 in light).
  - Radius scale collapsed to 4px sharp / 8px comfortable / 9999px pill.
  - Deleted .theme-tokyo-night.dark block entirely (was dead code —
    ThemeContext never applied the class).
  - Rewrote hljs syntax highlighting: one dark block under .dark .hljs
    using volt for keywords, pale volt for strings, silver for
    comments; one light block under .hljs using forest/dark-olive/
    silver. Replaced all three Catppuccin + Tokyo Night hljs rule sets.
  - Rewrote scrollbar rules to use var(--muted) / var(--charcoal-
    border) / var(--hover-gray) instead of hardcoded oklch values.
  - Added @font-face declarations for Inter (normal + italic) from the
    self-hosted woff2 files at /fonts/InterVariable*.woff2. font-weight
    100-900 range unlocks weight 900 for DESIGN.md hero moments from
    a single variable font.
  - Set --font-sans to Inter-first stack; body rule pulls the token.

ThemeContext.tsx
  - Simplified to binary Theme = "light" | "dark". Dropped "custom"
    theme type, PaletteRole interface, ROLE_TO_TOKEN map, and the
    /api/nexus/settings custom-theme hydration effect.
  - applyTheme() now just toggles .dark on <html> and sets
    colorScheme. applyCustomTheme() left as a deprecated no-op (no
    external callers but keeping the export avoids churn).
  - Legacy localStorage values (catppuccin-mocha, tokyo-night, custom,
    catppuccin-latte) coerced to "dark" on read so existing users
    don't see a crash after the migration.
  - Default theme: "dark".

Layout.tsx
  - Dropped THEME_META import and the THEME_CYCLE map. Theme toggle
    is now a binary sun/moon flip via toggleTheme().

index.html
  - Added <link rel="preload" href="/fonts/InterVariable.woff2"
    as="font" type="font/woff2" crossorigin>.
  - Set inline style="background:#000000; color-scheme:dark;" on
    <html> so the pre-React paint is already dark — no white flash.
  - Boot script coerces legacy localStorage theme values and persists
    "light" or "dark" only.

ui/public/fonts/
  - Added InterVariable.woff2 (344 KB) and InterVariable-Italic.woff2
    (379 KB), both Inter v4.x from rsms.me/inter (the canonical
    upstream). Self-hosted for LAN/offline reliability.

Not changed:
  - lib/status-colors.ts, lib/agent-role-colors.ts — next phase
  - Any component files — phase 3
  - MIGRATION-PLAN.md — will be updated with resolved decisions later

Expected visual state: pages using theme tokens (bg-background,
text-muted-foreground, border-border, ~1,250 instances) immediately
render with the new palette. Pages using raw Tailwind utilities
(bg-red-500, text-amber-600, ~274 instances) still show old colors
until phase 3 sweep.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nexus Dev 2026-04-10 17:19:56 +00:00
parent ab45bc063d
commit e49144a4e8
6 changed files with 283 additions and 311 deletions

View file

@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<html lang="en" class="dark" style="background:#000000; color-scheme:dark;">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#1e1e2e" />
<meta name="theme-color" content="#000000" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
@ -18,23 +18,25 @@
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<!-- PAPERCLIP_FAVICON_END -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="preload" as="font" type="font/woff2" href="/fonts/InterVariable.woff2" crossorigin />
<link rel="manifest" href="/site.webmanifest" />
<script>
(() => {
const key = "paperclip.theme";
const VALID = ["catppuccin-mocha", "tokyo-night", "catppuccin-latte"];
const LEGACY_DARK = ["catppuccin-mocha", "tokyo-night", "custom"];
try {
const stored = window.localStorage.getItem(key);
const theme = VALID.includes(stored) ? stored : "catppuccin-mocha";
const isDark = theme !== "catppuccin-latte";
let stored = window.localStorage.getItem(key);
if (stored && stored !== "light" && stored !== "dark") {
// Coerce legacy values. Latte was the old light default; everything else -> dark.
stored = stored === "catppuccin-latte" ? "light" : LEGACY_DARK.includes(stored) ? "dark" : "dark";
try { window.localStorage.setItem(key, stored); } catch {}
}
const theme = stored === "light" ? "light" : "dark";
const isDark = theme === "dark";
document.documentElement.classList.toggle("dark", isDark);
document.documentElement.classList.toggle("theme-tokyo-night", theme === "tokyo-night");
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) {
const bg = { "catppuccin-mocha": "#1e1e2e", "tokyo-night": "#1a1b26", "catppuccin-latte": "#eff1f5" };
meta.setAttribute("content", bg[theme] || "#1e1e2e");
}
if (meta) meta.setAttribute("content", isDark ? "#000000" : "#fafafa");
} catch {
document.documentElement.classList.add("dark");
document.documentElement.style.colorScheme = "dark";

Binary file not shown.

Binary file not shown.

View file

@ -23,7 +23,7 @@ import { usePanel } from "../context/PanelContext";
import { useChatPanel } from "../context/ChatPanelContext";
import { useCompany } from "../context/CompanyContext";
import { useSidebar } from "../context/SidebarContext";
import { useTheme, THEME_META } from "../context/ThemeContext";
import { useTheme } from "../context/ThemeContext";
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
import { healthApi } from "../api/health";
@ -64,12 +64,8 @@ export function Layout() {
setSelectedCompanyId,
} = useCompany();
const { theme, toggleTheme } = useTheme();
const THEME_CYCLE: Record<string, string> = {
"catppuccin-mocha": "Tokyo Night",
"tokyo-night": "Catppuccin Latte",
"catppuccin-latte": "Catppuccin Mocha",
};
const nextThemeLabel = THEME_CYCLE[theme] ?? "next theme";
const isDarkTheme = theme === "dark";
const nextThemeLabel = isDarkTheme ? "Light" : "Dark";
const { companyPrefix } = useParams<{ companyPrefix: string }>();
const navigate = useNavigate();
const location = useLocation();
@ -78,7 +74,6 @@ export function Layout() {
const lastMainScrollTop = useRef(0);
const [mobileNavVisible, setMobileNavVisible] = useState(true);
const [instanceSettingsTarget, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath());
const isDarkTheme = THEME_META[theme].dark;
const matchedCompany = useMemo(() => {
if (!companyPrefix) return null;
const requestedPrefix = companyPrefix.toUpperCase();

View file

@ -8,64 +8,52 @@ import {
type ReactNode,
} from "react";
export interface PaletteRole {
name: string;
dark: { oklch: string; hex: string; wcagAA: boolean };
light: { oklch: string; hex: string; wcagAA: boolean };
}
export type Theme = "light" | "dark" | "custom";
export type Theme = "light" | "dark";
/** Metadata for each theme — used by Layout, MarkdownBody, InstanceGeneralSettings */
export const THEME_META: Record<Theme, { label: string; dark: boolean; bg: string; primary: string }> = {
dark: { label: "Dark", dark: true, bg: "#18181b", primary: "#a78bfa" },
light: { label: "Light", dark: false, bg: "#ffffff", primary: "#7c3aed" },
custom: { label: "Custom", dark: true, bg: "#18181b", primary: "#a78bfa" },
dark: { label: "Dark", dark: true, bg: "#000000", primary: "#faff69" },
light: { label: "Light", dark: false, bg: "#fafafa", primary: "#166534" },
};
/** Ordered list of themes for display in theme selector UI */
export const ORDERED_THEMES: Theme[] = ["dark", "light", "custom"];
export const ORDERED_THEMES: Theme[] = ["dark", "light"];
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
applyCustomTheme: (palette: PaletteRole[], variant: "dark" | "light") => void;
/** @deprecated Custom themes are no longer supported. No-op retained for backwards compat. */
applyCustomTheme: (...args: unknown[]) => void;
}
const THEME_STORAGE_KEY = "paperclip.theme";
const DARK_THEME_COLOR = "#18181b";
const LIGHT_THEME_COLOR = "#ffffff";
const DARK_THEME_COLOR = "#000000";
const LIGHT_THEME_COLOR = "#fafafa";
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
const ROLE_TO_TOKEN: Record<string, string> = {
background: "--background",
surface: "--card",
overlay: "--secondary",
text: "--foreground",
"accent-1": "--primary",
"accent-2": "--accent",
"accent-3": "--muted",
};
function coerceStoredTheme(raw: string | null): Theme {
if (raw === "light") return "light";
if (raw === "dark") return "dark";
// Legacy values: "catppuccin-latte" was the old light; anything else defaulted to dark.
if (raw === "catppuccin-latte") return "light";
return "dark";
}
function resolveInitialTheme(): Theme {
if (typeof localStorage !== "undefined") {
const stored = localStorage.getItem(THEME_STORAGE_KEY);
if (stored === "dark" || stored === "light" || stored === "custom") return stored;
if (stored !== null) return coerceStoredTheme(stored);
}
if (typeof document !== "undefined") {
return document.documentElement.classList.contains("dark") ? "dark" : "light";
return document.documentElement.classList.contains("dark") ? "dark" : "dark";
}
return "dark";
}
function isDarkTheme(theme: Theme): boolean {
return theme === "dark" || theme === "custom";
}
function applyTheme(theme: Theme) {
if (typeof document === "undefined") return;
const dark = isDarkTheme(theme);
const dark = theme === "dark";
const root = document.documentElement;
root.classList.toggle("dark", dark);
root.style.colorScheme = dark ? "dark" : "light";
@ -78,61 +66,18 @@ function applyTheme(theme: Theme) {
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => resolveInitialTheme());
// On mount: if stored theme is "custom", fetch nexus settings to restore customTheme palette
useEffect(() => {
if (theme !== "custom") return;
if (typeof document === "undefined") return;
fetch("/api/nexus/settings", { credentials: "include" })
.then((res) => res.ok ? res.json() : null)
.then((data: { customTheme?: { palette?: PaletteRole[] } } | null) => {
const palette = data?.customTheme?.palette;
if (!palette || !Array.isArray(palette)) return;
const root = document.documentElement;
palette.forEach((role) => {
const tokenName = ROLE_TO_TOKEN[role.name];
if (!tokenName) return;
// Restore dark variant by default for custom theme
root.style.setProperty(tokenName, role.dark.hex);
});
})
.catch(() => {
// silently ignore - custom theme tokens will simply not be restored on this mount
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // only on mount
const setTheme = useCallback((nextTheme: Theme) => {
setThemeState(nextTheme);
}, []);
const toggleTheme = useCallback(() => {
setThemeState((current) => {
if (isDarkTheme(current)) return "light";
return "dark";
});
setThemeState((current) => (current === "dark" ? "light" : "dark"));
}, []);
const applyCustomTheme = useCallback(
(palette: PaletteRole[], paletteVariant: "dark" | "light") => {
if (typeof document === "undefined") return;
const root = document.documentElement;
palette.forEach((role) => {
const tokenName = ROLE_TO_TOKEN[role.name];
if (!tokenName) return;
const value = paletteVariant === "dark" ? role.dark.hex : role.light.hex;
root.style.setProperty(tokenName, value);
});
setThemeState("custom");
try {
localStorage.setItem(THEME_STORAGE_KEY, "custom");
} catch (_e) {
// ignore write failures
}
},
[],
);
// Deprecated no-op kept so any stale imports don't break the build.
const applyCustomTheme = useCallback((..._args: unknown[]) => {
void _args;
}, []);
useEffect(() => {
applyTheme(theme);

View file

@ -3,6 +3,24 @@
@custom-variant dark (&:is(.dark *));
/* -- Fonts ------------------------------------------------------------- */
@font-face {
font-family: "Inter";
src: url("/fonts/InterVariable.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("/fonts/InterVariable-Italic.woff2") format("woff2");
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
@ -20,6 +38,10 @@
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@ -36,178 +58,199 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0px;
--radius-xl: 0px;
--color-volt: var(--volt);
--color-volt-pale: var(--volt-pale);
--color-volt-border: var(--volt-border);
--color-forest: var(--forest);
--color-forest-dark: var(--forest-dark);
--color-near-black: var(--near-black);
--color-hover-gray: var(--hover-gray);
--color-silver: var(--silver);
--radius-sm: 4px;
--radius-md: 4px;
--radius-lg: 8px;
--radius-xl: 8px;
--radius-full: 9999px;
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-mono: ui-monospace, "SFMono-Regular", Menlo, monospace;
--font-display: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
}
/* -- Light mode (default :root) ---------------------------------------- */
:root {
color-scheme: light;
--radius: 0;
--background: #eff1f5;
--foreground: #4c4f69;
--card: #e6e9ef;
--card-foreground: #4c4f69;
--popover: #e6e9ef;
--popover-foreground: #4c4f69;
--primary: #1e66f5;
--primary-foreground: #eff1f5;
--secondary: #ccd0da;
--secondary-foreground: #4c4f69;
--muted: #ccd0da;
--muted-foreground: #9ca0b0;
--accent: #bcc0cc;
--accent-foreground: #4c4f69;
--destructive: #d20f39;
--destructive-foreground: #eff1f5;
--border: #ccd0da;
--input: #ccd0da;
--ring: #1e66f5;
--chart-1: #1e66f5;
--chart-2: #40a02b;
--chart-3: #8839ef;
--chart-4: #df8e1d;
--chart-5: #d20f39;
--sidebar: #e6e9ef;
--sidebar-foreground: #4c4f69;
--sidebar-primary: #1e66f5;
--sidebar-primary-foreground: #eff1f5;
--sidebar-accent: #ccd0da;
--sidebar-accent-foreground: #4c4f69;
--sidebar-border: #ccd0da;
--sidebar-ring: #1e66f5;
--radius: 8px;
--background: #fafafa;
--foreground: #0a0a0a;
--card: #ffffff;
--card-foreground: #0a0a0a;
--popover: #ffffff;
--popover-foreground: #0a0a0a;
--primary: #166534;
--primary-foreground: #ffffff;
--secondary: #f1f5f1;
--secondary-foreground: #0a0a0a;
--muted: #f4f4f5;
--muted-foreground: #6b6b6b;
--accent: #fafff0;
--accent-foreground: #4f5100;
--destructive: #dc2626;
--destructive-foreground: #ffffff;
--warning: #b45309;
--warning-foreground: #ffffff;
--success: #166534;
--success-foreground: #ffffff;
--border: rgba(20, 20, 20, 0.12);
--input: #ffffff;
--ring: #166534;
--chart-1: #166534;
--chart-2: #4f5100;
--chart-3: #6b6b6b;
--chart-4: #8a8c00;
--chart-5: #dc2626;
--sidebar: #ffffff;
--sidebar-foreground: #6b6b6b;
--sidebar-primary: #166534;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f4f4f5;
--sidebar-accent-foreground: #0a0a0a;
--sidebar-border: rgba(20, 20, 20, 0.12);
--sidebar-ring: #166534;
/* Brand direct refs */
--volt: #4f5100;
--volt-pale: #8a8c00;
--volt-border: #4f5100;
--forest: #166534;
--forest-dark: #14572f;
--near-black: #0a0a0a;
--hover-gray: #e4e4e7;
--silver: #6b6b6b;
--charcoal-border: rgba(20, 20, 20, 0.12);
--charcoal-divider: rgba(20, 20, 20, 0.08);
}
/* -- Dark mode --------------------------------------------------------- */
.dark {
color-scheme: dark;
--background: #1e1e2e;
--foreground: #cdd6f4;
--card: #181825;
--card-foreground: #cdd6f4;
--popover: #181825;
--popover-foreground: #cdd6f4;
--primary: #89b4fa;
--primary-foreground: #1e1e2e;
--secondary: #313244;
--secondary-foreground: #cdd6f4;
--muted: #313244;
--muted-foreground: #6c7086;
--accent: #45475a;
--accent-foreground: #cdd6f4;
--destructive: #f38ba8;
--destructive-foreground: #1e1e2e;
--border: #313244;
--input: #313244;
--ring: #89b4fa;
--chart-1: #89b4fa;
--chart-2: #a6e3a1;
--chart-3: #cba6f7;
--chart-4: #f9e2af;
--chart-5: #f38ba8;
--sidebar: #181825;
--sidebar-foreground: #cdd6f4;
--sidebar-primary: #89b4fa;
--sidebar-primary-foreground: #1e1e2e;
--sidebar-accent: #313244;
--sidebar-accent-foreground: #cdd6f4;
--sidebar-border: #313244;
--sidebar-ring: #89b4fa;
}
.theme-tokyo-night.dark {
--background: #1a1b26;
--foreground: #c0caf5;
--card: #16161e;
--card-foreground: #c0caf5;
--popover: #16161e;
--popover-foreground: #c0caf5;
--primary: #7aa2f7;
--primary-foreground: #1a1b26;
--secondary: #292e42;
--secondary-foreground: #c0caf5;
--muted: #292e42;
--muted-foreground: #565f89;
--accent: #3b4261;
--accent-foreground: #c0caf5;
--destructive: #f7768e;
--destructive-foreground: #1a1b26;
--border: #292e42;
--input: #292e42;
--ring: #7aa2f7;
--chart-1: #7aa2f7;
--chart-2: #9ece6a;
--chart-3: #bb9af7;
--chart-4: #e0af68;
--chart-5: #f7768e;
--sidebar: #16161e;
--sidebar-foreground: #c0caf5;
--sidebar-primary: #7aa2f7;
--sidebar-primary-foreground: #1a1b26;
--sidebar-accent: #292e42;
--sidebar-accent-foreground: #c0caf5;
--sidebar-border: #292e42;
--sidebar-ring: #7aa2f7;
--radius: 8px;
--background: #000000;
--foreground: #ffffff;
--card: #141414;
--card-foreground: #ffffff;
--popover: #141414;
--popover-foreground: #ffffff;
--primary: #faff69;
--primary-foreground: #151515;
--secondary: #166534;
--secondary-foreground: #ffffff;
--muted: #141414;
--muted-foreground: #a0a0a0;
--accent: #141414;
--accent-foreground: #faff69;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--warning: #f59e0b;
--warning-foreground: #151515;
--success: #166534;
--success-foreground: #ffffff;
--border: rgba(65, 65, 65, 0.8);
--input: #141414;
--ring: #faff69;
--chart-1: #faff69;
--chart-2: #166534;
--chart-3: #a0a0a0;
--chart-4: #f4f692;
--chart-5: #ef4444;
--sidebar: #000000;
--sidebar-foreground: #a0a0a0;
--sidebar-primary: #faff69;
--sidebar-primary-foreground: #151515;
--sidebar-accent: #141414;
--sidebar-accent-foreground: #ffffff;
--sidebar-border: rgba(65, 65, 65, 0.8);
--sidebar-ring: #faff69;
/* Brand direct refs */
--volt: #faff69;
--volt-pale: #f4f692;
--volt-border: #4f5100;
--forest: #166534;
--forest-dark: #14572f;
--near-black: #141414;
--hover-gray: #3a3a3a;
--silver: #a0a0a0;
--charcoal-border: rgba(65, 65, 65, 0.8);
--charcoal-divider: #343434;
}
/* -- highlight.js syntax theme overrides (chat code blocks) ------------- */
/* Base hljs reset -- ensure code blocks use our themed variables */
/* Base hljs surface — themed via CSS vars */
.hljs {
background: var(--code-block-bg, hsl(var(--card))) !important;
color: var(--code-block-fg, hsl(var(--card-foreground))) !important;
background: var(--card) !important;
color: var(--foreground) !important;
}
/* Catppuccin Mocha (default dark) */
.dark .hljs { --code-block-bg: #1e1e2e; --code-block-fg: #cdd6f4; }
.dark .hljs-keyword { color: #cba6f7; }
.dark .hljs-string { color: #a6e3a1; }
.dark .hljs-number { color: #fab387; }
.dark .hljs-comment { color: #6c7086; font-style: italic; }
.dark .hljs-function { color: #89b4fa; }
.dark .hljs-title { color: #89b4fa; }
.dark .hljs-built_in { color: #f38ba8; }
.dark .hljs-type { color: #f9e2af; }
.dark .hljs-attr { color: #89dceb; }
.dark .hljs-variable { color: #cdd6f4; }
.dark .hljs-literal { color: #fab387; }
.dark .hljs-meta { color: #f5e0dc; }
.dark .hljs-selector-class { color: #89dceb; }
.dark .hljs-selector-tag { color: #cba6f7; }
/* Light mode hljs (default) */
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-number {
color: #166534;
}
.hljs-string,
.hljs-attr {
color: #4f5100;
}
.hljs-function,
.hljs-title,
.hljs-built_in,
.hljs-variable,
.hljs-type,
.hljs-meta {
color: #0a0a0a;
}
.hljs-comment {
color: #6b6b6b;
font-style: italic;
}
.hljs-deletion,
.hljs-selector-class {
color: #dc2626;
}
/* Tokyo Night */
.theme-tokyo-night .hljs { --code-block-bg: #1a1b26; --code-block-fg: #a9b1d6; }
.theme-tokyo-night .hljs-keyword { color: #bb9af7; }
.theme-tokyo-night .hljs-string { color: #9ece6a; }
.theme-tokyo-night .hljs-number { color: #ff9e64; }
.theme-tokyo-night .hljs-comment { color: #565f89; font-style: italic; }
.theme-tokyo-night .hljs-function { color: #7aa2f7; }
.theme-tokyo-night .hljs-title { color: #7aa2f7; }
.theme-tokyo-night .hljs-built_in { color: #f7768e; }
.theme-tokyo-night .hljs-type { color: #e0af68; }
.theme-tokyo-night .hljs-attr { color: #73daca; }
.theme-tokyo-night .hljs-variable { color: #a9b1d6; }
.theme-tokyo-night .hljs-literal { color: #ff9e64; }
.theme-tokyo-night .hljs-meta { color: #c0caf5; }
.theme-tokyo-night .hljs-selector-class { color: #73daca; }
.theme-tokyo-night .hljs-selector-tag { color: #bb9af7; }
/* Catppuccin Latte (light) */
:root .hljs { --code-block-bg: #eff1f5; --code-block-fg: #4c4f69; }
:root .hljs-keyword { color: #8839ef; }
:root .hljs-string { color: #40a02b; }
:root .hljs-number { color: #fe640b; }
:root .hljs-comment { color: #9ca0b0; font-style: italic; }
:root .hljs-function { color: #1e66f5; }
:root .hljs-title { color: #1e66f5; }
:root .hljs-built_in { color: #d20f39; }
:root .hljs-type { color: #df8e1d; }
:root .hljs-attr { color: #179299; }
:root .hljs-variable { color: #4c4f69; }
:root .hljs-literal { color: #fe640b; }
:root .hljs-meta { color: #dc8a78; }
:root .hljs-selector-class { color: #179299; }
:root .hljs-selector-tag { color: #8839ef; }
/* Dark mode hljs — overrides the light defaults */
.dark .hljs {
background: #141414 !important;
color: #ffffff !important;
}
.dark .hljs-keyword,
.dark .hljs-selector-tag,
.dark .hljs-literal,
.dark .hljs-number,
.dark .hljs-meta {
color: #faff69;
}
.dark .hljs-string,
.dark .hljs-attr,
.dark .hljs-type {
color: #f4f692;
}
.dark .hljs-function,
.dark .hljs-title,
.dark .hljs-built_in,
.dark .hljs-variable,
.dark .hljs-selector-class {
color: #ffffff;
}
.dark .hljs-comment {
color: #a0a0a0;
font-style: italic;
}
.dark .hljs-deletion {
color: #ef4444;
}
@layer base {
* {
@ -219,6 +262,7 @@
}
body {
@apply bg-background text-foreground antialiased;
font-family: var(--font-sans);
height: 100%;
overflow: hidden;
}
@ -259,23 +303,23 @@
}
}
/* Dark mode scrollbars */
.dark *::-webkit-scrollbar {
/* Scrollbars — themed via CSS vars so they follow light/dark. */
*::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.dark *::-webkit-scrollbar-track {
background: oklch(0.205 0 0);
*::-webkit-scrollbar-track {
background: var(--muted);
}
.dark *::-webkit-scrollbar-thumb {
background: oklch(0.4 0 0);
*::-webkit-scrollbar-thumb {
background: var(--charcoal-border);
border-radius: 4px;
}
.dark *::-webkit-scrollbar-thumb:hover {
background: oklch(0.5 0 0);
*::-webkit-scrollbar-thumb:hover {
background: var(--hover-gray);
}
/* Auto-hide scrollbar: always reserves space, thumb visible only on hover */
@ -289,25 +333,14 @@
.scrollbar-auto-hide::-webkit-scrollbar-thumb {
background: transparent !important;
}
/* Light mode scrollbar on hover */
.scrollbar-auto-hide:hover::-webkit-scrollbar-track {
background: oklch(0.92 0 0) !important;
background: var(--muted) !important;
}
.scrollbar-auto-hide:hover::-webkit-scrollbar-thumb {
background: oklch(0.7 0 0) !important;
background: var(--charcoal-border) !important;
}
.scrollbar-auto-hide:hover::-webkit-scrollbar-thumb:hover {
background: oklch(0.6 0 0) !important;
}
/* Dark mode scrollbar on hover */
.dark .scrollbar-auto-hide:hover::-webkit-scrollbar-track {
background: oklch(0.205 0 0) !important;
}
.dark .scrollbar-auto-hide:hover::-webkit-scrollbar-thumb {
background: oklch(0.4 0 0) !important;
}
.dark .scrollbar-auto-hide:hover::-webkit-scrollbar-thumb:hover {
background: oklch(0.5 0 0) !important;
background: var(--hover-gray) !important;
}
/* Expandable dialog transition for max-width changes */
@ -449,14 +482,14 @@
}
.paperclip-mdxeditor-content a:not(.paperclip-mention-chip):not(.paperclip-project-mention-chip) {
color: color-mix(in oklab, var(--foreground) 76%, #0969da 24%);
color: color-mix(in oklab, var(--foreground) 76%, var(--primary) 24%);
text-decoration: underline;
text-underline-offset: 0.15em;
cursor: pointer;
}
.dark .paperclip-mdxeditor-content a:not(.paperclip-mention-chip):not(.paperclip-project-mention-chip) {
color: color-mix(in oklab, var(--foreground) 80%, #58a6ff 20%);
color: var(--volt);
}
.paperclip-mdxeditor-content a.paperclip-mention-chip,
@ -564,57 +597,56 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
}
.paperclip-mdxeditor-content code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-family: var(--font-mono);
font-size: 1em;
}
.paperclip-mdxeditor-content pre {
margin: 0.4rem 0;
padding: 0;
border: 1px solid color-mix(in oklab, var(--foreground) 12%, transparent);
border: 1px solid var(--border);
border-radius: calc(var(--radius) - 3px);
background: #1e1e2e;
color: #cdd6f4;
background: var(--card);
color: var(--card-foreground);
overflow-x: auto;
}
/* Dark theme for CodeMirror code blocks inside the MDXEditor.
Overrides the default cm6-theme-basic-light that MDXEditor bundles. */
/* CodeMirror code blocks inside the MDXEditor — themed via CSS vars. */
.paperclip-mdxeditor .cm-editor {
background-color: #1e1e2e !important;
color: #cdd6f4 !important;
background-color: var(--card) !important;
color: var(--card-foreground) !important;
font-size: 1em;
}
.paperclip-mdxeditor .cm-gutters {
background-color: #181825 !important;
color: #585b70 !important;
border-right: 1px solid #313244 !important;
background-color: var(--muted) !important;
color: var(--muted-foreground) !important;
border-right: 1px solid var(--border) !important;
}
.paperclip-mdxeditor .cm-activeLineGutter {
background-color: #1e1e2e !important;
background-color: var(--card) !important;
}
.paperclip-mdxeditor .cm-activeLine {
background-color: color-mix(in oklab, #cdd6f4 5%, transparent) !important;
background-color: color-mix(in oklab, var(--foreground) 5%, transparent) !important;
}
.paperclip-mdxeditor .cm-cursor,
.paperclip-mdxeditor .cm-dropCursor {
border-left-color: #cdd6f4 !important;
border-left-color: var(--foreground) !important;
}
.paperclip-mdxeditor .cm-selectionBackground {
background-color: color-mix(in oklab, #89b4fa 25%, transparent) !important;
background-color: color-mix(in oklab, var(--primary) 25%, transparent) !important;
}
.paperclip-mdxeditor .cm-focused .cm-selectionBackground {
background-color: color-mix(in oklab, #89b4fa 30%, transparent) !important;
background-color: color-mix(in oklab, var(--primary) 30%, transparent) !important;
}
.paperclip-mdxeditor .cm-content {
caret-color: #cdd6f4;
caret-color: var(--foreground);
}
/* MDXEditor code block language selector show on hover only */
@ -634,9 +666,9 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
.paperclip-mdxeditor-content [class*="_codeMirrorToolbar_"] select,
.paperclip-mdxeditor-content [class*="_codeBlockToolbar_"] select {
background-color: #313244;
color: #cdd6f4;
border-color: #45475a;
background-color: var(--muted);
color: var(--foreground);
border-color: var(--border);
}
.paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:hover [class*="_codeMirrorToolbar_"],
@ -646,21 +678,19 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
opacity: 1;
}
/* Rendered markdown code blocks & inline code (prose/MarkdownBody context).
Dark theme code blocks with compact sizing.
Override prose CSS variables so prose-invert can't revert to defaults. */
/* Rendered markdown code blocks & inline code (prose/MarkdownBody context). */
.paperclip-markdown {
--tw-prose-pre-bg: #1e1e2e;
--tw-prose-pre-code: #cdd6f4;
--tw-prose-invert-pre-bg: #1e1e2e;
--tw-prose-invert-pre-code: #cdd6f4;
--tw-prose-pre-bg: var(--card);
--tw-prose-pre-code: var(--card-foreground);
--tw-prose-invert-pre-bg: var(--card);
--tw-prose-invert-pre-code: var(--card-foreground);
}
.paperclip-markdown pre {
border: 1px solid color-mix(in oklab, var(--foreground) 12%, transparent) !important;
border: 1px solid var(--border) !important;
border-radius: calc(var(--radius) - 3px) !important;
background-color: #1e1e2e !important;
color: #cdd6f4 !important;
background-color: var(--card) !important;
color: var(--card-foreground) !important;
padding: 0.5rem 0.65rem !important;
margin: 0.4rem 0 !important;
font-size: 1em !important;
@ -669,7 +699,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
}
.paperclip-markdown code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-family: var(--font-mono);
font-size: 1em;
}
@ -694,7 +724,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
}
.dark .prose :not(pre) > code {
background-color: #ffffff0f;
background-color: color-mix(in oklab, var(--foreground) 6%, transparent);
}
.paperclip-markdown {
@ -776,7 +806,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
}
.paperclip-markdown a {
color: color-mix(in oklab, var(--foreground) 76%, #0969da 24%);
color: color-mix(in oklab, var(--foreground) 76%, var(--primary) 24%);
text-decoration: underline;
text-underline-offset: 0.15em;
cursor: pointer;
@ -787,7 +817,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
}
.dark .paperclip-markdown a {
color: color-mix(in oklab, var(--foreground) 80%, #58a6ff 20%);
color: var(--volt);
}
.paperclip-markdown blockquote {