README.md

# LiveStyle

Atomic CSS-in-Elixir for Phoenix LiveView, inspired by [Meta's StyleX](https://stylexjs.com/).

LiveStyle provides a type-safe, composable styling system with:

- **Atomic CSS**: Each property-value pair becomes a single class
- **Deterministic hashing**: Same styles always produce same class names
- **CSS Variables**: Type-safe design tokens with `css_vars/2`
- **Theming**: Override variables with `css_theme/3`
- **@layer support**: CSS cascade layers for predictable specificity
- **Last-wins merging**: Like StyleX, later styles override earlier ones

## Installation

Add `live_style` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:live_style, "~> 0.7.0"}
  ]
end
```

Add the LiveStyle compiler to your project:

```elixir
def project do
  [
    # ...
    compilers: Mix.compilers() ++ [:live_style]
  ]
end
```

If your tests define LiveStyle modules (e.g., test fixtures with `use LiveStyle.Sheet`),
add the test setup task to your aliases:

```elixir
defp aliases do
  [
    test: ["live_style.setup_tests", "test"]
  ]
end
```

Include the generated CSS in your root layout:

```heex
<link rel="stylesheet" href={~p"/assets/live.css"} />
```

## Configuration

LiveStyle works out of the box with sensible defaults. See `LiveStyle.Config` for all available options.

## Quick Start

```elixir
defmodule MyAppWeb.Components.Button do
  use Phoenix.Component
  use LiveStyle.Sheet

  # Define styles using keyword list syntax
  css_class :base,
    display: "flex",
    align_items: "center",
    padding: "8px 16px",
    border_radius: "8px"

  css_class :primary,
    background_color: css_var({MyApp.Tokens, :fill, :primary}),
    color: "white"

  def button(assigns) do
    ~H"""
    <button class={css_class([:base, :primary])}>
      <%= render_slot(@inner_block) %>
    </button>
    """
  end
end
```

## Module Organization

LiveStyle provides two specialized modules for clearer intent:

### `LiveStyle.Tokens` - Design Tokens

For centralized design tokens (CSS variables, keyframes, themes):

```elixir
defmodule MyApp.Tokens do
  use LiveStyle.Tokens

  # Raw color palette (not themed)
  css_vars :colors,
    white: "#ffffff",
    black: "#000000",
    gray_900: "#111827",
    indigo_600: "#4f46e5"

  # Semantic tokens referencing colors (themed)
  css_vars :semantic,
    text_primary: css_var({:colors, :gray_900}),
    text_inverse: css_var({:colors, :white}),
    fill_primary: css_var({:colors, :indigo_600})

  # Dark theme overrides semantic tokens
  css_theme :semantic, :dark,
    text_primary: css_var({:colors, :white}),
    text_inverse: css_var({:colors, :gray_900}),
    fill_primary: css_var({:colors, :indigo_600})

  css_vars :space,
    sm: "8px",
    md: "16px",
    lg: "24px"

  css_keyframes :spin,
    from: [transform: "rotate(0deg)"],
    to: [transform: "rotate(360deg)"]
end
```

### `LiveStyle.Sheet` - Component Styles

For component-specific styles:

```elixir
defmodule MyApp.Button do
  use Phoenix.Component
  use LiveStyle.Sheet

  css_class :base,
    display: "inline-flex",
    padding: "0.5rem 1rem",
    border_radius: "0.25rem"

  css_class :primary,
    background_color: css_var({MyApp.Tokens, :semantic, :fill_primary}),
    color: css_var({MyApp.Tokens, :semantic, :text_inverse})

  def button(assigns) do
    ~H"""
    <button class={css_class([:base, :primary])}>
      <%= render_slot(@inner_block) %>
    </button>
    """
  end
end
```

## Syntax Options

LiveStyle supports both **keyword list syntax** (recommended) and **map syntax**:

```elixir
# Keyword list syntax (recommended - more idiomatic Elixir)
css_class :button,
  display: "flex",
  padding: "8px"

# Map syntax (also supported)
css_class :button, %{
  display: "flex",
  padding: "8px"
}
```

**Computed keys:** When using `css_const` references or module attributes as keys, you have two options:

```elixir
# Option 1: Map syntax with =>
css_class :responsive,
  font_size: %{
    :default => "1rem",
    css_const({MyApp.Tokens, :breakpoint, :lg}) => "1.5rem"
  }

