# Phase 2: Theme System - Research

**Researched:** 2026-02-27
**Domain:** CSS custom property token architecture, Tailwind CSS v4 theming, font bundling, runtime theme switching via Tauri IPC
**Confidence:** HIGH

## Summary

Phase 2 establishes the CSS custom property token system that every subsequent phase depends on for visual styling. The research confirms that Tailwind CSS v4's `@theme` directive natively supports CSS custom property design tokens, but with an important nuance: `@theme` blocks cannot be scoped to `data-theme` selectors. The correct pattern is to define semantic token names in `@theme inline` (which generates Tailwind utility classes), then override the underlying CSS variables in `[data-theme="x"]` selectors within `@layer base`. This was confirmed by Tailwind CSS maintainer Adam Wathan in GitHub Discussion #16292.

The two-tier token system (primitive + semantic) maps cleanly to this pattern: primitives are raw CSS variables defined in `:root`, semantic tokens are `@theme inline` declarations that reference primitives via `var()`, and theme switching redefines the primitives under `[data-theme="cyberpunk"]` selectors. The browser resolves `var()` references live, so changing the `data-theme` attribute on `<html>` instantly re-skins all themed elements with zero JavaScript re-renders -- this is native CSS cascade behavior.

Font bundling uses Fontsource npm packages (`@fontsource/fira-code` and `@fontsource/share-tech`) which embed WOFF2 files directly into the Vite bundle. This produces a fully self-contained binary with no external font dependencies, satisfying the "works offline, zero external dependencies" requirement. For the Rust-to-frontend theme switching API, the established pattern is a Tauri command that the frontend invokes, which returns the theme name, and the frontend sets `document.documentElement.dataset.theme`. Alternatively, Rust can push theme changes via events or `webview.eval()`.

**Primary recommendation:** Use `@theme inline` for semantic tokens that need Tailwind utility classes, raw CSS variables in `@layer base` for primitives, and `[data-theme="cyberpunk"]` selectors for theme definitions. Bundle fonts via Fontsource. Theme switching is a single `document.documentElement.dataset.theme = "cyberpunk"` call triggered by Tauri IPC.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- Dual-tone primary palette: cyan `#009BF7` (primary) and magenta `#FE00B3` (secondary)
- Background: dark gradient (near-black with subtle variation, not flat black) -- creates spatial depth
- Feedback colors are traditional but neon-ified:
  - Error: neon red
  - Warning: cyber lime `#FCFE04` (not amber)
  - Success: proper cyan (distinct from primary accent which is blue-flavored)
- Neutral text colors: cool grays with blue undertone, cohesive with the cyan-dominant palette
- Monospace: Fira Code (used for code, data, and technical content)
- Display: Share Tech (used for headings, titles, and prominent UI text)
- Both fonts bundled as embedded assets in the binary (zero external dependencies, works offline)
- Default glow intensity: subtle (2-4px blur range) -- restrained, premium feel
- Glow color always matches the element's accent color (no independent glow color tokens)
- Surface transparency: token-based opacity levels (e.g., surface-opaque, surface-translucent, surface-glass) -- components pick from constrained set
- Glow intensity scales with interaction state, ordered:
  1. Default (dimmest)
  2. Focus/active (second-dimmest)
  3. Hover (second-brightest)
  4. Click (brightest, then fades back)
- Button-specific: secondary inner glow layer beneath the surface -- lower z-axis, less strength than edge glow but greater throw/range, bleeds inward toward button text. Creates impression of glow beneath glass surface.
- Two-tier system: primitive tokens (raw values) and semantic tokens (aliases referencing primitives)
- Components reference semantic tokens only; primitives are implementation detail
- Naming convention: flat with `--hh-` namespace prefix (e.g., `--hh-color-primary`, `--hh-glow-blur-sm`, `--hh-font-mono`)
- Theme switching via `data-theme` attribute on root `<html>` element -- each theme redefines semantic tokens under `[data-theme="x"]` selector
- Token files split by category: primitives.css, semantic.css, typography.css, effects.css -- imported into a single theme entry point

### Claude's Discretion
- Body text font assignment (monospace vs display for paragraph text)
- Exact gradient values for the void background
- Specific neon red hex value for error state
- Exact opacity values for the surface transparency levels
- Size scale and weight hierarchy for typography tokens
- How many primitive color steps per hue (e.g., cyan-100 through cyan-900)

### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| THEM-01 | CSS custom property token system (primitive + semantic tokens) | Tailwind CSS v4 `@theme inline` directive creates semantic tokens as CSS variables that generate utility classes. Primitive tokens defined as raw CSS variables in `@layer base` under `:root`. Semantic tokens reference primitives via `var()`. `@theme` cannot be scoped to selectors (confirmed by Tailwind maintainer), so theme overrides go in `[data-theme="x"]` selectors in `@layer base`. The `--hh-` namespace prefix avoids collisions with Tailwind's built-in variables. |
| THEM-02 | One polished default cyberpunk theme (dark-only) | Theme is defined by setting primitive CSS variables under `[data-theme="cyberpunk"]` and also under `:root` as the default. Colors: cyan `#009BF7` primary, magenta `#FE00B3` secondary, cyber lime `#FCFE04` warning, neon red error, dark gradient background. Glow effects via multi-layer `box-shadow` tokens with `--hh-glow-*` variables. Surface transparency via `--hh-surface-*` opacity tokens. |
| THEM-03 | Font system with default monospace + display pairing | Fontsource packages (`@fontsource/fira-code`, `@fontsource/share-tech`) bundle WOFF2 files into the Vite build output. Imported in the CSS entry point. Typography tokens defined as `--hh-font-mono` (Fira Code) and `--hh-font-display` (Share Tech). Registered in `@theme inline` to generate `font-mono` and `font-display` Tailwind utilities. |
</phase_requirements>

## Standard Stack

### Core

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Tailwind CSS | 4.x (already installed) | CSS utility generation from `@theme` tokens | v4's `@theme` directive turns CSS variables into utility classes. `@theme inline` enables runtime resolution for theme switching. Already in the project from Phase 1. |
| CSS Custom Properties | Native | Design token storage and cascade | Browser-native, zero-JS, instant resolution. `var()` references recalculate automatically when ancestor variables change. No library needed. |
| @fontsource/fira-code | 5.x | Monospace font (Fira Code) WOFF2 bundle | Self-hosted, npm-installable, includes all weights 300-700. WOFF2 format for optimal compression. Font-display: swap for fast rendering. |
| @fontsource/share-tech | 5.x | Display font (Share Tech) WOFF2 bundle | Self-hosted, npm-installable, weight 400 only. WOFF2 format. Latin subset. |

### Supporting

| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| @tauri-apps/api | 2.x (already installed) | IPC for Rust-triggered theme changes | When Rust backend needs to set/change the active theme. Uses `invoke()` or event system. |

### Alternatives Considered

| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Fontsource npm packages | Manual WOFF2 files in `public/fonts/` | Manual approach works but requires hand-written `@font-face` declarations and manual subsetting. Fontsource handles this automatically and keeps font metadata consistent. |
| `@theme inline` + CSS variable overrides | `@custom-variant` per theme | `@custom-variant` requires theme-prefixed utility classes on every element (e.g., `theme-dark:bg-black`). CSS variable overrides cascade automatically -- components just use `bg-background` and it resolves to the active theme's value. Variable approach is far cleaner for a design token system. |
| `--hh-` prefixed custom vars | Tailwind's built-in `--color-*` namespace | Using Tailwind's namespace would work but risks collisions with Tailwind's default palette and makes it unclear which tokens are HoloHue's design system vs Tailwind defaults. The `--hh-` prefix creates a clean boundary. |

**Installation:**

```bash
npm install @fontsource/fira-code @fontsource/share-tech
```

No additional dependencies needed -- Tailwind CSS v4 and Vite are already installed from Phase 1.

## Architecture Patterns

### Recommended Project Structure

```
src/
├── theme/                     # Theme system (Phase 2)
│   ├── index.css              # Entry point -- imports all token files
│   ├── primitives.css         # Primitive tokens (raw values, color steps)
│   ├── semantic.css           # Semantic tokens (@theme inline + aliases)
│   ├── typography.css         # Font tokens + @font-face via fontsource
│   └── effects.css            # Glow, shadow, and surface tokens
├── App.css                    # Global styles (imports theme/index.css)
├── App.tsx                    # Root component
├── index.tsx                  # SolidJS mount
└── components/                # Phase 1 demo components
```

### Pattern 1: Two-Tier Token Architecture with Tailwind v4

**What:** Primitive tokens hold raw values. Semantic tokens reference primitives via `var()` and are registered with `@theme inline` to generate Tailwind utility classes. Theme switching overrides primitives under `[data-theme="x"]` selectors.

**When to use:** All token definitions in this phase.

