// 80% opacity
+
+// CSS output
+background-color: hsl(217 91% 60% / 0.5);
+```
+
+## Component Classes
+
+### Button Example
+
+```css
+@layer components {
+ .btn {
+ @apply inline-flex items-center justify-center
+ rounded-md font-medium
+ transition-colors
+ focus-visible:outline-none focus-visible:ring-2
+ focus-visible:ring-ring focus-visible:ring-offset-2
+ disabled:pointer-events-none disabled:opacity-50;
+ }
+
+ .btn-default {
+ @apply bg-primary text-primary-foreground
+ hover:bg-primary/90;
+ }
+
+ .btn-secondary {
+ @apply bg-secondary text-secondary-foreground
+ hover:bg-secondary/80;
+ }
+
+ .btn-outline {
+ @apply border border-input bg-background
+ hover:bg-accent hover:text-accent-foreground;
+ }
+
+ .btn-ghost {
+ @apply hover:bg-accent hover:text-accent-foreground;
+ }
+
+ .btn-destructive {
+ @apply bg-destructive text-destructive-foreground
+ hover:bg-destructive/90;
+ }
+
+ /* Sizes */
+ .btn-sm { @apply h-8 px-3 text-xs; }
+ .btn-md { @apply h-10 px-4 text-sm; }
+ .btn-lg { @apply h-12 px-6 text-base; }
+}
+```
+
+## Spacing Integration
+
+```typescript
+// tailwind.config.ts
+theme: {
+ extend: {
+ spacing: {
+ // Map to CSS variables if needed
+ 'section': 'var(--spacing-section)',
+ 'component': 'var(--spacing-component)',
+ }
+ }
+}
+```
+
+## Animation Tokens
+
+```typescript
+// tailwind.config.ts
+theme: {
+ extend: {
+ transitionDuration: {
+ fast: '150ms',
+ normal: '200ms',
+ slow: '300ms',
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: { height: '0' },
+ to: { height: 'var(--radix-accordion-content-height)' },
+ },
+ 'accordion-up': {
+ from: { height: 'var(--radix-accordion-content-height)' },
+ to: { height: '0' },
+ },
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ },
+ }
+}
+```
+
+## Dark Mode Toggle
+
+```typescript
+// Toggle dark mode
+function toggleDarkMode() {
+ document.documentElement.classList.toggle('dark')
+}
+
+// System preference
+if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ document.documentElement.classList.add('dark')
+}
+```
+
+## shadcn/ui Alignment
+
+This configuration aligns with shadcn/ui conventions:
+
+- Same CSS variable naming
+- Same HSL format
+- Same color scale structure
+- Compatible with `npx shadcn@latest add` commands
+
+### Using with shadcn/ui
+
+```bash
+# Initialize (uses same token structure)
+npx shadcn@latest init
+
+# Add components (styled with these tokens)
+npx shadcn@latest add button card input
+```
+
+Components will automatically use your design system tokens.
diff --git a/.agents/skills/design-system/references/token-architecture.md b/.agents/skills/design-system/references/token-architecture.md
new file mode 100644
index 0000000..e13ed2b
--- /dev/null
+++ b/.agents/skills/design-system/references/token-architecture.md
@@ -0,0 +1,224 @@
+# Token Architecture
+
+Three-layer token system for scalable, themeable design systems.
+
+## Layer Overview
+
+```
+┌─────────────────────────────────────────┐
+│ Component Tokens │ Per-component overrides
+│ --button-bg, --card-padding │
+├─────────────────────────────────────────┤
+│ Semantic Tokens │ Purpose-based aliases
+│ --color-primary, --spacing-section │
+├─────────────────────────────────────────┤
+│ Primitive Tokens │ Raw design values
+│ --color-blue-600, --space-4 │
+└─────────────────────────────────────────┘
+```
+
+## Why Three Layers?
+
+| Layer | Purpose | When to Change |
+|-------|---------|----------------|
+| Primitive | Base values (colors, sizes) | Rarely - foundational |
+| Semantic | Meaning assignment | Theme switching |
+| Component | Component customization | Per-component needs |
+
+## Layer 1: Primitive Tokens
+
+Raw design values without semantic meaning.
+
+```css
+:root {
+ /* Colors */
+ --color-gray-50: #F9FAFB;
+ --color-gray-900: #111827;
+ --color-blue-500: #3B82F6;
+ --color-blue-600: #2563EB;
+
+ /* Spacing (4px base) */
+ --space-1: 0.25rem; /* 4px */
+ --space-2: 0.5rem; /* 8px */
+ --space-4: 1rem; /* 16px */
+ --space-6: 1.5rem; /* 24px */
+
+ /* Typography */
+ --font-size-sm: 0.875rem;
+ --font-size-base: 1rem;
+ --font-size-lg: 1.125rem;
+
+ /* Radius */
+ --radius-sm: 0.25rem;
+ --radius-default: 0.5rem;
+ --radius-lg: 0.75rem;
+
+ /* Shadows */
+ --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05);
+ --shadow-default: 0 1px 3px rgb(0 0 0 / 0.1);
+}
+```
+
+## Layer 2: Semantic Tokens
+
+Purpose-based aliases that reference primitives.
+
+```css
+:root {
+ /* Background */
+ --color-background: var(--color-gray-50);
+ --color-foreground: var(--color-gray-900);
+
+ /* Primary */
+ --color-primary: var(--color-blue-600);
+ --color-primary-hover: var(--color-blue-700);
+
+ /* Secondary */
+ --color-secondary: var(--color-gray-100);
+ --color-secondary-foreground: var(--color-gray-900);
+
+ /* Muted */
+ --color-muted: var(--color-gray-100);
+ --color-muted-foreground: var(--color-gray-500);
+
+ /* Destructive */
+ --color-destructive: var(--color-red-600);
+ --color-destructive-foreground: white;
+
+ /* Spacing */
+ --spacing-component: var(--space-4);
+ --spacing-section: var(--space-6);
+}
+```
+
+## Layer 3: Component Tokens
+
+Component-specific tokens referencing semantic layer.
+
+```css
+:root {
+ /* Button */
+ --button-bg: var(--color-primary);
+ --button-fg: white;
+ --button-hover-bg: var(--color-primary-hover);
+ --button-padding-x: var(--space-4);
+ --button-padding-y: var(--space-2);
+ --button-radius: var(--radius-default);
+
+ /* Input */
+ --input-bg: var(--color-background);
+ --input-border: var(--color-gray-300);
+ --input-focus-ring: var(--color-primary);
+ --input-padding: var(--space-2) var(--space-3);
+
+ /* Card */
+ --card-bg: var(--color-background);
+ --card-border: var(--color-gray-200);
+ --card-padding: var(--space-4);
+ --card-radius: var(--radius-lg);
+ --card-shadow: var(--shadow-default);
+}
+```
+
+## Dark Mode
+
+Override semantic tokens for dark theme:
+
+```css
+.dark {
+ --color-background: var(--color-gray-900);
+ --color-foreground: var(--color-gray-50);
+ --color-muted: var(--color-gray-800);
+ --color-muted-foreground: var(--color-gray-400);
+ --color-secondary: var(--color-gray-800);
+}
+```
+
+## Naming Convention
+
+```
+--{category}-{item}-{variant}-{state}
+
+Examples:
+--color-primary # category-item
+--color-primary-hover # category-item-state
+--button-bg-hover # component-property-state
+--space-section-sm # category-semantic-variant
+```
+
+## Categories
+
+| Category | Examples |
+|----------|----------|
+| color | primary, secondary, muted, destructive |
+| space | 1, 2, 4, 8, section, component |
+| font-size | xs, sm, base, lg, xl |
+| radius | sm, default, lg, full |
+| shadow | sm, default, lg |
+| duration | fast, normal, slow |
+
+## File Organization
+
+```
+tokens/
+├── primitives.css # Raw values
+├── semantic.css # Purpose aliases
+├── components.css # Component tokens
+└── index.css # Imports all
+```
+
+Or single file with layer comments:
+
+```css
+/* === PRIMITIVES === */
+:root { ... }
+
+/* === SEMANTIC === */
+:root { ... }
+
+/* === COMPONENTS === */
+:root { ... }
+
+/* === DARK MODE === */
+.dark { ... }
+```
+
+## Migration from Flat Tokens
+
+Before (flat):
+```css
+--button-primary-bg: #2563EB;
+--button-secondary-bg: #F3F4F6;
+```
+
+After (three-layer):
+```css
+/* Primitive */
+--color-blue-600: #2563EB;
+--color-gray-100: #F3F4F6;
+
+/* Semantic */
+--color-primary: var(--color-blue-600);
+--color-secondary: var(--color-gray-100);
+
+/* Component */
+--button-bg: var(--color-primary);
+--button-secondary-bg: var(--color-secondary);
+```
+
+## W3C DTCG Alignment
+
+Token JSON format (W3C Design Tokens Community Group):
+
+```json
+{
+ "color": {
+ "blue": {
+ "600": {
+ "$value": "#2563EB",
+ "$type": "color"
+ }
+ }
+ }
+}
+```
diff --git a/.agents/skills/design-system/scripts/embed-tokens.cjs b/.agents/skills/design-system/scripts/embed-tokens.cjs
new file mode 100644
index 0000000..419c104
--- /dev/null
+++ b/.agents/skills/design-system/scripts/embed-tokens.cjs
@@ -0,0 +1,99 @@
+#!/usr/bin/env node
+/**
+ * embed-tokens.cjs
+ * Reads design-tokens.css and outputs embeddable inline CSS.
+ * Use when generating standalone HTML files (infographics, slides, etc.)
+ *
+ * Usage:
+ * node embed-tokens.cjs # Output full CSS
+ * node embed-tokens.cjs --minimal # Output only commonly used tokens
+ * node embed-tokens.cjs --style # Wrap in `;
+ } else {
+ output = `/* Design Tokens (embedded for standalone HTML) */\n${output}`;
+ }
+
+ console.log(output);
+} catch (err) {
+ console.error(`Error reading tokens: ${err.message}`);
+ process.exit(1);
+}
diff --git a/.agents/skills/design-system/scripts/fetch-background.py b/.agents/skills/design-system/scripts/fetch-background.py
new file mode 100644
index 0000000..bcbd357
--- /dev/null
+++ b/.agents/skills/design-system/scripts/fetch-background.py
@@ -0,0 +1,317 @@
+#!/usr/bin/env python3
+"""
+Background Image Fetcher
+Fetches real images from Pexels for slide backgrounds.
+Uses web scraping (no API key required) or WebFetch tool integration.
+"""
+
+import json
+import csv
+import re
+import sys
+from pathlib import Path
+
+# Project root relative to this script
+PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent
+TOKENS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json'
+BACKGROUNDS_CSV = Path(__file__).parent.parent / 'data' / 'slide-backgrounds.csv'
+
+
+def resolve_token_reference(ref: str, tokens: dict) -> str:
+ """Resolve token reference like {primitive.color.ocean-blue.500} to hex value."""
+ if not ref or not ref.startswith('{') or not ref.endswith('}'):
+ return ref # Already a value, not a reference
+
+ # Parse reference: {primitive.color.ocean-blue.500}
+ path = ref[1:-1].split('.') # ['primitive', 'color', 'ocean-blue', '500']
+ current = tokens
+ for key in path:
+ if isinstance(current, dict):
+ current = current.get(key)
+ else:
+ return None # Invalid path
+ # Return $value if it's a token object
+ if isinstance(current, dict) and '$value' in current:
+ return current['$value']
+ return current
+
+
+def load_brand_colors():
+ """Load colors from assets/design-tokens.json for overlay gradients.
+
+ Resolves semantic token references to actual hex values.
+ """
+ try:
+ with open(TOKENS_PATH) as f:
+ tokens = json.load(f)
+
+ colors = tokens.get('primitive', {}).get('color', {})
+ semantic = tokens.get('semantic', {}).get('color', {})
+
+ # Try semantic tokens first (preferred) - resolve references
+ if semantic:
+ primary_ref = semantic.get('primary', {}).get('$value')
+ secondary_ref = semantic.get('secondary', {}).get('$value')
+ accent_ref = semantic.get('accent', {}).get('$value')
+ background_ref = semantic.get('background', {}).get('$value')
+
+ primary = resolve_token_reference(primary_ref, tokens)
+ secondary = resolve_token_reference(secondary_ref, tokens)
+ accent = resolve_token_reference(accent_ref, tokens)
+ background = resolve_token_reference(background_ref, tokens)
+
+ if primary and secondary:
+ return {
+ 'primary': primary,
+ 'secondary': secondary,
+ 'accent': accent or primary,
+ 'background': background or '#0D0D0D',
+ }
+
+ # Fallback: find first color palette with 500 value (primary)
+ primary_keys = ['ocean-blue', 'coral', 'blue', 'primary']
+ secondary_keys = ['golden-amber', 'purple', 'amber', 'secondary']
+ accent_keys = ['emerald', 'mint', 'green', 'accent']
+
+ primary_color = None
+ secondary_color = None
+ accent_color = None
+
+ for key in primary_keys:
+ if key in colors and isinstance(colors[key], dict):
+ primary_color = colors[key].get('500', {}).get('$value')
+ if primary_color:
+ break
+
+ for key in secondary_keys:
+ if key in colors and isinstance(colors[key], dict):
+ secondary_color = colors[key].get('500', {}).get('$value')
+ if secondary_color:
+ break
+
+ for key in accent_keys:
+ if key in colors and isinstance(colors[key], dict):
+ accent_color = colors[key].get('500', {}).get('$value')
+ if accent_color:
+ break
+
+ background = colors.get('dark', {}).get('800', {}).get('$value', '#0D0D0D')
+
+ return {
+ 'primary': primary_color or '#3B82F6',
+ 'secondary': secondary_color or '#F59E0B',
+ 'accent': accent_color or '#10B981',
+ 'background': background,
+ }
+ except (FileNotFoundError, KeyError, TypeError):
+ # Fallback defaults
+ return {
+ 'primary': '#3B82F6',
+ 'secondary': '#F59E0B',
+ 'accent': '#10B981',
+ 'background': '#0D0D0D',
+ }
+
+
+def load_backgrounds_config():
+ """Load background configuration from CSV."""
+ config = {}
+ try:
+ with open(BACKGROUNDS_CSV, newline='') as f:
+ reader = csv.DictReader(f)
+ for row in reader:
+ config[row['slide_type']] = row
+ except FileNotFoundError:
+ print(f"Warning: {BACKGROUNDS_CSV} not found")
+ return config
+
+
+def get_overlay_css(style: str, brand_colors: dict) -> str:
+ """Generate overlay CSS using brand colors from design-tokens.json."""
+ overlays = {
+ 'gradient-dark': f"linear-gradient(135deg, {brand_colors['background']}E6, {brand_colors['background']}B3)",
+ 'gradient-brand': f"linear-gradient(135deg, {brand_colors['primary']}CC, {brand_colors['secondary']}99)",
+ 'gradient-accent': f"linear-gradient(135deg, {brand_colors['accent']}99, transparent)",
+ 'blur-dark': f"rgba(13,13,13,0.8)",
+ 'desaturate-dark': f"rgba(13,13,13,0.7)",
+ }
+ return overlays.get(style, overlays['gradient-dark'])
+
+
+# Curated high-quality images from Pexels (free to use, pre-selected for brand aesthetic)
+CURATED_IMAGES = {
+ 'hero': [
+ 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/2582937/pexels-photo-2582937.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/1089438/pexels-photo-1089438.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+ 'vision': [
+ 'https://images.pexels.com/photos/3183150/pexels-photo-3183150.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/3182812/pexels-photo-3182812.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/3184291/pexels-photo-3184291.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+ 'team': [
+ 'https://images.pexels.com/photos/3184418/pexels-photo-3184418.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/3184338/pexels-photo-3184338.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/3182773/pexels-photo-3182773.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+ 'testimonial': [
+ 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/1181622/pexels-photo-1181622.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+ 'cta': [
+ 'https://images.pexels.com/photos/3184339/pexels-photo-3184339.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/3184298/pexels-photo-3184298.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+ 'problem': [
+ 'https://images.pexels.com/photos/3760529/pexels-photo-3760529.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/897817/pexels-photo-897817.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+ 'solution': [
+ 'https://images.pexels.com/photos/3184292/pexels-photo-3184292.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/3184644/pexels-photo-3184644.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+ 'hook': [
+ 'https://images.pexels.com/photos/2582937/pexels-photo-2582937.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/1089438/pexels-photo-1089438.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+ 'social': [
+ 'https://images.pexels.com/photos/3184360/pexels-photo-3184360.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+ 'demo': [
+ 'https://images.pexels.com/photos/1181675/pexels-photo-1181675.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ 'https://images.pexels.com/photos/3861958/pexels-photo-3861958.jpeg?auto=compress&cs=tinysrgb&w=1920',
+ ],
+}
+
+
+def get_curated_images(slide_type: str) -> list:
+ """Get curated images for slide type."""
+ return CURATED_IMAGES.get(slide_type, CURATED_IMAGES.get('hero', []))
+
+
+def get_pexels_search_url(keywords: str) -> str:
+ """Generate Pexels search URL for manual lookup."""
+ import urllib.parse
+ return f"https://www.pexels.com/search/{urllib.parse.quote(keywords)}/"
+
+
+def get_background_image(slide_type: str) -> dict:
+ """
+ Get curated image matching slide type and brand aesthetic.
+ Uses pre-selected Pexels images (no API/scraping needed).
+ """
+ brand_colors = load_brand_colors()
+ config = load_backgrounds_config()
+
+ slide_config = config.get(slide_type)
+ overlay_style = 'gradient-dark'
+ keywords = slide_type
+
+ if slide_config:
+ keywords = slide_config.get('search_keywords', slide_config.get('image_category', slide_type))
+ overlay_style = slide_config.get('overlay_style', 'gradient-dark')
+
+ # Get curated images
+ urls = get_curated_images(slide_type)
+ if urls:
+ return {
+ 'url': urls[0],
+ 'all_urls': urls,
+ 'overlay': get_overlay_css(overlay_style, brand_colors),
+ 'attribution': 'Photo from Pexels (free to use)',
+ 'source': 'pexels-curated',
+ 'search_url': get_pexels_search_url(keywords),
+ }
+
+ # Fallback: provide search URL for manual selection
+ return {
+ 'url': None,
+ 'overlay': get_overlay_css(overlay_style, brand_colors),
+ 'keywords': keywords,
+ 'search_url': get_pexels_search_url(keywords),
+ 'available_types': list(CURATED_IMAGES.keys()),
+ }
+
+
+def generate_css_for_background(result: dict, slide_class: str = '.slide-with-bg') -> str:
+ """Generate CSS for a background slide."""
+ if not result.get('url'):
+ search_url = result.get('search_url', '')
+ return f"""/* No image scraped. Search manually: {search_url} */
+/* Overlay ready: {result.get('overlay', 'gradient-dark')} */
+"""
+
+ return f"""{slide_class} {{
+ background-image: url('{result['url']}');
+ background-size: cover;
+ background-position: center;
+ position: relative;
+}}
+
+{slide_class}::before {{
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: {result['overlay']};
+}}
+
+{slide_class} .content {{
+ position: relative;
+ z-index: 1;
+}}
+
+/* {result.get('attribution', 'Pexels')} - {result.get('search_url', '')} */
+"""
+
+
+def main():
+ """CLI entry point."""
+ import argparse
+
+ parser = argparse.ArgumentParser(description='Get background images for slides')
+ parser.add_argument('slide_type', nargs='?', help='Slide type (hero, vision, team, etc.)')
+ parser.add_argument('--list', action='store_true', help='List available slide types')
+ parser.add_argument('--css', action='store_true', help='Output CSS for the background')
+ parser.add_argument('--json', action='store_true', help='Output JSON')
+ parser.add_argument('--colors', action='store_true', help='Show brand colors')
+ parser.add_argument('--all', action='store_true', help='Show all curated URLs')
+
+ args = parser.parse_args()
+
+ if args.colors:
+ colors = load_brand_colors()
+ print("\nBrand Colors (from design-tokens.json):")
+ for name, value in colors.items():
+ print(f" {name}: {value}")
+ return
+
+ if args.list:
+ print("\nAvailable slide types (curated images):")
+ for slide_type, urls in CURATED_IMAGES.items():
+ print(f" {slide_type}: {len(urls)} images")
+ return
+
+ if not args.slide_type:
+ parser.print_help()
+ return
+
+ result = get_background_image(args.slide_type)
+
+ if args.json:
+ print(json.dumps(result, indent=2))
+ elif args.css:
+ print(generate_css_for_background(result))
+ elif args.all:
+ print(f"\nAll images for '{args.slide_type}':")
+ for i, url in enumerate(result.get('all_urls', []), 1):
+ print(f" {i}. {url}")
+ else:
+ print(f"\nImage URL: {result['url']}")
+ print(f"Alternatives: {len(result.get('all_urls', []))} available (use --all)")
+ print(f"Overlay: {result['overlay']}")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.agents/skills/design-system/scripts/generate-slide.py b/.agents/skills/design-system/scripts/generate-slide.py
new file mode 100644
index 0000000..2de390d
--- /dev/null
+++ b/.agents/skills/design-system/scripts/generate-slide.py
@@ -0,0 +1,770 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Slide Generator - Generates HTML slides using design tokens
+ALL styles MUST use CSS variables from design-tokens.css
+NO hardcoded colors, fonts, or spacing allowed
+"""
+
+import argparse
+import json
+from html import escape
+from pathlib import Path
+from datetime import datetime
+
+
+def _e(value, default=''):
+ """HTML-escape a user-supplied value for safe embedding in HTML content."""
+ return escape(str(value if value is not None else default))
+
+
+def _safe_url(url, default='#'):
+ """Validate and escape a URL for use in href attributes.
+
+ Only allows http://, https://, #, and / schemes to prevent
+ javascript: URI injection (CWE-79).
+ """
+ if url and str(url).strip().lower().startswith(('http://', 'https://', '#', '/')):
+ return escape(str(url), quote=True)
+ return default
+
+# Paths
+SCRIPT_DIR = Path(__file__).parent
+DATA_DIR = SCRIPT_DIR.parent / "data"
+TOKENS_CSS = Path(__file__).resolve().parents[4] / "assets" / "design-tokens.css"
+TOKENS_JSON = Path(__file__).resolve().parents[4] / "assets" / "design-tokens.json"
+OUTPUT_DIR = Path(__file__).resolve().parents[4] / "assets" / "designs" / "slides"
+
+# ============ BRAND-COMPLIANT SLIDE TEMPLATE ============
+# ALL values reference CSS variables from design-tokens.css
+
+SLIDE_TEMPLATE = '''
+
+
+
+
+
{title}
+
+
+
+
+
+
+
+
+
+
+
+
+ {slides_content}
+
+
+
+'''
+
+
+# ============ SLIDE GENERATORS ============
+
+def generate_title_slide(data):
+ """Title slide with gradient headline"""
+ return f'''
+
+ {_e(data.get('badge', 'Pitch Deck'))}
+ {_e(data.get('title', 'Your Title Here'))}
+ {_e(data.get('subtitle', 'Your compelling subtitle'))}
+
+
+
+ '''
+
+
+def generate_problem_slide(data):
+ """Problem statement slide using PAS formula"""
+ return f'''
+
+ The Problem
+ {_e(data.get('headline', 'The problem your audience faces'))}
+
+
+
01
+
{_e(data.get('pain_1_title', 'Pain Point 1'))}
+
{_e(data.get('pain_1_desc', 'Description of the first pain point'))}
+
+
+
02
+
{_e(data.get('pain_2_title', 'Pain Point 2'))}
+
{_e(data.get('pain_2_desc', 'Description of the second pain point'))}
+
+
+
03
+
{_e(data.get('pain_3_title', 'Pain Point 3'))}
+
{_e(data.get('pain_3_desc', 'Description of the third pain point'))}
+
+
+
+
+ '''
+
+
+def generate_solution_slide(data):
+ """Solution slide with feature highlights"""
+ return f'''
+
+ The Solution
+ {_e(data.get('headline', 'How we solve this'))}
+
+
+
+
✓
+
+
{_e(data.get('feature_1_title', 'Feature 1'))}
+
{_e(data.get('feature_1_desc', 'Description of feature 1'))}
+
+
+
+
✓
+
+
{_e(data.get('feature_2_title', 'Feature 2'))}
+
{_e(data.get('feature_2_desc', 'Description of feature 2'))}
+
+
+
+
✓
+
+
{_e(data.get('feature_3_title', 'Feature 3'))}
+
{_e(data.get('feature_3_desc', 'Description of feature 3'))}
+
+
+
+
+
+
◆
+
Product screenshot or demo
+
+
+
+
+
+ '''
+
+
+def generate_metrics_slide(data):
+ """Traction/metrics slide with large numbers"""
+ metrics = data.get('metrics', [
+ {'value': '10K+', 'label': 'Active Users'},
+ {'value': '95%', 'label': 'Retention Rate'},
+ {'value': '3x', 'label': 'Revenue Growth'},
+ {'value': '$2M', 'label': 'ARR'}
+ ])
+
+ metrics_html = ''.join([f'''
+
+
{_e(m.get('value', ''))}
+
{_e(m.get('label', ''))}
+
+ ''' for m in metrics[:4]])
+
+ return f'''
+
+ Traction
+ {_e(data.get('headline', 'Our Growth'))}
+
+ {metrics_html}
+
+
+
+ '''
+
+
+def generate_chart_slide(data):
+ """Chart slide with CSS bar chart"""
+ bars = data.get('bars', [
+ {'label': 'Q1', 'value': 40},
+ {'label': 'Q2', 'value': 60},
+ {'label': 'Q3', 'value': 80},
+ {'label': 'Q4', 'value': 100}
+ ])
+
+ bars_html = ''.join([f'''
+
+ {_e(b.get('display', str(b.get('value', 0)) + '%'))}
+ {_e(b.get('label', ''))}
+
+ ''' for b in bars])
+
+ return f'''
+
+ {_e(data.get('badge', 'Growth'))}
+ {_e(data.get('headline', 'Revenue Growth'))}
+
+
{_e(data.get('chart_title', 'Quarterly Revenue'))}
+
+ {bars_html}
+
+
+
+
+ '''
+
+
+def generate_testimonial_slide(data):
+ """Social proof slide"""
+ return f'''
+
+ What They Say
+
+
"{_e(data.get('quote', 'This product changed how we work. Incredible results.'))}"
+
{_e(data.get('author', 'Jane Doe'))}
+
{_e(data.get('role', 'CEO, Example Company'))}
+
+
+
+ '''
+
+
+def generate_cta_slide(data):
+ """Closing CTA slide"""
+ return f'''
+
+ {_e(data.get('headline', 'Ready to get started?'))}
+ {_e(data.get('subheadline', 'Join thousands of teams already using our solution.'))}
+
+
+
+ '''
+
+
+# Slide type mapping
+SLIDE_GENERATORS = {
+ 'title': generate_title_slide,
+ 'problem': generate_problem_slide,
+ 'solution': generate_solution_slide,
+ 'metrics': generate_metrics_slide,
+ 'traction': generate_metrics_slide,
+ 'chart': generate_chart_slide,
+ 'testimonial': generate_testimonial_slide,
+ 'cta': generate_cta_slide,
+ 'closing': generate_cta_slide
+}
+
+
+def generate_deck(slides_data, title="Pitch Deck"):
+ """Generate complete deck from slide data list"""
+ slides_html = ""
+ for slide in slides_data:
+ slide_type = slide.get('type', 'title')
+ generator = SLIDE_GENERATORS.get(slide_type)
+ if generator:
+ slides_html += generator(slide)
+ else:
+ print(f"Warning: Unknown slide type '{slide_type}'")
+
+ # Calculate relative path to tokens CSS
+ tokens_rel_path = "../../../assets/design-tokens.css"
+
+ return SLIDE_TEMPLATE.format(
+ title=escape(str(title)),
+ tokens_css_path=tokens_rel_path,
+ slides_content=slides_html
+ )
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Generate brand-compliant slides")
+ parser.add_argument("--json", "-j", help="JSON file with slide data")
+ parser.add_argument("--output", "-o", help="Output HTML file path")
+ parser.add_argument("--demo", action="store_true", help="Generate demo deck")
+
+ args = parser.parse_args()
+
+ if args.demo:
+ # Demo deck showcasing all slide types
+ demo_slides = [
+ {
+ 'type': 'title',
+ 'badge': 'Investor Deck 2024',
+ 'title': 'ClaudeKit Marketing',
+ 'subtitle': 'Your AI marketing team. Always on.',
+ 'cta': 'Join Waitlist',
+ 'secondary_cta': 'See Demo',
+ 'company': 'ClaudeKit',
+ 'date': 'December 2024'
+ },
+ {
+ 'type': 'problem',
+ 'headline': 'Marketing teams are drowning',
+ 'pain_1_title': 'Content Overload',
+ 'pain_1_desc': 'Need to produce 10x content with same headcount',
+ 'pain_2_title': 'Tool Fatigue',
+ 'pain_2_desc': '15+ tools that don\'t talk to each other',
+ 'pain_3_title': 'No Time to Think',
+ 'pain_3_desc': 'Strategy suffers when execution consumes all hours',
+ 'company': 'ClaudeKit',
+ 'page': '2'
+ },
+ {
+ 'type': 'solution',
+ 'headline': 'AI agents that actually get marketing',
+ 'feature_1_title': 'Content Creation',
+ 'feature_1_desc': 'Blog posts, social, email - all on brand, all on time',
+ 'feature_2_title': 'Campaign Management',
+ 'feature_2_desc': 'Multi-channel orchestration with one command',
+ 'feature_3_title': 'Analytics & Insights',
+ 'feature_3_desc': 'Real-time optimization without the spreadsheets',
+ 'company': 'ClaudeKit',
+ 'page': '3'
+ },
+ {
+ 'type': 'metrics',
+ 'headline': 'Early traction speaks volumes',
+ 'metrics': [
+ {'value': '500+', 'label': 'Beta Users'},
+ {'value': '85%', 'label': 'Weekly Active'},
+ {'value': '4.9', 'label': 'NPS Score'},
+ {'value': '50hrs', 'label': 'Saved/Week'}
+ ],
+ 'company': 'ClaudeKit',
+ 'page': '4'
+ },
+ {
+ 'type': 'chart',
+ 'badge': 'Revenue',
+ 'headline': 'Growing month over month',
+ 'chart_title': 'MRR Growth ($K)',
+ 'bars': [
+ {'label': 'Sep', 'value': 20, 'display': '$5K'},
+ {'label': 'Oct', 'value': 40, 'display': '$12K'},
+ {'label': 'Nov', 'value': 70, 'display': '$28K'},
+ {'label': 'Dec', 'value': 100, 'display': '$45K'}
+ ],
+ 'company': 'ClaudeKit',
+ 'page': '5'
+ },
+ {
+ 'type': 'testimonial',
+ 'quote': 'ClaudeKit replaced 3 tools and 2 contractors. Our content output tripled while costs dropped 60%.',
+ 'author': 'Sarah Chen',
+ 'role': 'Head of Marketing, TechStartup',
+ 'company': 'ClaudeKit',
+ 'page': '6'
+ },
+ {
+ 'type': 'cta',
+ 'headline': 'Ship campaigns while you sleep',
+ 'subheadline': 'Early access available. Limited spots.',
+ 'cta': 'Join the Waitlist',
+ 'contact': 'hello@claudekit.ai',
+ 'website': 'claudekit.ai'
+ }
+ ]
+
+ html = generate_deck(demo_slides, "ClaudeKit Marketing - Pitch Deck")
+
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
+ output_path = OUTPUT_DIR / f"demo-pitch-{datetime.now().strftime('%y%m%d')}.html"
+ output_path.write_text(html, encoding='utf-8')
+ print(f"Demo deck generated: {output_path}")
+
+ elif args.json:
+ with open(args.json, 'r') as f:
+ data = json.load(f)
+
+ html = generate_deck(data.get('slides', []), data.get('title', 'Presentation'))
+
+ output_path = Path(args.output) if args.output else OUTPUT_DIR / f"deck-{datetime.now().strftime('%y%m%d-%H%M')}.html"
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ output_path.write_text(html, encoding='utf-8')
+ print(f"Deck generated: {output_path}")
+
+ else:
+ parser.print_help()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.agents/skills/design-system/scripts/generate-tokens.cjs b/.agents/skills/design-system/scripts/generate-tokens.cjs
new file mode 100644
index 0000000..73cc7d7
--- /dev/null
+++ b/.agents/skills/design-system/scripts/generate-tokens.cjs
@@ -0,0 +1,205 @@
+#!/usr/bin/env node
+/**
+ * Generate CSS variables from design tokens JSON
+ *
+ * Usage:
+ * node generate-tokens.cjs --config tokens.json -o tokens.css
+ * node generate-tokens.cjs --config tokens.json --format tailwind
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+/**
+ * Parse command line arguments
+ */
+function parseArgs() {
+ const args = process.argv.slice(2);
+ const options = {
+ config: null,
+ output: null,
+ format: 'css' // css | tailwind
+ };
+
+ for (let i = 0; i < args.length; i++) {
+ if (args[i] === '--config' || args[i] === '-c') {
+ options.config = args[++i];
+ } else if (args[i] === '--output' || args[i] === '-o') {
+ options.output = args[++i];
+ } else if (args[i] === '--format' || args[i] === '-f') {
+ options.format = args[++i];
+ } else if (args[i] === '--help' || args[i] === '-h') {
+ console.log(`
+Usage: node generate-tokens.cjs [options]
+
+Options:
+ -c, --config
Input JSON token file (required)
+ -o, --output Output file (default: stdout)
+ -f, --format Output format: css | tailwind (default: css)
+ -h, --help Show this help
+ `);
+ process.exit(0);
+ }
+ }
+
+ return options;
+}
+
+/**
+ * Resolve token references like {primitive.color.blue.600}
+ */
+function resolveReference(value, tokens) {
+ if (typeof value !== 'string' || !value.startsWith('{')) {
+ return value;
+ }
+
+ const path = value.slice(1, -1).split('.');
+ let result = tokens;
+
+ for (const key of path) {
+ result = result?.[key];
+ }
+
+ if (result?.$value) {
+ return resolveReference(result.$value, tokens);
+ }
+
+ return result || value;
+}
+
+/**
+ * Convert token name to CSS variable name
+ */
+function toCssVarName(path) {
+ return '--' + path.join('-').replace(/\./g, '-');
+}
+
+/**
+ * Flatten tokens into CSS variables
+ */
+function flattenTokens(obj, tokens, prefix = [], result = {}) {
+ for (const [key, value] of Object.entries(obj)) {
+ const currentPath = [...prefix, key];
+
+ if (value && typeof value === 'object') {
+ if (value.$value !== undefined) {
+ // This is a token
+ const cssVar = toCssVarName(currentPath);
+ const resolvedValue = resolveReference(value.$value, tokens);
+ result[cssVar] = resolvedValue;
+ } else {
+ // Recurse into nested object
+ flattenTokens(value, tokens, currentPath, result);
+ }
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Generate CSS output
+ */
+function generateCSS(tokens) {
+ const primitive = flattenTokens(tokens.primitive || {}, tokens, ['primitive']);
+ const semantic = flattenTokens(tokens.semantic || {}, tokens, []);
+ const component = flattenTokens(tokens.component || {}, tokens, []);
+ const darkSemantic = flattenTokens(tokens.dark?.semantic || {}, tokens, []);
+
+ let css = `/* Design Tokens - Auto-generated */
+/* Do not edit directly - modify tokens.json instead */
+
+/* === PRIMITIVES === */
+:root {
+${Object.entries(primitive).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
+}
+
+/* === SEMANTIC === */
+:root {
+${Object.entries(semantic).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
+}
+
+/* === COMPONENTS === */
+:root {
+${Object.entries(component).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
+}
+`;
+
+ if (Object.keys(darkSemantic).length > 0) {
+ css += `
+/* === DARK MODE === */
+.dark {
+${Object.entries(darkSemantic).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
+}
+`;
+ }
+
+ return css;
+}
+
+/**
+ * Generate Tailwind config output
+ */
+function generateTailwind(tokens) {
+ const semantic = flattenTokens(tokens.semantic || {}, tokens, []);
+
+ // Extract colors for Tailwind
+ const colors = {};
+ for (const [key, value] of Object.entries(semantic)) {
+ if (key.includes('color')) {
+ const name = key.replace('--color-', '').replace(/-/g, '.');
+ colors[name] = `var(${key})`;
+ }
+ }
+
+ return `// Tailwind color config - Auto-generated
+// Add to tailwind.config.ts theme.extend.colors
+
+module.exports = {
+ colors: ${JSON.stringify(colors, null, 2).replace(/"/g, "'")}
+};
+`;
+}
+
+/**
+ * Main
+ */
+function main() {
+ const options = parseArgs();
+
+ if (!options.config) {
+ console.error('Error: --config is required');
+ process.exit(1);
+ }
+
+ // Resolve config path
+ const configPath = path.resolve(process.cwd(), options.config);
+
+ if (!fs.existsSync(configPath)) {
+ console.error(`Error: Config file not found: ${configPath}`);
+ process.exit(1);
+ }
+
+ // Read and parse tokens
+ const tokens = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
+
+ // Generate output
+ let output;
+ if (options.format === 'tailwind') {
+ output = generateTailwind(tokens);
+ } else {
+ output = generateCSS(tokens);
+ }
+
+ // Write output
+ if (options.output) {
+ const outputPath = path.resolve(process.cwd(), options.output);
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
+ fs.writeFileSync(outputPath, output);
+ console.log(`Generated: ${outputPath}`);
+ } else {
+ console.log(output);
+ }
+}
+
+main();
diff --git a/.agents/skills/design-system/scripts/html-token-validator.py b/.agents/skills/design-system/scripts/html-token-validator.py
new file mode 100644
index 0000000..a722498
--- /dev/null
+++ b/.agents/skills/design-system/scripts/html-token-validator.py
@@ -0,0 +1,327 @@
+#!/usr/bin/env python3
+"""
+HTML Design Token Validator
+Ensures all HTML assets (slides, infographics, etc.) use design tokens.
+Source of truth: assets/design-tokens.css
+
+Usage:
+ python html-token-validator.py # Validate all HTML assets
+ python html-token-validator.py --type slides # Validate only slides
+ python html-token-validator.py --type infographics # Validate only infographics
+ python html-token-validator.py path/to/file.html # Validate specific file
+ python html-token-validator.py --fix # Auto-fix issues (WIP)
+"""
+
+import re
+import json
+import sys
+from pathlib import Path
+from typing import Dict, List, Tuple, Optional
+
+# Project root relative to this script
+PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent
+TOKENS_JSON_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json'
+TOKENS_CSS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.css'
+
+# Asset directories to validate
+ASSET_DIRS = {
+ 'slides': PROJECT_ROOT / 'assets' / 'designs' / 'slides',
+ 'infographics': PROJECT_ROOT / 'assets' / 'infographics',
+}
+
+# Patterns that indicate hardcoded values (should use tokens)
+FORBIDDEN_PATTERNS = [
+ (r'#[0-9A-Fa-f]{3,8}\b', 'hex color'),
+ (r'rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)', 'rgb color'),
+ (r'rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)', 'rgba color'),
+ (r'hsl\([^)]+\)', 'hsl color'),
+ (r"font-family:\s*'[^v][^a][^r][^']*',", 'hardcoded font'), # Exclude var()
+ (r'font-family:\s*"[^v][^a][^r][^"]*",', 'hardcoded font'),
+]
+
+# Allowed rgba patterns (brand colors with transparency - CSS limitation)
+# These are derived from brand tokens but need rgba for transparency
+ALLOWED_RGBA_PATTERNS = [
+ r'rgba\(\s*59\s*,\s*130\s*,\s*246', # --color-primary (#3B82F6)
+ r'rgba\(\s*245\s*,\s*158\s*,\s*11', # --color-secondary (#F59E0B)
+ r'rgba\(\s*16\s*,\s*185\s*,\s*129', # --color-accent (#10B981)
+ r'rgba\(\s*20\s*,\s*184\s*,\s*166', # --color-accent alt (#14B8A6)
+ r'rgba\(\s*0\s*,\s*0\s*,\s*0', # black transparency (common)
+ r'rgba\(\s*255\s*,\s*255\s*,\s*255', # white transparency (common)
+ r'rgba\(\s*15\s*,\s*23\s*,\s*42', # --color-surface (#0F172A)
+ r'rgba\(\s*7\s*,\s*11\s*,\s*20', # --color-background (#070B14)
+]
+
+# Allowed exceptions (external images, etc.)
+ALLOWED_EXCEPTIONS = [
+ 'pexels.com', 'unsplash.com', 'youtube.com', 'ytimg.com',
+ 'googlefonts', 'fonts.googleapis.com', 'fonts.gstatic.com',
+]
+
+
+class ValidationResult:
+ """Validation result for a single file."""
+ def __init__(self, file_path: Path):
+ self.file_path = file_path
+ self.errors: List[str] = []
+ self.warnings: List[str] = []
+ self.passed = True
+
+ def add_error(self, msg: str):
+ self.errors.append(msg)
+ self.passed = False
+
+ def add_warning(self, msg: str):
+ self.warnings.append(msg)
+
+
+def load_css_variables() -> Dict[str, str]:
+ """Load CSS variables from design-tokens.css."""
+ variables = {}
+ if TOKENS_CSS_PATH.exists():
+ content = TOKENS_CSS_PATH.read_text()
+ # Extract --var-name: value patterns
+ for match in re.finditer(r'(--[\w-]+):\s*([^;]+);', content):
+ variables[match.group(1)] = match.group(2).strip()
+ return variables
+
+
+def is_inside_block(content: str, match_pos: int, open_tag: str, close_tag: str) -> bool:
+ """Check if position is inside a specific HTML block."""
+ pre = content[:match_pos]
+ tag_open = pre.rfind(open_tag)
+ tag_close = pre.rfind(close_tag)
+ return tag_open > tag_close
+
+
+def is_allowed_exception(context: str) -> bool:
+ """Check if the hardcoded value is in an allowed exception context."""
+ context_lower = context.lower()
+ return any(exc in context_lower for exc in ALLOWED_EXCEPTIONS)
+
+
+def is_allowed_rgba(match_text: str) -> bool:
+ """Check if rgba pattern uses brand colors (allowed for transparency)."""
+ return any(re.match(pattern, match_text) for pattern in ALLOWED_RGBA_PATTERNS)
+
+
+def get_context(content: str, pos: int, chars: int = 100) -> str:
+ """Get surrounding context for a match position."""
+ start = max(0, pos - chars)
+ end = min(len(content), pos + chars)
+ return content[start:end]
+
+
+def validate_html(content: str, file_path: Path, verbose: bool = False) -> ValidationResult:
+ """
+ Validate HTML content for design token compliance.
+
+ Checks:
+ 1. design-tokens.css import present
+ 2. No hardcoded colors in CSS (except in '):
+ if verbose:
+ result.add_warning(f"Allowed in
+
+
+
+
+
+
+
+
+
+
+
+
+
Title Slide
+
Subtitle or tagline
+
+
+
+
+
+
+
+
+
+
+ 1 / 9
+
+
+
+
+
+