# Option 2: Tuple list syntax (more consistent with keyword style)
css_class :responsive,
  font_size: [
    {:default, "1rem"},
    {css_const({MyApp.Tokens, :breakpoint, :lg}), "1.5rem"}
  ]
```

Both produce identical CSS output. Use whichever style you prefer.

## Referencing Tokens

Use `css_var/1` to reference CSS variables from other modules:

```elixir
css_class :container,
  padding: css_var({MyApp.Tokens, :space, :md}),
  background_color: css_var({MyApp.Tokens, :semantic, :fill_primary})
```

## Theming

The standard pattern for theming uses two layers:

1. **`:colors`** - Raw color palette (hex values, not themed)
2. **`:semantic`** - Semantic tokens that reference colors via `css_var` (themed)

This separation keeps color values in one place while allowing themes to swap which colors semantic tokens point to.

```elixir
defmodule MyApp.Tokens do
  use LiveStyle.Tokens

  # Raw colors (not themed)
  css_vars :colors,
    white: "#ffffff",
    gray_900: "#111827",
    gray_50: "#f9fafb"

  # Semantic tokens reference colors (themed)
  css_vars :semantic,
    fill_page: css_var({:colors, :white}),
    fill_surface: css_var({:colors, :gray_50}),
    text_primary: css_var({:colors, :gray_900})

  # Theme overrides swap which colors semantics point to
  css_theme :semantic, :dark,
    fill_page: css_var({:colors, :gray_900}),
    fill_surface: css_var({:colors, :gray_900}),
    text_primary: css_var({:colors, :gray_50})
end
```

### Applying Themes

Use `css_theme/1` to apply a theme to a subtree:

```heex
<div class={css_theme({MyApp.Tokens, :semantic, :dark})}>
  <!-- Children use dark theme colors -->
  <.button>I use dark colors</.button>
</div>
```

Components don't need to know about themes - they just reference semantic tokens:

```elixir
css_class :card,
  background: css_var({MyApp.Tokens, :semantic, :fill_page}),
  color: css_var({MyApp.Tokens, :semantic, :text_primary})
```

### Multiple Themes

Define multiple themes for different contexts:

```elixir
# In your tokens module
css_theme :semantic, :dark,
  text_primary: css_var({:colors, :gray_50}),
  fill_page: css_var({:colors, :gray_900})

css_theme :semantic, :high_contrast,
  text_primary: css_var({:colors, :black}),
  fill_page: css_var({:colors, :white})
```

Themes can be nested - inner themes override outer ones:

```heex
<html class={@theme == :dark && css_theme({MyApp.Tokens, :semantic, :dark})}>
  <body>
    <!-- Uses dark or default theme based on @theme -->
  </body>
</html>
```

## Conditional Styles

Use Elixir's boolean logic for conditional styles:

```elixir
def button(assigns) do
  ~H"""
  <button class={css_class([:base, @variant == :primary && :primary, @disabled && :disabled])}>
    <%= render_slot(@inner_block) %>
  </button>
  """
end
```

## Pseudo-classes and Media Queries

LiveStyle uses the StyleX pattern of condition-in-value:

```elixir
css_class :link,
  color: [
    default: "blue",
    ":hover": "darkblue",
    ":focus": "navy"
  ],
  text_decoration: "none"

css_class :container,
  padding: [
    default: "16px",
    "@media (min-width: 768px)": "32px"
  ]
```

## Pseudo-elements

```elixir
css_class :with_before,
  position: "relative",
  "::before": [
    content: "'*'",
    color: "red",
    position: "absolute",
    left: "-1em"
  ]
```

## Style Composition

Include styles from other modules or self-reference within the same module:

```elixir
# External module include
defmodule MyApp.BaseStyles do
  use LiveStyle.Sheet

  css_class :button_base,
    display: "inline-flex",
    padding: "8px 16px",
    cursor: "pointer"
end

defmodule MyApp.Button do
  use LiveStyle.Sheet

  css_class :primary,
    __include__: [{MyApp.BaseStyles, :button_base}],
    background_color: css_var({MyApp.Tokens, :fill, :primary})
end

# Self-reference (same module)
defmodule MyApp.Card do
  use LiveStyle.Sheet

  css_class :base,
    border_radius: "8px",
    padding: "16px"

  css_class :elevated,
    __include__: [:base],
    box_shadow: "0 4px 6px rgba(0,0,0,0.1)"