**Why `@theme inline`:** Without `inline`, Tailwind resolves theme variables at compile time using the `:root` value only. The `inline` modifier forces runtime resolution via `var()`, enabling `[data-theme]` overrides to cascade properly. This is the official pattern confirmed by Tailwind maintainer Adam Wathan (GitHub Discussion #16292).

**Example:**

```css
/* src/theme/primitives.css */

/* Primitive color tokens -- raw values, never referenced by components */
:root {
  /* Cyan palette (primary hue) */
  --hh-raw-cyan-400: #33b5f9;
  --hh-raw-cyan-500: #009BF7;
  --hh-raw-cyan-600: #007acc;

  /* Magenta palette (secondary hue) */
  --hh-raw-magenta-400: #fe4dc6;
  --hh-raw-magenta-500: #FE00B3;
  --hh-raw-magenta-600: #cc008f;

  /* Feedback colors */
  --hh-raw-red-500: #FF1744;
  --hh-raw-lime-500: #FCFE04;
  --hh-raw-green-500: #00E5CC;

  /* Neutrals (cool gray with blue undertone) */
  --hh-raw-gray-50: #f0f4f8;
  --hh-raw-gray-100: #d9e2ec;
  --hh-raw-gray-200: #bcccdc;
  --hh-raw-gray-300: #9fb3c8;
  --hh-raw-gray-400: #829ab1;
  --hh-raw-gray-500: #627d98;
  --hh-raw-gray-600: #486581;
  --hh-raw-gray-700: #334e68;
  --hh-raw-gray-800: #243b53;
  --hh-raw-gray-900: #102a43;
  --hh-raw-gray-950: #0a1929;

  /* Background */
  --hh-raw-void-start: #030712;
  --hh-raw-void-end: #0a0f1a;
}
```

```css
/* src/theme/semantic.css */
@import "tailwindcss";
/* NOTE: @import "tailwindcss" is only in the entry point,
   shown here for clarity of the @theme inline usage */

/* Semantic tokens -- registered with Tailwind for utility generation */
@theme inline {
  /* Colors */
  --color-primary: var(--hh-color-primary);
  --color-secondary: var(--hh-color-secondary);
  --color-background: var(--hh-color-bg);
  --color-surface: var(--hh-color-surface);
  --color-text: var(--hh-color-text);
  --color-text-muted: var(--hh-color-text-muted);
  --color-text-dim: var(--hh-color-text-dim);
  --color-error: var(--hh-color-error);
  --color-warning: var(--hh-color-warning);
  --color-success: var(--hh-color-success);
  --color-border: var(--hh-color-border);

  /* Fonts */
  --font-mono: var(--hh-font-mono);
  --font-display: var(--hh-font-display);
}

/* Theme: Cyberpunk (default) */
:root,
[data-theme="cyberpunk"] {
  /* Map semantic tokens to primitives */
  --hh-color-primary: var(--hh-raw-cyan-500);
  --hh-color-secondary: var(--hh-raw-magenta-500);
  --hh-color-bg: var(--hh-raw-void-start);
  --hh-color-surface: var(--hh-raw-gray-900);
  --hh-color-text: var(--hh-raw-gray-50);
  --hh-color-text-muted: var(--hh-raw-gray-400);
  --hh-color-text-dim: var(--hh-raw-gray-600);
  --hh-color-error: var(--hh-raw-red-500);
  --hh-color-warning: var(--hh-raw-lime-500);
  --hh-color-success: var(--hh-raw-green-500);
  --hh-color-border: var(--hh-raw-gray-800);
}
```

**Usage in components (Tailwind utility classes):**
```html
<div class="bg-background text-text">
  <h1 class="font-display text-primary">HoloHue</h1>
  <p class="font-mono text-text-muted">System online</p>
  <span class="text-error">Alert: breach detected</span>
</div>
```

### Pattern 2: Glow Effect Token System

**What:** Multi-layer `box-shadow` glow effects tokenized as CSS custom properties, with intensity scaling by interaction state.

**When to use:** All glow/effect definitions. Components reference these tokens, never raw box-shadow values.

**Example:**

```css
/* src/theme/effects.css */

:root,
[data-theme="cyberpunk"] {
  /* Glow blur radii */
  --hh-glow-blur-xs: 2px;
  --hh-glow-blur-sm: 3px;
  --hh-glow-blur-md: 6px;
  --hh-glow-blur-lg: 10px;
  --hh-glow-blur-xl: 16px;

  /* Glow spread */
  --hh-glow-spread-tight: 0px;
  --hh-glow-spread-sm: 1px;
  --hh-glow-spread-md: 2px;

  /* Surface transparency levels */
  --hh-surface-opaque: 1;
  --hh-surface-translucent: 0.75;
  --hh-surface-glass: 0.35;
  --hh-surface-ghost: 0.15;

  /* Interaction-state glow intensities (opacity multipliers) */
  --hh-glow-intensity-default: 0.3;
  --hh-glow-intensity-focus: 0.45;
  --hh-glow-intensity-hover: 0.65;
  --hh-glow-intensity-active: 0.9;
}
```

**Glow composition pattern for components (Phase 3 will use these):**
```css
/* Example: how a button component would consume glow tokens */
.hh-btn {
  /* Edge glow -- tight, crisp */
  box-shadow:
    0 0 var(--hh-glow-blur-sm) var(--hh-glow-spread-tight)
      color-mix(in srgb, var(--hh-color-primary) calc(var(--hh-glow-intensity-default) * 100%), transparent);

  transition: box-shadow 150ms ease-out;
}

.hh-btn:hover {
  box-shadow:
    /* Edge glow -- intensified */
    0 0 var(--hh-glow-blur-md) var(--hh-glow-spread-sm)
      color-mix(in srgb, var(--hh-color-primary) calc(var(--hh-glow-intensity-hover) * 100%), transparent),
    /* Inner glow -- softer, wider spread, bleeds inward */
    inset 0 0 var(--hh-glow-blur-lg) var(--hh-glow-spread-md)
      color-mix(in srgb, var(--hh-color-primary) calc(var(--hh-glow-intensity-default) * 100%), transparent);
}
```

### Pattern 3: Font Loading via Fontsource

**What:** Import Fontsource CSS which contains `@font-face` declarations pointing to bundled WOFF2 files. Register font families as Tailwind theme tokens.

**When to use:** Typography setup.

**Example:**

```css
/* src/theme/typography.css */

/* Fontsource imports -- bundles WOFF2 into Vite output */
/* These must be CSS @import or JS import, not @font-face */

:root,
[data-theme="cyberpunk"] {
  --hh-font-mono: 'Fira Code', ui-monospace, monospace;
  --hh-font-display: 'Share Tech', system-ui, sans-serif;

  /* Type scale */
  --hh-text-xs: 0.75rem;    /* 12px */
  --hh-text-sm: 0.875rem;   /* 14px */
  --hh-text-base: 1rem;     /* 16px */
  --hh-text-lg: 1.125rem;   /* 18px */
  --hh-text-xl: 1.25rem;    /* 20px */
  --hh-text-2xl: 1.5rem;    /* 24px */
  --hh-text-3xl: 2rem;      /* 32px */
  --hh-text-4xl: 2.5rem;    /* 40px */

  /* Font weights */
  --hh-weight-normal: 400;
  --hh-weight-medium: 500;
  --hh-weight-semibold: 600;
  --hh-weight-bold: 700;
}
```

```typescript
// src/theme/fonts.ts -- JS import for Fontsource (triggers Vite asset bundling)
import '@fontsource/fira-code/400.css';
import '@fontsource/fira-code/500.css';
import '@fontsource/fira-code/600.css';
import '@fontsource/fira-code/700.css';
import '@fontsource/share-tech/400.css';
```

### Pattern 4: Theme Switching from Rust via Tauri IPC

**What:** A Tauri command that triggers theme switching. The frontend sets the `data-theme` attribute on `<html>`.

**When to use:** When Rust backend needs to control the theme (e.g., user preference loaded from config file, API-driven theme change).

**Example:**

```rust
// src-tauri/src/lib.rs (additions)

#[tauri::command]
fn set_theme(theme: String) -> Result<String, String> {
    // Validate theme name
    match theme.as_str() {
        "cyberpunk" => Ok(theme),
        _ => Err(format!("Unknown theme: {}", theme)),
    }
}
```

```typescript
// Frontend: theme switching utility
import { invoke } from '@tauri-apps/api/core';

export async function setTheme(theme: string): Promise<void> {
  // Validate with Rust backend
  const validated = await invoke<string>('set_theme', { theme });
  // Apply to DOM -- instant, zero re-render
  document.documentElement.dataset.theme = validated;
}

// On app startup: apply default theme before first paint
document.documentElement.dataset.theme = 'cyberpunk';
```

### Pattern 5: Dark Gradient Background

**What:** The void background uses a CSS gradient rather than a flat color, creating spatial depth per the user's decision.

**When to use:** Applied to the root element or body.

**Example:**

```css
/* In semantic.css or effects.css */
:root,
[data-theme="cyberpunk"] {
  --hh-bg-gradient: linear-gradient(
    180deg,
    var(--hh-raw-void-start) 0%,
    var(--hh-raw-void-end) 50%,
    var(--hh-raw-void-start) 100%
  );
}
```

### Anti-Patterns to Avoid

- **Defining all tokens in `@theme` blocks with scoped selectors:** `@theme` is global and cannot be scoped to `[data-theme]`. Attempting to nest `@theme` inside a selector has no effect (the last `@theme` wins globally). Use `@theme inline` for utility class generation, then override the underlying CSS variables in `[data-theme]` selectors within `@layer base`.
- **Using `@theme` (without `inline`) for runtime-resolved tokens:** Without `inline`, Tailwind resolves variable values at build time from `:root` only. Theme switching will not work because the compiled CSS contains the resolved value, not the `var()` reference.
- **Importing fonts from Google Fonts CDN:** The user explicitly requires fonts bundled as embedded assets with zero external dependencies. Use Fontsource npm packages which bundle WOFF2 files into the Vite build.
- **Creating independent glow color tokens:** The user specified that glow color always matches the element's accent color. Glow tokens should control blur, spread, and intensity -- not color. The glow color is always derived from the contextual accent color using `color-mix()` or direct `var()` reference.
- **Putting semantic token definitions in component CSS files:** Semantic tokens are system-level. They belong in the theme directory, not scattered across components. Components only consume tokens via utility classes or `var()` references.
- **Skipping the `data-theme` attribute on initial load:** The `data-theme="cyberpunk"` attribute must be set on `<html>` before first paint, either in the static HTML or via an inline `<script>` in `<head>`. Setting it after React/SolidJS mounts causes a flash of unstyled content.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Font hosting / @font-face declarations | Manual WOFF2 downloads + hand-written @font-face | `@fontsource/fira-code` + `@fontsource/share-tech` npm packages | Fontsource handles subsetting, format declarations, font-display strategy, and updates. Manual approach is error-prone and requires maintenance. |
| Design token utility class generation | Custom PostCSS plugin or manual utility classes | Tailwind CSS v4 `@theme inline` directive | `@theme` automatically generates utility classes from CSS variables. Custom generation is redundant work. |
| Color mixing for glow opacity | Manual hex/rgba calculations for semi-transparent glows | CSS `color-mix(in srgb, color percentage, transparent)` | `color-mix()` is natively supported in all modern browsers (and WebView2/Chromium). It composes cleanly with CSS variables. No JS color math needed. |
| Theme persistence across sessions | Custom localStorage + state management | Tauri-managed config file read by Rust, passed to frontend on init | Theme preference is application state, not presentation state. Rust owns persistence; frontend owns rendering. |

**Key insight:** The entire theme system is pure CSS. No JavaScript framework code runs during theme switches. The only JS involved is setting the `data-theme` attribute (one DOM property write). Everything else cascades via native CSS variable resolution.

## Common Pitfalls

### Pitfall 1: @theme Scoping Limitation

**What goes wrong:** Developer defines theme-specific `@theme` blocks expecting them to be scoped to `[data-theme]` selectors. All themes appear to use the values from the last `@theme` block, regardless of the `data-theme` attribute.
**Why it happens:** Tailwind's `@theme` directive operates at the compilation level, not the CSS cascade level. Multiple `@theme` blocks merge globally. `@theme` cannot be nested inside selectors.
**How to avoid:** Use `@theme inline` once to declare semantic token names (mapping to CSS variables). Override the underlying CSS variables in `[data-theme="x"]` selectors within `@layer base`. This is the pattern confirmed by Tailwind maintainer Adam Wathan.
**Warning signs:** All themes look identical. Changing `data-theme` has no visual effect.

### Pitfall 2: Missing `inline` Keyword on @theme

**What goes wrong:** Theme switching appears to work in isolation but Tailwind utility classes don't respond to theme changes. `bg-primary` always resolves to the `:root` value regardless of `data-theme`.
**Why it happens:** Without `inline`, Tailwind resolves `var()` references at build time. The compiled CSS for `bg-primary` contains the hard-coded color value, not a `var()` reference. Runtime variable changes have no effect on the compiled utilities.
**How to avoid:** Always use `@theme inline { ... }` when the token values need to change at runtime (which is all theme tokens in this project).
**Warning signs:** Inspecting the compiled CSS shows literal color values instead of `var()` references in utility classes.

### Pitfall 3: Flash of Unstyled Content on Theme Load

**What goes wrong:** The app briefly shows the wrong theme (or no theme) before the correct theme applies. Background flashes white or shows default colors.
**Why it happens:** The `data-theme` attribute is set after SolidJS mounts (in `onMount` or `createEffect`), which runs after the first paint. For the first frame, no `data-theme` attribute exists, so CSS falls back to `:root` defaults.
**How to avoid:** Set `data-theme="cyberpunk"` directly on the `<html>` element in `index.html` (static markup), or use a blocking inline `<script>` in `<head>` that sets it before any CSS is applied. The existing Phase 1 pattern of `style="background: #030712;"` on `<body>` addresses the background flash; extend it with `data-theme`.
**Warning signs:** Brief color flash on app startup. Background flickers from a different color before settling.

### Pitfall 4: Tailwind Utility Name Collisions with --hh- Prefix

**What goes wrong:** Defining `@theme inline { --color-hh-primary: ... }` generates a utility class `bg-hh-primary` instead of `bg-primary`. Or the `--hh-` prefix creates unnecessarily verbose class names.
**Why it happens:** Tailwind generates utility class names from the variable name after the namespace prefix (`--color-`). If the variable is `--color-hh-primary`, the utility is `bg-hh-primary`.
**How to avoid:** In `@theme inline`, use standard Tailwind namespace names WITHOUT the `--hh-` prefix (e.g., `--color-primary`). The `--hh-` prefix is only for the intermediate CSS variables that these point to. The `@theme` declaration maps clean utility names to `--hh-`-prefixed variables: `--color-primary: var(--hh-color-primary)`.
**Warning signs:** Utility classes have unexpected names with `hh-` in them.

### Pitfall 5: Animating box-shadow for Glow Effects

**What goes wrong:** Glow hover transitions cause frame drops or janky animation, especially with multiple stacked shadow layers.
**Why it happens:** `box-shadow` is NOT a compositor-only property. Changing it triggers repaint on every frame. Multiple large-blur shadows compound the repaint cost.
**How to avoid:** For static glows (default state), `box-shadow` is fine -- it's only computed once. For transitions between states (hover, focus, active), use `transition: box-shadow` with a short duration (100-200ms) and avoid animating large blur radius changes. Alternatively, use a pseudo-element with `opacity` animation to fade between glow states (opacity IS compositor-only). Keep blur values within the 2-4px range specified by the user for default state.
**Warning signs:** FPS drops during hover transitions on glow elements. DevTools "Paint" profiler shows large paint regions on hover.

### Pitfall 6: Fontsource Import Location

**What goes wrong:** Fonts are imported but not available. `font-family: 'Fira Code'` falls back to the browser default monospace font.
**Why it happens:** Fontsource CSS files contain `@font-face` declarations that must be processed by Vite's CSS pipeline. If imported only in a `.ts` file that isn't part of the CSS dependency graph, the `@font-face` declarations may be tree-shaken or not included.
**How to avoid:** Import Fontsource CSS from a TypeScript/JavaScript entry file that Vite processes (e.g., `index.tsx` or a dedicated `fonts.ts` imported by `index.tsx`). Verify in browser DevTools Network tab that WOFF2 files are loaded.
**Warning signs:** Font loads fallback. Network tab shows no WOFF2 requests. `document.fonts.check('16px Fira Code')` returns false.

## Code Examples

### Complete Theme Entry Point

```css
/* src/theme/index.css */

/* Font imports (Fontsource WOFF2 bundles) */
/* Note: Fontsource CSS is imported via JS -- see fonts.ts */

/* Token layers */
@import "./primitives.css";
@import "./effects.css";
@import "./typography.css";
@import "./semantic.css";
```

### Semantic Token Registration with Tailwind

```css
/* src/theme/semantic.css */
/* Source: Tailwind CSS v4 @theme docs + GitHub Discussion #16292 */

@theme inline {
  /* Color tokens -> Tailwind utilities (bg-primary, text-secondary, etc.) */
  --color-primary: var(--hh-color-primary);
  --color-secondary: var(--hh-color-secondary);
  --color-background: var(--hh-color-bg);
  --color-surface: var(--hh-color-surface);
  --color-text: var(--hh-color-text);
  --color-text-muted: var(--hh-color-text-muted);
  --color-text-dim: var(--hh-color-text-dim);
  --color-error: var(--hh-color-error);
  --color-warning: var(--hh-color-warning);
  --color-success: var(--hh-color-success);
  --color-border: var(--hh-color-border);

  /* Font tokens -> Tailwind utilities (font-mono, font-display) */
  --font-mono: var(--hh-font-mono);
  --font-display: var(--hh-font-display);
}

/* Default theme assignment (cyberpunk) */
:root,
[data-theme="cyberpunk"] {
  --hh-color-primary: var(--hh-raw-cyan-500);
  --hh-color-secondary: var(--hh-raw-magenta-500);
  --hh-color-bg: var(--hh-raw-void-start);
  --hh-color-surface: var(--hh-raw-gray-900);
  --hh-color-text: var(--hh-raw-gray-50);
  --hh-color-text-muted: var(--hh-raw-gray-400);
  --hh-color-text-dim: var(--hh-raw-gray-600);
  --hh-color-error: var(--hh-raw-red-500);
  --hh-color-warning: var(--hh-raw-lime-500);
  --hh-color-success: var(--hh-raw-green-500);
  --hh-color-border: var(--hh-raw-gray-800);

  --hh-font-mono: 'Fira Code', ui-monospace, monospace;
  --hh-font-display: 'Share Tech', system-ui, sans-serif;
}
```

### Glow Effect Tokens

```css
/* src/theme/effects.css */

:root,
[data-theme="cyberpunk"] {
  /* Glow blur scale (restrained, premium feel per user decision) */
  --hh-glow-blur-xs: 2px;
  --hh-glow-blur-sm: 3px;
  --hh-glow-blur-md: 6px;
  --hh-glow-blur-lg: 10px;
  --hh-glow-blur-xl: 16px;

  /* Glow spread */
  --hh-glow-spread-none: 0px;
  --hh-glow-spread-sm: 1px;
  --hh-glow-spread-md: 2px;

  /* Interaction-state glow intensity (applied as opacity) */
  --hh-glow-intensity-default: 0.3;
  --hh-glow-intensity-focus: 0.45;
  --hh-glow-intensity-hover: 0.65;
  --hh-glow-intensity-active: 0.9;

  /* Surface transparency tiers */
  --hh-surface-opaque: 1;
  --hh-surface-translucent: 0.75;
  --hh-surface-glass: 0.35;
  --hh-surface-ghost: 0.15;
}
```

### Initial HTML with data-theme Attribute (Flash Prevention)

```html
<!-- index.html -->
<!doctype html>
<html lang="en" data-theme="cyberpunk">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HoloHue</title>
  </head>
  <body style="background: #030712;">
    <div id="root"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>
```

### Rust Theme Command

```rust
// Addition to src-tauri/src/lib.rs
// Source: https://v2.tauri.app/develop/calling-rust/

#[tauri::command]
fn set_theme(theme: String) -> Result<String, String> {
    match theme.as_str() {
        "cyberpunk" => Ok(theme),
        _ => Err(format!("Unknown theme: {}", theme)),
    }
}

// Register in invoke_handler:
// .invoke_handler(tauri::generate_handler![greet, ping, set_theme])
```

### Font Import in TypeScript Entry

```typescript
// src/theme/fonts.ts
// Source: https://fontsource.org/fonts/fira-code/install
// Source: https://fontsource.org/fonts/share-tech/install

// Fira Code -- monospace for code, data, technical content
import '@fontsource/fira-code/400.css';  // Normal weight
import '@fontsource/fira-code/500.css';  // Medium weight
import '@fontsource/fira-code/600.css';  // Semibold weight
import '@fontsource/fira-code/700.css';  // Bold weight

// Share Tech -- display font for headings, titles, prominent UI text
import '@fontsource/share-tech/400.css'; // Only available weight
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Tailwind v3 `tailwind.config.js` theme extension | Tailwind v4 `@theme` directive in CSS | Jan 2025 (v4 release) | Theme configuration is CSS-native. No JS config file. Tokens are CSS variables with utility class generation built-in. |
| Tailwind v3 `darkMode: 'class'` config | Tailwind v4 `@custom-variant dark (...)` or CSS variable override | Jan 2025 (v4 release) | Dark mode is defined in CSS, not config. For token-based themes, CSS variable override is preferred over `@custom-variant`. |
| Google Fonts CDN `<link>` tags | Fontsource npm packages with bundled WOFF2 | Fontsource v5 (2023+) | Self-hosted, privacy-respecting, works offline, consistent with Vite asset pipeline. No external network dependency. |
| SCSS/LESS variables for theming | CSS custom properties (native variables) | Mainstream since 2020+ | CSS variables are live (runtime-resolved), work with the cascade, and need no preprocessor. SCSS variables are compile-time only. |
| JavaScript-based theme context (React Context, SolidJS context) | CSS custom properties with `data-theme` attribute | Ongoing best practice | CSS variables don't trigger JS re-renders. Changing a `data-theme` attribute is a single DOM mutation. The entire UI updates via CSS cascade, not component tree traversal. |
| Manual `rgba()` for semi-transparent colors | `color-mix(in srgb, color percentage, transparent)` | Chrome 111+ (2023), full support | `color-mix()` composes with CSS variables cleanly. `rgba()` requires knowing the RGB components at definition time, which breaks with variable references. |

**Deprecated/outdated:**
- `tailwind.config.js` theme extension: Use `@theme` directive in CSS for Tailwind v4
- `darkMode: 'class'` in Tailwind config: Use `@custom-variant` or CSS variable override approach
- `@import url('https://fonts.googleapis.com/...')`: Use Fontsource for self-hosted fonts
- `postcss-import` + `postcss-nesting` plugins: Tailwind v4's Vite plugin handles these natively

## Open Questions

1. **Body text font assignment (Claude's Discretion)**
   - What we know: Fira Code is for "code, data, and technical content." Share Tech is for "headings, titles, and prominent UI text." Body/paragraph text is unspecified.
   - Recommendation: Use Fira Code (monospace) as the default body font. This is a cyberpunk/hacker aesthetic GUI framework -- monospace body text reinforces the terminal/tech identity. Share Tech should be reserved for display contexts (headings, labels, prominent UI text). This is a common pattern in cyberpunk/sci-fi interfaces.

2. **Exact gradient values for void background (Claude's Discretion)**
   - What we know: "Dark gradient (near-black with subtle variation, not flat black) -- creates spatial depth."
   - Recommendation: A top-to-bottom gradient from `#030712` (current body bg) through `#0a0f1a` (slightly blue-shifted) back to `#030712`. The blue shift is subtle (5-10% hue) and reinforces the cool/cyan palette. Exact values in the code examples above.

3. **Neon red hex value for error state (Claude's Discretion)**
   - What we know: "Error: neon red" -- must be neon-ified, not traditional dark red.
   - Recommendation: `#FF1744` (Material Design "Red A400") -- a vibrant, neon-saturated red that reads clearly on dark backgrounds and fits the neon aesthetic. Alternative: `#FF0040` for a more pure neon feel.

4. **Surface opacity exact values (Claude's Discretion)**
   - What we know: Token-based opacity levels (opaque, translucent, glass, ghost).
   - Recommendation: `opaque: 1.0`, `translucent: 0.75`, `glass: 0.35`, `ghost: 0.15`. These four tiers provide sufficient range. Glass at 0.35 allows background to show through while maintaining readability. Ghost at 0.15 is for subtle overlays.

5. **Primitive color step count (Claude's Discretion)**
   - What we know: Each hue needs steps (e.g., cyan-100 through cyan-900). The project primarily uses 2-3 stops per accent hue.
   - Recommendation: Define 3 steps per accent hue (400/500/600 for light/base/dark), full 11-step scale (50-950) only for neutrals (gray). Accent colors are used at 1-2 values in practice; a full 11-step scale per hue is wasteful for a 2-accent system. Neutrals need the full range for text hierarchy, borders, and surface variations.

## Sources

### Primary (HIGH confidence)
- [Tailwind CSS v4 @theme directive documentation](https://tailwindcss.com/docs/theme) - @theme inline, variable namespaces, utility class generation, theme sharing
- [Tailwind CSS v4 Adding Custom Styles](https://tailwindcss.com/docs/adding-custom-styles) - @layer base, @custom-variant, custom utilities, theme integration patterns
- [Tailwind CSS GitHub Discussion #16292](https://github.com/tailwindlabs/tailwindcss/discussions/16292) - Adam Wathan confirming @theme cannot be scoped to data-theme selectors; recommended CSS variable override pattern
- [Tailwind CSS GitHub Discussion #15600](https://github.com/tailwindlabs/tailwindcss/discussions/15600) - @theme inline pattern for runtime-resolved variables
- [Tailwind CSS GitHub Discussion #18471](https://github.com/tailwindlabs/tailwindcss/discussions/18471) - @theme inline + @layer theme pattern for multi-theme systems
- [Fontsource Fira Code installation](https://fontsource.org/fonts/fira-code/install) - npm package, CSS import, WOFF2 bundling, available weights (300-700)
- [Fontsource Share Tech installation](https://fontsource.org/fonts/share-tech/install) - npm package, CSS import, weight 400 only, Latin subset
- [Tauri v2 Calling Frontend from Rust](https://v2.tauri.app/develop/calling-frontend/) - events, eval(), channels for Rust-to-frontend communication
- [Tauri v2 Calling Rust from Frontend](https://v2.tauri.app/develop/calling-rust/) - invoke(), command registration, argument serialization
- [MDN CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) - var() resolution, cascade behavior, inheritance

### Secondary (MEDIUM confidence)
- [Jake Bukuts - Theming with TailwindCSS v4](https://www.jbukuts.com/posts/theming-tailwind-v4) - Complete example of @theme inline + data-theme + @layer base pattern
- [Penpot Design Tokens Guide](https://penpot.app/blog/the-developers-guide-to-design-tokens-and-css-variables/) - Two-tier token architecture (primitive + semantic) best practices
- [CSS Neon Glow Techniques](https://css3shapes.com/how-to-make-a-neon-glow-effect-in-css/) - Multi-layer box-shadow for neon glow effects
- [Neon Morphism CSS Pattern](https://dev.to/er-raj-aryan/how-to-implement-neon-morphism-in-css-the-hottest-design-trend-of-2023-2if5) - Inner/outer glow combination, interaction state patterns

### Tertiary (LOW confidence)
- None -- all findings verified against primary or secondary sources

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH - Tailwind v4 @theme inline is officially documented; Fontsource is the standard npm font bundling solution
- Architecture: HIGH - Token architecture pattern (primitive -> semantic -> utility) confirmed by Tailwind maintainer and multiple credible sources; @theme scoping limitation verified in official GitHub discussion
- Pitfalls: HIGH - @theme scoping limitation confirmed by maintainer; font loading patterns verified by Fontsource docs; flash prevention is standard web pattern
- Glow effects: MEDIUM - Token structure is based on established CSS patterns; exact `color-mix()` composition with CSS variables needs runtime validation in WebView2

**Research date:** 2026-02-27
**Valid until:** 2026-03-29 (stable stack, 30-day validity)
