Banner VC

Emblem Theme System Implementation

Implemented a complete multi-mode theming system for banner-site following the dark-matter pattern. The 'emblem' theme supports light, dark, and vibrant modes with semantic color tokens, effect tokens, and CSS variable-based architecture.

Claude
Claude Claude

Changelog - 2026-01-19 (#1)

Emblem Theme System Implementation

Overview

Implemented a complete multi-mode theming system for banner-site (Banner VC) using the established dark-matter pattern. The theme is named "emblem" to keep the client name out of the codebase. Supports three modes: light (clean, professional), dark (bold, confident), and vibrant (high-impact expressive).


Architecture

Theme Token Hierarchy

:root (Brand-level named colors)
├── --color-crimson (#B91C1C)        Primary red
├── --color-crimson-bright (#DC2626) Brighter red for dark modes
├── --color-haiti-blue (#141531)     Dark mode background
├── --color-navy (#1D4ED8)           Secondary blue
├── --color-navy-bright (#2563EB)    Brighter blue for dark modes
├── --color-amber (#B45309)          Accent warm
├── --color-electric (#60A5FA)       Electric blue for vibrant
├── --color-ivory (#F9FAFB)          Light mode background
├── --color-midnight (#172554)       Alternative dark background
├── --color-charcoal (#171717)       Light mode text
├── --color-snow (#FFFFFF)           Dark mode text
└── --color-graphite-* (50-950)      Neutral grey scale

[data-theme="emblem"][data-mode="light|dark|vibrant"]
├── --color-background              Core background
├── --color-foreground              Core text color
├── --color-surface                 Card/panel backgrounds
├── --color-surface-muted           Subdued surfaces
├── --color-primary                 Primary action color
├── --color-primary-foreground      Text on primary
├── --color-secondary               Secondary action color
├── --color-accent                  Accent highlights
├── --color-muted                   Muted backgrounds
├── --color-muted-foreground        Subdued text
├── --color-border                  Border color
├── --color-input                   Input backgrounds
├── --color-ring                    Focus ring color
└── Effect tokens (--fx-*)
    ├── --fx-glow-opacity
    ├── --fx-glow-spread
    ├── --fx-glow-color
    ├── --fx-text-shadow
    ├── --fx-hero-gradient
    ├── --fx-hero-bg
    ├── --fx-card-bg
    ├── --fx-card-border
    ├── --fx-card-shadow
    └── --fx-headline-gradient

Key Technical Decisions

1. Tailwind 4 Integration

Used @tailwindcss/vite plugin (not deprecated @astrojs/tailwind):

// astro.config.mjs
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
});

2. Custom Color Utilities with @theme inline

Tailwind 4 requires explicit registration of custom colors for utility generation. Used @theme inline to enable utilities like bg-background/80:

@theme inline {
  --color-background: var(--color-background);
  --color-foreground: var(--color-foreground);
  --color-surface: var(--color-surface);
  /* ... etc */
}

3. color-mix() for Opacity Variants

Since Tailwind can't compute opacity on CSS variable colors, we defined custom utilities using color-mix():

@layer utilities {
  .bg-background\/80 {
    background-color: color-mix(in srgb, var(--color-background) 80%, transparent);
  }
  .border-border\/20 {
    border-color: color-mix(in srgb, var(--color-border) 20%, transparent);
  }
}

4. Derived Colors from Background

To ensure changing --color-background propagates throughout the theme, surface and effect colors are derived using color-mix():

[data-theme="emblem"][data-mode="dark"] {
  --color-background: var(--color-haiti-blue);

  /* Derived from background */
  --color-surface: color-mix(in srgb, var(--color-background) 85%, white);
  --color-surface-muted: color-mix(in srgb, var(--color-background) 70%, black);
  --color-muted: color-mix(in srgb, var(--color-background) 80%, white);
  --color-border: color-mix(in srgb, var(--color-background) 70%, white);

  /* Effect tokens also derived */
  --fx-hero-bg: var(--color-background);
  --fx-card-bg: color-mix(in srgb, var(--color-background) 80%, transparent);
}

Files Created

src/styles/global.css

Base Tailwind 4 setup with:

  • @import "tailwindcss";
  • @theme inline block for custom color registration
  • Breakpoint CSS variables
  • Typography system variables
  • Transition variables for smooth theme changes
  • Custom utility classes for semantic colors
  • Responsive visibility utilities

src/styles/themes/emblem-theme.css

Complete theme definition with:

  • :root block with brand-level named colors
  • Light mode semantic tokens and effect tokens
  • Dark mode semantic tokens and effect tokens
  • Vibrant mode semantic tokens and effect tokens
  • Helper classes (.card, .btn-primary, .btn-secondary)
  • Hero section helpers (.hero-shell, .hero-panel)
  • Glass morphism variant (.card-glass)
  • Gradient text effect (.gradient-text)

src/layouts/BaseThemeLayout.astro

Layout wrapper that:

  • Sets <html class="theme-emblem" data-theme="emblem" data-mode={mode}>
  • Imports global.css and emblem-theme.css
  • Includes Header component
  • Props: title, description, mode, hideHeader

src/components/ui/ModeToggle.astro

Mode toggle component with:

  • Three icons: sun (light), moon (dark), star (vibrant)
  • LocalStorage persistence via emblem-mode key
  • System preference detection on initial load
  • Click cycles through modes: light → dark → vibrant → light

src/components/basics/Header.astro

Navigation header with:

  • Sticky positioning with backdrop blur
  • Brand logo and site title
  • Desktop navigation with dropdown menu
  • Mobile hamburger menu with slide-in animation
  • Mode toggle integration
  • Emblem-specific styling (red/blue gradient underlines)

src/pages/test.astro

Test page demonstrating:

  • All three theme modes
  • Color token swatches
  • Card components
  • Button variants
  • Glass morphism card
  • Hero section
  • Gradient text effect

Mode Characteristics

Mode Background Primary Effects
Light Ivory (#F9FAFB) Crimson (#B91C1C) Minimal shadows
Dark Haiti Blue (#141531) Crimson Bright (#DC2626) Moderate glow
Vibrant Haiti Blue (#141531) Loud Red (#EF4444) Maximum glow, dramatic shadows

Usage

Basic Page

---
import BaseThemeLayout from '../layouts/BaseThemeLayout.astro';
---

<BaseThemeLayout title="My Page" mode="dark">
  <div class="hero-shell">
    <section class="container mx-auto px-4 py-16">
      <h1 class="gradient-text text-4xl font-bold">Hello</h1>
      <div class="card p-6">Card content</div>
      <button class="btn-primary">Action</button>
    </section>
  </div>
</BaseThemeLayout>

Changing Theme Background

Edit the dark mode --color-background in emblem-theme.css:

[data-theme="emblem"][data-mode="dark"] {
  --color-background: var(--color-haiti-blue); /* Change this */
}

All derived colors (surface, muted, border, card backgrounds) will automatically update.


Known Issues / Future Work

Hardcoded Colors to Address

Several effect tokens still use hardcoded rgba values instead of referencing theme variables:

  1. Light mode glow colors: rgba(185, 28, 28, ...) should use color-mix(in srgb, var(--color-crimson) X%, transparent)

  2. Vibrant mode primary colors: #EF4444, #3B82F6, #FBBF24 should be defined as named variables (--color-crimson-loud, --color-navy-loud, --color-amber-loud)

  3. Muted foreground in dark mode: #94A3B8 should reference graphite scale

  4. Hero panel gradients: Use var(--color-primary) and var(--color-secondary) with color-mix instead of hardcoded rgba


Dependencies

  • tailwindcss: ^4.1.18
  • @tailwindcss/vite: ^4.1.18
  • astro: ^5.16.11

No additional npm packages required. Theme is pure CSS with CSS variables.