end
```

## Dynamic Styles

For styles that depend on runtime values, use a function in `css_class/2`:

```elixir
defmodule MyApp.Components do
  use LiveStyle.Sheet

  css_class :dynamic_opacity, fn opacity ->
    [opacity: opacity]
  end

  css_class :dynamic_color, fn r, g, b ->
    [color: "rgb(#{r}, #{g}, #{b})"]
  end
end
```

Dynamic styles return `%LiveStyle.Attrs{}` structs. Spread them in templates:

```heex
<div {css([{:dynamic_opacity, 0.5}])}>
  Faded content
</div>
```

### Merging Multiple Styles

Use `css/1` with a list to merge multiple style sources:

```heex
<div {css([
  :button,
  {:dynamic_color, [255, 0, 0]},
  {:dynamic_size, [100]},
  @is_active && :active
])}>
  Button
</div>
```

The list can contain:
- Atoms (static style names)
- `{atom, args}` tuples (dynamic styles with arguments)
- `nil` or `false` (ignored, useful for conditional styles)

## Keyframes Animations

```elixir
defmodule MyApp.Tokens do
  use LiveStyle.Tokens

  css_keyframes :spin,
    from: [transform: "rotate(0deg)"],
    to: [transform: "rotate(360deg)"]

  css_keyframes :pulse,
    "0%": [opacity: "1"],
    "50%": [opacity: "0.5"],
    "100%": [opacity: "1"]
end

defmodule MyApp.Spinner do
  use LiveStyle.Sheet

  css_class :spinner,
    animation: "#{css_keyframes({MyApp.Tokens, :spin})} 1s linear infinite"
end
```

## Fallback Values

Use `first_that_works/1` for CSS fallbacks:

```elixir
css_class :sticky_header,
  position: first_that_works(["sticky", "-webkit-sticky", "fixed"])
```

## Contextual Selectors (LiveStyle.When)

Style elements based on ancestor, descendant, or sibling state - like StyleX's `stylex.when.*` API:

```elixir
defmodule MyApp.Card do
  use Phoenix.Component
  use LiveStyle.Sheet
  alias LiveStyle.When

  css_class :card_content,
    transform: %{
      :default => "translateX(0)",
      When.ancestor(":hover") => "translateX(10px)"
    }

  def render(assigns) do
    ~H"""
    <div class={LiveStyle.default_marker()}>
      <div class={css_class(:card_content)}>
        Hover the parent to move me
      </div>
    </div>
    """
  end
end
```

> **Note:** When using computed keys like `When.ancestor(":hover")`, you must use map syntax with `=>` arrows. This is an Elixir language requirement, not a LiveStyle limitation.

### Available Selectors

| Function | Description | Generated CSS Pattern |
|----------|-------------|----------------------|
| `ancestor(pseudo)` | Style when ancestor has state | `.class:where(.marker:hover *)` |
| `descendant(pseudo)` | Style when descendant has state | `.class:where(:has(.marker:focus))` |
| `sibling_before(pseudo)` | Style when preceding sibling has state | `.class:where(.marker:hover ~ *)` |
| `sibling_after(pseudo)` | Style when following sibling has state | `.class:where(:has(~ .marker:focus))` |
| `any_sibling(pseudo)` | Style when any sibling has state | Combined selector |

### Custom Markers

Use custom markers to create independent sets of contextual selectors:

```elixir
defmodule MyApp.Table do
  use Phoenix.Component
  use LiveStyle.Sheet
  alias LiveStyle.When

  @row_marker LiveStyle.define_marker(:row)
  @row_hover When.ancestor(":hover", @row_marker)
  @col_hover When.ancestor(":has(td:nth-of-type(2):hover)")

  css_class :cell,
    opacity: [
      {:default, "1"},
      {When.ancestor(":hover"), "0.1"},     # Dim when container hovered
      {@row_hover, "1"},                     # Restore for hovered row
      {":hover", "1"}                        # Restore for direct hover
    ],
    background_color: [
      {:default, "transparent"},
      {@row_hover, "#2266cc77"},
      {@col_hover, "#2266cc77"},
      {":hover", "#2266cc77"}
    ]

  def render(assigns) do
    ~H"""
    <div class={LiveStyle.default_marker()}>
      <table>
        <tr class={@row_marker}>
          <td class={css_class(:cell)}>Cell</td>
        </tr>
      </table>
    </div>
    """
  end
