# Styrene Theming System

A comprehensive theming engine for Textual TUIs that derives all colors from a single "phosphex" root color.

## Core Concept: Single-Source Color Derivation

Every color in the UI derives algorithmically from one configurable color called the **phosphex** (named after Warhammer 40k's Mechanicum phosphex weapons). Change the phosphex, and the entire UI shifts cohesively.

```
Phosphex (#39ff14 green)
    ├── bright   (100%) → accent, success, online status
    ├── medium   (60%)  → primary text, warning, pending status
    ├── dim      (35%)  → borders, danger, scanning status
    ├── dark     (20%)  → secondary, offline status
    ├── bg_screen       → near-black with phosphex tint
    └── bg_panel        → slightly lighter with phosphex tint
```

## Architecture

```
src/styrene/themes/
├── __init__.py          # Public exports
├── color_cascade.py     # ColorCascade class + 32 forge world presets
├── semantic.py          # Symbols, patterns, text-style utilities
└── imperial_crt.py      # Legacy static theme (backwards compat)

src/styrene/styles/
└── imperial_crt.tcss    # CSS using $variables, not hardcoded colors

src/styrene/widgets/
├── highlighted_panel.py # Corner-highlighted panel widget
└── animated_status.py   # Animated status indicators
```

## Usage

### Basic Theme Application

```python
from styrene.themes import ColorCascade, generate_all_themes

class MyApp(App):
    def __init__(self):
        super().__init__()
        # Register all 32 forge world themes
        for theme in generate_all_themes().values():
            self.register_theme(theme)
        # Start with Mars (green)
        self.theme = "mars"

    def switch_theme(self, preset_key: str):
        """Switch to a different forge world theme."""
        self.theme = preset_key  # e.g., "xana", "hydraphur", "lucius"
```

### Available Presets (32 Forge Worlds)

| Family | Presets |
|--------|---------|
| Greens | `mars`, `terra`, `graia`, `incaladion` |
| Oranges | `ryza`, `phaeton`, `tigrus`, `estaban` |
| Reds | `sarum`, `cyclothrathe`, `samech`, `xana` |
| Blues | `stygies`, `deimos`, `hydraphur`, `mezoa` |
| Cyans | `agripinaa`, `triplex_phall`, `kiavahr`, `konor` |
| Purples | `lucius`, `voss`, `zhao_arkhad`, `moirae` |
| Pinks | `accatran`, `urdesh` |
| Grays | `metalica`, `vostroya`, `gantz` |
| Yellows | `gryphonne`, `shenlong`, `vanaheim` |

### Accessing Cascade Colors Directly

```python
from styrene.themes import ColorCascade

cascade = ColorCascade.from_preset("mars")
print(cascade.phosphex)    # #39ff14
print(cascade.bright)      # #39ff14 (100%)
print(cascade.medium)      # #22990c (60%)
print(cascade.dim)         # #135907 (35%)
print(cascade.dark)        # #0b3905 (20%)
print(cascade.bg_screen)   # #0a0c0a (near-black with green tint)
```

## Semantic Differentiation (Not Color-Based)

Since all semantic colors derive from the same phosphex, visual differentiation uses **three layers**:

### Layer 1: Symbols

```python
from styrene.themes import format_status, SemanticSymbols

# Status indicators
format_status("online", "CONNECTED")   # "● CONNECTED"
format_status("offline", "DISCONNECTED") # "○ DISCONNECTED"
format_status("pending", "WAITING")    # "◐ WAITING"
format_status("scanning", "DISCOVERY") # "◓ DISCOVERY"

# Mechanicum-style
SemanticSymbols.APPROVED    # ⊕
SemanticSymbols.REJECTED    # ⊗
SemanticSymbols.PROCESSING  # ⊙
SemanticSymbols.IDLE        # ⊚
```

### Layer 2: Text Styles (CSS)

```css
/* imperial_crt.tcss */
.status-online {
    color: $success;
    text-style: bold;
}

.status-offline {
    color: $error;
    text-style: italic strike;
}

Button.-success {
    text-style: bold underline;
}

Button.-warning {
    text-style: bold italic;
}

Button.-danger {
    text-style: reverse;
}
```

### Layer 3: Shade Patterns

```python
from styrene.themes import format_button_label

format_button_label("success", "Confirm")  # "▓ Confirm ▓" (dark shade)
format_button_label("warning", "Caution")  # "▒ Caution ▒" (medium shade)
format_button_label("danger", "Delete")    # "░ Delete ░" (light shade)
```

## CSS Theme Variables

The CSS uses Textual theme variables, NOT hardcoded colors:

```css
/* CORRECT - uses theme variables */
Button {
    background: $surface;
    border: solid $border;
    color: $primary;
}

.status-online {
    color: $success;
}

/* WRONG - hardcoded colors break theming */
Button {
    background: #0d2d0d;
    color: #32cd32;
}
```

### Available Theme Variables

| Variable | ColorCascade Source | Usage |
|----------|---------------------|-------|
| `$primary` | `medium` | Main text, borders |
| `$secondary` | `dark` | Muted elements |
| `$accent` | `bright` | Highlights, corners |
| `$background` | `bg_screen` | Screen background |
| `$surface` | `bg_panel` | Panel backgrounds |
| `$panel` | `dim` | Border color |
| `$success` | `bright` | Online, success |
| `$warning` | `medium` | Pending, warning |
| `$error` | `dim` | Offline, danger |
| `$border` | `border_medium` | Standard borders |

## Animated Indicators

```python
from styrene.widgets.animated_status import (
    AnimatedStatusIndicator,
    ScanningBar,
    PulsingIndicator,
)

# Auto-animates when status is "scanning" or "pending"
indicator = AnimatedStatusIndicator(status="scanning", label="DISCOVERY")
indicator.status = "online"  # Stops animation, shows ● ONLINE

# Bouncing bar for discovery operations
bar = ScanningBar(width=30, bar_width=5)
bar.start()

# Opacity pulsing for processing states
pulse = PulsingIndicator("⬤ PROCESSING")
pulse.start()
```

### Animation Frames

| State | Symbol Frames | Interval |
|-------|---------------|----------|
| Scanning | `⣾⣽⣻⢿⡿⣟⣯⣷` (braille spinner) | 100ms |
| Pending | `◴◷◶◵` (rotating quarter) | 100ms |
| ScanningBar | `██████` bouncing through `░░░░░░` | 50ms |

## HighlightedPanel Widget

Custom panel with corner-highlighted borders:

```python
from styrene.widgets.highlighted_panel import HighlightedPanel

yield HighlightedPanel(
    Label("Content here"),
    Button("Action"),
    title="PANEL TITLE",
)
```

Renders as:
```
┌────── PANEL TITLE ──────┐
│ Content here            │
│ [Action]                │
└─────────────────────────┘
```

Corners (`┌┐└┘` + adjacent segments) use `$accent` (bright), middle borders use `$panel` (dim).

## Live Theme Switching

To enable runtime theme switching:

1. **Register themes at startup:**
```python
for theme in generate_all_themes().values():
    self.register_theme(theme)
```

2. **Switch via `app.theme`:**
```python
self.theme = "xana"  # Switches to blood red
```

3. **For Rich markup panels, refresh manually:**
```python
# Rich markup uses literal colors, not CSS variables
panel.update(f"[{cascade.bright}]Text[/]")
```

## Creating Custom Presets

```python
from styrene.themes.color_cascade import ColorCascade, ForgeWorldPreset, FORGE_WORLD_PRESETS

# Add a custom preset
FORGE_WORLD_PRESETS["custom"] = ForgeWorldPreset(
    name="Custom Pattern",
    phosphex="#ff00ff",  # Magenta
    description="Custom magenta theme"
)

# Use it
cascade = ColorCascade.from_preset("custom")
theme = cascade.to_textual_theme(name="custom")
app.register_theme(theme)
app.theme = "custom"
```

## Summary Table

| Element | Color Source | Differentiator |
|---------|--------------|----------------|
| Success/Online | `bright` | `●` + bold underline + `▓` |
| Warning/Pending | `medium` | `◐` + bold italic + `▒` |
| Danger/Offline | `dim` | `○` + reverse + `░` |
| Scanning | `dim` | `◓` (animated) + underline |
| Primary text | `medium` | — |
| Accent/highlights | `bright` | — |
| Backgrounds | `bg_*` | Phosphex-tinted near-black |