end
```

### Nested Conditions

Combine pseudo-classes with contextual selectors for precise targeting:

```elixir
css_class :cell,
  background_color: [
    {:default, "transparent"},
    # Only apply to nth-child(2) when column 2 is hovered
    {":nth-child(2)", %{
      :default => nil,
      When.ancestor(":has(td:nth-of-type(2):hover)") => "#2266cc77"
    }}
  ]
```

## View Transitions

LiveStyle provides first-class support for the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API), following StyleX's `viewTransitionClass` pattern.

### Basic Usage

```elixir
defmodule MyApp.Tokens do
  use LiveStyle.Tokens

  # Define keyframes for your animations
  css_keyframes :scale_in,
    from: [opacity: "0", transform: "scale(0.8)"],
    to: [opacity: "1", transform: "scale(1)"]

  css_keyframes :scale_out,
    from: [opacity: "1", transform: "scale(1)"],
    to: [opacity: "0", transform: "scale(0.8)"]

  # Define view transitions
  css_view_transition :card,
    old: [
      animation_name: css_keyframes(:scale_out),
      animation_duration: "200ms",
      animation_fill_mode: "both"
    ],
    new: [
      animation_name: css_keyframes(:scale_in),
      animation_duration: "200ms",
      animation_fill_mode: "both"
    ]
end
```

### Available Pseudo-element Keys

| Key | CSS Selector |
|-----|-------------|
| `:old` | `::view-transition-old(name)` |
| `:new` | `::view-transition-new(name)` |
| `:group` | `::view-transition-group(name)` |
| `:image_pair` | `::view-transition-image-pair(name)` |
| `:old_only_child` | `::view-transition-old(name):only-child` |
| `:new_only_child` | `::view-transition-new(name):only-child` |

The `:only-child` variants apply when an element is being added or removed (not replaced), useful for different add/remove vs reorder animations.

### Respecting Reduced Motion

```elixir
css_view_transition :todo,
  old: [
    animation_name: %{
      :default => css_keyframes(:fade_out),
      "@media (prefers-reduced-motion: reduce)" => "none"
    },
    animation_duration: "200ms"
  ]
```

### JavaScript Integration

To enable View Transitions with Phoenix LiveView 1.1.18+, use the `onDocumentPatch` callback:

```javascript
const liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  dom: {
    onDocumentPatch(proceed) {
      if (document.startViewTransition) {
        document.startViewTransition(proceed)
      } else {
        proceed()
      }
    }
  }
})
```

For more advanced usage with transition types, see the [Phoenix LiveView View Transitions documentation](https://hexdocs.pm/phoenix_live_view/live-navigation.html#view-transitions).

### Using in Templates

Add `view-transition-name` to elements you want to animate. The view transition name
must be unique for each element you want to animate independently:

```heex
<li style={"view-transition-name: #{css_view_transition({MyApp.Tokens, :todo})}-#{@item.id}"}>
  <%= @item.text %>
</li>
```

Note: Append a unique identifier (like the item's ID) to create unique transition names
for list items. This allows each item to animate independently during view transitions.

### Browser Support

View Transitions are supported in Chrome 111+, Edge 111+, and Safari 18+. Animations gracefully degrade in unsupported browsers.

## Typed Variables

For advanced use cases like animating gradients:

```elixir
defmodule MyApp.Tokens do
  use LiveStyle.Tokens
  import LiveStyle.Types

  css_vars :anim,
    rotation: angle("0deg"),
    progress: percentage("0%")

  css_vars :theme,
    accent: color("#ff0000")
end
```

This generates CSS `@property` rules that enable CSS to interpolate values.

## CSS Generation

CSS is automatically generated when you compile with the LiveStyle compiler.
You can also generate manually:

```bash
mix live_style.gen.css
```

Or specify a custom output path:

```bash
mix live_style.gen.css -o assets/css/live_style.css
```

## Development Watcher

For development, add the watcher to your Phoenix endpoint:

```elixir
# config/dev.exs
config :my_app, MyAppWeb.Endpoint,
  watchers: [
    live_style: {LiveStyle.Compiler, :run, [:default, ~w(--watch)]}
  ]
```

The watcher monitors the LiveStyle manifest file and regenerates CSS whenever styles are recompiled. This requires the `file_system` dependency (included with `phoenix_live_reload`).

## Generated CSS Structure

```css
/* Typed variables generate @property rules */
@property --v1b5bwzm { syntax: "<angle>"; inherits: true; initial-value: 0deg }

/* All CSS variables in a single :root block */
:root {
  --vc1svcr: #ffffff;
  --v1co3hjg: #111827;
  --v1m8p5kx: #6366f1;
}

/* Keyframes with content-based hashes */
@keyframes x1wc8ddo-B {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* Atomic classes (uses :not(#\#) hack for specificity by default) */
.x1a2a7pz { outline: none }
.xh72szh { padding: var(--vdccikb) }
.x5u4613:not(#\#):hover { border-color: var(--vublb2l) }
```

## API Reference

### Token Macros (via `use LiveStyle.Tokens`)

| Macro | Description |
|-------|-------------|
| `css_vars/2` | Define CSS custom properties with a namespace |
| `css_consts/2` | Define compile-time constants (not CSS variables) |
| `css_keyframes/2` | Define keyframes animation |
| `css_theme/3` | Create theme overrides for a variable group |
| `css_position_try/2` | Define `@position-try` rules for CSS Anchor Positioning |
| `css_view_transition/2` | Define view transition styles |

### Style Macros (via `use LiveStyle.Sheet`)

| Macro | Description |
|-------|-------------|
| `css_class/2` | Define a named style with CSS declarations |
| `first_that_works/1` | Declare fallback values for a property |

### Reference Macros (available in both)

| Macro | Description |
|-------|-------------|
| `css_var/1` | Reference a CSS variable: `css_var({Module, :namespace, :name})` |
| `css_const/1` | Reference a constant: `css_const({Module, :namespace, :name})` |
| `css_keyframes/1` | Reference keyframes: `css_keyframes({Module, :name})` |
| `css_position_try/1` | Reference position-try: `css_position_try({Module, :name})` |
| `css_view_transition/1` | Reference view transition: `css_view_transition({Module, :name})` |
| `css_theme/1` | Reference theme: `css_theme({Module, :namespace, :theme_name})` |

### Generated Functions (in modules using `LiveStyle.Sheet`)

| Function | Description |
|----------|-------------|
| `css_class/1` | Returns a class string for use with `class={...}` |
| `css/1` | Returns `%LiveStyle.Attrs{}` for spreading with `{...}` |

### Module Functions

| Function | Description |
|----------|-------------|
| `LiveStyle.default_marker/0` | Returns the default marker class for contextual selectors |
| `LiveStyle.define_marker/1` | Creates a unique marker class for custom contexts |
| `LiveStyle.get_css/2` | Get `%LiveStyle.Attrs{}` from another module: `LiveStyle.get_css(Module, :class)` |
| `LiveStyle.get_css_class/2` | Get class string from another module: `LiveStyle.get_css_class(Module, :class)` |

### Compiler Functions (via `LiveStyle.Compiler`)

| Function | Description |
|----------|-------------|
| `LiveStyle.Compiler.run/2` | Run CSS generation for a profile |
| `LiveStyle.Compiler.install_and_run/2` | Same as `run/2` (for Tailwind API compatibility) |
| `LiveStyle.Compiler.write_css/1` | Write CSS to file if changed |

### Config Functions (via `LiveStyle.Config`)

| Function | Description |
|----------|-------------|
| `LiveStyle.Config.output_path/0` | Get configured CSS output path |
| `LiveStyle.Config.shorthand_behavior/0` | Get configured shorthand behavior |
| `LiveStyle.Config.config_for!/1` | Get configuration for a profile |

### Storage Functions (via `LiveStyle.Storage`)

| Function | Description |
|----------|-------------|
| `LiveStyle.Storage.path/0` | Get current manifest path |
| `LiveStyle.Storage.read/0` | Read manifest from file |
| `LiveStyle.Storage.write/1` | Write manifest to file |
| `LiveStyle.Storage.update/1` | Update manifest atomically |

### Contextual Selectors (via `alias LiveStyle.When`)

| Function | Description |
|----------|-------------|
| `When.ancestor/1,2` | Style when ancestor has pseudo-state |
| `When.descendant/1,2` | Style when descendant has pseudo-state |
| `When.sibling_before/1,2` | Style when preceding sibling has pseudo-state |
| `When.sibling_after/1,2` | Style when following sibling has pseudo-state |
| `When.any_sibling/1,2` | Style when any sibling has pseudo-state |

### Type Helpers (via `import LiveStyle.Types`)

| Function | Description |
|----------|-------------|
| `color/1` | CSS `<color>` type |
| `length/1` | CSS `<length>` type |
| `angle/1` | CSS `<angle>` type |
| `integer/1` | CSS `<integer>` type |
| `number/1` | CSS `<number>` type |
| `time/1` | CSS `<time>` type |
| `percentage/1` | CSS `<percentage>` type |

## Shorthand Behaviors

LiveStyle supports three behaviors for handling CSS shorthand properties:

```elixir
config :live_style,
  shorthand_behavior: :accept_shorthands  # default
```

### Available Behaviors

| Behavior | Description |
|----------|-------------|
| `:accept_shorthands` | Pass through with nil resets for cascade control (default) |
| `:flatten_shorthands` | Expand to longhand properties |
| `:forbid_shorthands` | Error on disallowed shorthands |

### `:accept_shorthands` (Default)

Keeps shorthands intact and allows all shorthand properties. Uses nil resets internally for cascade control. Last style wins, matching developer expectations from traditional CSS.

### `:flatten_shorthands`

Expands shorthand properties to their longhand equivalents. Produces more verbose CSS but provides maximum specificity predictability.

### `:forbid_shorthands`

Certain shorthands are disallowed and raise compile-time errors. Use this mode for large codebases where you want to enforce explicit property declarations:

```elixir
# These raise compile errors in :forbid_shorthands mode:
css_class :button, border: "1px solid red"      # Use border_width, border_style, border_color
css_class :card, background: "red url(...)"     # Use background_color, background_image
```

## CSS Anchor Positioning

LiveStyle supports [CSS Anchor Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) through the `css_position_try/2` macro, which creates `@position-try` at-rules for fallback positioning.

```elixir
defmodule MyApp.Tooltip do
  use LiveStyle.Sheet

  css_class :tooltip,
    position: "absolute",
    position_anchor: "--trigger",
    top: "anchor(bottom)",
    left: "anchor(center)",
    # Fallback position if tooltip doesn't fit below
    position_try_fallbacks: css_position_try(
      bottom: "anchor(top)",
      left: "anchor(center)"
    )
end
```

This generates CSS like:

```css
@position-try --x1a2b3c4 {
  bottom: anchor(top);
  left: anchor(center);
}
.x5e6f7g8 { position-try-fallbacks: --x1a2b3c4; }
```

### Allowed Properties

Only positioning-related properties are allowed in `css_position_try`:

- **Position anchor**: `position_anchor`, `position_area`
- **Inset**: `top`, `right`, `bottom`, `left`, `inset`, `inset_block`, `inset_inline`, etc.
- **Margin**: `margin`, `margin_top`, `margin_inline_start`, etc.
- **Size**: `width`, `height`, `min_width`, `max_height`, `block_size`, `inline_size`, etc.
- **Self-alignment**: `align_self`, `justify_self`, `place_self`

### Browser Support

CSS Anchor Positioning is available in Chromium 125+ (June 2024). Firefox and Safari do not yet support this feature. Consider using feature detection or providing fallback positioning for broader browser support.

## Why LiveStyle?

### vs Tailwind CSS

- **Type-safe tokens**: Design tokens are Elixir values, not magic strings
- **No purging complexity**: Only styles you use are generated
- **Elixir-native**: Conditional logic uses `&&` and `||`, not string concatenation
- **Scoped theming**: Override tokens for subtrees without global CSS

### vs Inline Styles

- **Pseudo-classes**: `:hover`, `:focus`, etc. work naturally
- **Media queries**: Responsive design without JavaScript
- **Performance**: Atomic classes are cached and deduplicated
- **DevTools**: Inspect class names instead of inline style blobs

### Inspired by StyleX

LiveStyle brings Meta's StyleX philosophy to Phoenix LiveView:

- Atomic CSS for minimal bundle size
- Last-wins merging for predictable composition
- Deterministic class names for caching
- CSS variables for theming

## License

MIT License - see [LICENSE](LICENSE) for details.