diff --git a/docs/GUIDE.md b/docs/GUIDE.md
new file mode 100644
index 0000000..be6fc79
--- /dev/null
+++ b/docs/GUIDE.md
@@ -0,0 +1,1067 @@
+# Forge Guide
+
+A complete guide to writing components with Forge.
+
+## Table of Contents
+
+- [Getting Started](#getting-started)
+- [Basic Components](#basic-components)
+- [HTML Tags](#html-tags)
+- [CSS Properties](#css-properties)
+- [States](#states)
+- [Variants](#variants)
+- [Parts](#parts)
+- [Custom Render](#custom-render)
+- [Selectors](#selectors)
+- [Scopes](#scopes)
+- [Themes](#themes)
+- [SSR and SPA](#ssr-and-spa)
+
+---
+
+## Getting Started
+
+Import `define` and start creating components. Each call to `define` generates real CSS classes and returns a JSX component.
+
+```tsx
+import { define } from 'forge'
+
+const Box = define('Box', {
+ padding: 20,
+ background: '#111',
+})
+
+// Renders:
...
+...
+```
+
+Names must be unique — defining the same name twice throws an error.
+
+### Anonymous components
+
+Omit the name and Forge generates one from the base tag: `Div`, `Button2`, `Anchor3`, etc.
+
+```tsx
+const Box = define({ display: 'flex', gap: 16 })
+// class="Div"
+
+const Link = define({ base: 'a', color: 'blue' })
+// class="Anchor"
+```
+
+---
+
+## HTML Tags
+
+By default, components render as ``. Use `base` to change the tag:
+
+```tsx
+const Button = define('Button', {
+ base: 'button',
+ padding: 20,
+ cursor: 'pointer',
+})
+//
...
+
+const Heading = define('Heading', {
+ base: 'h1',
+ fontSize: 28,
+})
+//
...
+```
+
+### Attribute shorthand
+
+Set default attributes right in the base string:
+
+```tsx
+const Radio = define('Radio', {
+ base: 'input[type=radio]',
+})
+//
+
+const Checkbox = define('Checkbox', {
+ base: 'input[type=checkbox]',
+})
+//
+```
+
+Props passed at usage time override base attributes.
+
+---
+
+## CSS Properties
+
+Write CSS properties in camelCase. They compile to real CSS at definition time.
+
+```tsx
+const Card = define('Card', {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 16,
+ padding: 20,
+ backgroundColor: '#111',
+ borderRadius: 8,
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
+})
+```
+
+Generated CSS:
+
+```css
+.Card {
+ background-color: #111;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 20px;
+}
+```
+
+### Numeric values
+
+Numbers are auto-converted to `px`:
+
+```tsx
+{ padding: 20 } // padding: 20px
+{ margin: 0 } // margin: 0px
+{ fontSize: 14 } // font-size: 14px
+{ borderRadius: 8 } // border-radius: 8px
+```
+
+Except for unitless properties, which stay as plain numbers:
+
+```tsx
+{ opacity: 0.5 } // opacity: 0.5
+{ zIndex: 10 } // z-index: 10
+{ flex: 1 } // flex: 1
+{ flexGrow: 2 } // flex-grow: 2
+{ flexShrink: 0 } // flex-shrink: 0
+{ fontWeight: 400 } // font-weight: 400
+{ lineHeight: 1.6 } // line-height: 1.6
+{ order: 3 } // order: 3
+```
+
+Strings are passed through as-is:
+
+```tsx
+{ padding: '12px 24px' } // padding: 12px 24px
+{ border: '1px solid #222' } // border: 1px solid #222
+{ margin: '0 auto' } // margin: 0 auto
+```
+
+### All supported properties
+
+Every standard CSS property is supported in camelCase form: layout (`display`, `position`, `flexDirection`, `gridTemplateColumns`, etc.), box model (`margin`, `padding`, `width`, `height`, etc.), typography (`fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, etc.), visual (`background`, `border`, `boxShadow`, `opacity`, etc.), animation (`transition`, `animation`, `transform`, etc.), and SVG (`fill`, `stroke`, `strokeWidth`, etc.).
+
+---
+
+## States
+
+Pseudo-selectors like `:hover`, `:focus`, `:active`, and `:disabled`.
+
+```tsx
+const Button = define('Button', {
+ base: 'button',
+ padding: 20,
+ background: 'blue',
+ cursor: 'pointer',
+
+ states: {
+ ':hover': { background: 'darkblue' },
+ ':active': { transform: 'translateY(1px)' },
+ ':focus': { outline: '2px solid white' },
+ ':disabled': { opacity: 0.3, cursor: 'not-allowed' },
+ },
+})
+```
+
+The colon is optional — `hover` and `:hover` are equivalent:
+
+```tsx
+states: {
+ hover: { background: 'darkblue' }, // same as ':hover'
+}
+```
+
+### Complex pseudo-selectors
+
+Use the full string for compound selectors:
+
+```tsx
+states: {
+ ':not(:disabled):hover': { background: 'darkblue' },
+ ':not(:disabled):active': { transform: 'translateY(1px)' },
+}
+```
+
+Generated CSS:
+
+```css
+.Button:not(:disabled):hover { background: darkblue; }
+.Button:not(:disabled):active { transform: translateY(1px); }
+```
+
+---
+
+## Variants
+
+Variants are typed props that apply conditional CSS classes. They replace inline styles with a clean, declarative API.
+
+### Boolean variants
+
+A variant whose value is a style object. Activated by passing `true`.
+
+```tsx
+const Button = define('Button', {
+ base: 'button',
+ padding: 20,
+ background: 'blue',
+
+ variants: {
+ disabled: {
+ opacity: 0.3,
+ cursor: 'not-allowed',
+ },
+ rounded: {
+ borderRadius: 999,
+ },
+ },
+})
+```
+
+```tsx
+
Normal
+
Disabled
+
Pill
+
Both
+```
+
+Generated CSS:
+
+```css
+.Button { padding: 20px; background: blue; }
+.Button.disabled { cursor: not-allowed; opacity: 0.3; }
+.Button.rounded { border-radius: 999px; }
+```
+
+HTML output for `
`:
+
+```html
+Both
+```
+
+Variant props are consumed by Forge and **not** passed to the HTML element.
+
+### Keyed variants
+
+A variant whose value is an object of named options. Activated by passing a string.
+
+```tsx
+const Button = define('Button', {
+ base: 'button',
+ padding: 16,
+
+ variants: {
+ intent: {
+ primary: { background: 'blue', color: 'white' },
+ secondary: { background: '#333', color: '#ccc' },
+ danger: { background: 'red', color: 'white' },
+ ghost: { background: 'transparent', color: 'gray' },
+ },
+ size: {
+ small: { padding: '8px 16px', fontSize: 12 },
+ large: { padding: '16px 32px', fontSize: 16 },
+ },
+ },
+})
+```
+
+```tsx
+Save
+Delete Account
+Cancel
+```
+
+Generated CSS:
+
+```css
+.Button.intent-primary { background: blue; color: white; }
+.Button.intent-danger { background: red; color: white; }
+.Button.size-small { font-size: 12px; padding: 8px 16px; }
+.Button.size-large { font-size: 16px; padding: 16px 32px; }
+```
+
+### Variants with states
+
+Variants can include their own pseudo-selectors:
+
+```tsx
+variants: {
+ intent: {
+ danger: {
+ background: 'red',
+ states: {
+ ':not(:disabled):hover': { background: '#cc0000' },
+ },
+ },
+ secondary: {
+ background: '#333',
+ states: {
+ ':not(:disabled):hover': { borderColor: 'green' },
+ },
+ },
+ },
+}
+```
+
+### Combining multiple variants
+
+Multiple keyed and boolean variants can be used together freely:
+
+```tsx
+Small Primary
+Large Danger Disabled
+```
+
+---
+
+## Parts
+
+Parts are named sub-components within a component. They get their own CSS classes and can have their own base tags, states, and selectors.
+
+```tsx
+const Card = define('Card', {
+ padding: 20,
+ background: '#111',
+
+ parts: {
+ Header: {
+ base: 'h2',
+ fontSize: 24,
+ marginBottom: 12,
+ },
+ Body: {
+ fontSize: 14,
+ lineHeight: 1.6,
+ color: '#888',
+ },
+ Footer: {
+ base: 'footer',
+ marginTop: 16,
+ paddingTop: 12,
+ borderTop: '1px solid #333',
+ fontSize: 12,
+ },
+ },
+
+ render({ props, parts: { Root, Header, Body, Footer } }) {
+ return (
+
+
+ {props.children}
+
+
+ )
+ },
+})
+```
+
+```tsx
+
+ This is the card body content.
+
+```
+
+Generated CSS classes:
+
+```css
+.Card { padding: 20px; background: #111; }
+.Card_Header { font-size: 24px; margin-bottom: 12px; }
+.Card_Body { color: #888; font-size: 14px; line-height: 1.6; }
+.Card_Footer { border-top: 1px solid #333; font-size: 12px; margin-top: 16px; padding-top: 12px; }
+```
+
+### Parts with states
+
+Parts can have their own pseudo-selectors:
+
+```tsx
+parts: {
+ Tab: {
+ base: 'button',
+ color: '#888',
+ borderBottom: '1px solid transparent',
+
+ states: {
+ ':hover': { color: '#fff' },
+ },
+ },
+}
+```
+
+### Variants that affect parts
+
+Variants can override styles on specific parts:
+
+```tsx
+const UserProfile = define('UserProfile', {
+ padding: 24,
+
+ parts: {
+ Avatar: { base: 'img', width: 64, height: 64, borderRadius: '50%' },
+ Name: { fontSize: 18 },
+ Bio: { fontSize: 14, color: '#888' },
+ },
+
+ variants: {
+ size: {
+ compact: {
+ padding: 16, // override root
+ parts: {
+ Avatar: { width: 48, height: 48 }, // override part
+ Name: { fontSize: 16 },
+ },
+ },
+ large: {
+ padding: 32,
+ parts: {
+ Avatar: { width: 96, height: 96 },
+ Name: { fontSize: 24 },
+ },
+ },
+ },
+ verified: {
+ parts: {
+ Avatar: { border: '2px solid gold' },
+ },
+ },
+ },
+
+ render({ props, parts: { Root, Avatar, Name, Bio } }) {
+ return (
+
+
+ {props.name}{props.verified && ' ✓'}
+ {props.bio}
+
+ )
+ },
+})
+```
+
+```tsx
+
+
+```
+
+Generated CSS:
+
+```css
+.UserProfile { padding: 24px; }
+.UserProfile_Avatar { border-radius: 50%; height: 64px; width: 64px; }
+.UserProfile.size-compact { padding: 16px; }
+.UserProfile_Avatar.size-compact { height: 48px; width: 48px; }
+.UserProfile_Avatar.verified { border: 2px solid gold; }
+```
+
+### Applying variants to parts in render
+
+Inside `render()`, you can pass variant props directly to part components:
+
+```tsx
+const Tabs = define('Tabs', {
+ display: 'flex',
+
+ parts: {
+ Tab: {
+ base: 'button',
+ color: '#888',
+ borderBottom: '1px solid transparent',
+ },
+ },
+
+ variants: {
+ active: {
+ parts: {
+ Tab: {
+ color: 'green',
+ borderBottom: '1px solid green',
+ },
+ },
+ },
+ },
+
+ render({ props, parts: { Root, Tab } }) {
+ return (
+
+ {props.items?.map((item: any) => (
+ {item.label}
+ ))}
+
+ )
+ },
+})
+```
+
+The `active` prop on `` adds the variant class to that specific tab instance. It is not passed through to the HTML.
+
+---
+
+## Custom Render
+
+Override the default rendering with a `render` function. It receives `props` (everything passed to the component) and `parts` (component functions for Root and all named parts).
+
+```tsx
+const FormGroup = define('FormGroup', {
+ marginBottom: 24,
+
+ parts: {
+ Label: { base: 'label', display: 'block', fontSize: 14, marginBottom: 8 },
+ Helper: { fontSize: 12, color: '#888', marginTop: 6 },
+ Error: { fontSize: 12, color: 'red', marginTop: 6 },
+ },
+
+ render({ props, parts: { Root, Label, Helper, Error } }) {
+ return (
+
+ {props.label && {props.label} }
+ {props.children}
+ {props.helper && {props.helper} }
+ {props.error && {props.error} }
+
+ )
+ },
+})
+```
+
+```tsx
+
+
+
+
+
+
+
+```
+
+### Destructuring props
+
+Destructure to separate custom props from HTML passthrough props:
+
+```tsx
+render({ props: { title, subtitle, ...rest }, parts: { Root, H2, P } }) {
+ return (
+
+ {title}
+ {subtitle}
+
+ )
+}
+```
+
+### Without render
+
+If no `render` is provided, the component renders its children into the root tag:
+
+```tsx
+const Box = define('Box', { padding: 20 })
+
+// Equivalent to:
+// render({ props, parts: { Root } }) {
+// return {props.children}
+// }
+```
+
+---
+
+## Selectors
+
+The `selectors` key lets you write custom CSS selectors. Use `&` for the current element and `@PartName` to reference other parts.
+
+### Basic selectors
+
+```tsx
+const NavLink = define('NavLink', {
+ base: 'a',
+ color: '#888',
+ textDecoration: 'none',
+
+ selectors: {
+ '&:hover': { color: '#fff' },
+ '&[aria-current]': { color: '#fff', textDecoration: 'underline' },
+ },
+})
+```
+
+```tsx
+Home
+About
+```
+
+### Cross-part selectors
+
+Reference other parts with `@PartName`. This is the mechanism that enables CSS-only interactive components.
+
+```tsx
+const Checkbox = define('Checkbox', {
+ parts: {
+ Input: {
+ base: 'input[type=checkbox]',
+ display: 'none',
+ },
+ Label: {
+ base: 'label',
+ padding: 10,
+ cursor: 'pointer',
+ color: 'gray',
+
+ selectors: {
+ // When the Input is checked, style this Label
+ '@Input:checked + &': {
+ color: 'green',
+ fontWeight: 'bold',
+ },
+ // When the Input is disabled, style this Label
+ '@Input:disabled + &': {
+ opacity: 0.5,
+ cursor: 'not-allowed',
+ },
+ },
+ },
+ },
+
+ render({ props, parts: { Root, Input, Label } }) {
+ return (
+
+
+ {props.label}
+
+ )
+ },
+})
+```
+
+The `@Input` in `@Input:checked + &` is replaced with `.Checkbox_Input`, and `&` is replaced with `.Checkbox_Label`, producing:
+
+```css
+.Checkbox_Input:checked + .Checkbox_Label {
+ color: green;
+ font-weight: bold;
+}
+```
+
+### CSS-only tab switcher
+
+Hidden radio inputs + sibling selectors = tabs without JavaScript:
+
+```tsx
+const TabSwitcher = define('TabSwitcher', {
+ parts: {
+ Input: {
+ base: 'input[type=radio]',
+ display: 'none',
+ },
+ TabBar: {
+ display: 'flex',
+ borderBottom: '1px solid #333',
+ },
+ TabLabel: {
+ base: 'label',
+ padding: '12px 24px',
+ color: '#888',
+ cursor: 'pointer',
+
+ states: {
+ ':hover': { color: '#fff' },
+ },
+
+ selectors: {
+ '@Input:checked + &': {
+ color: 'green',
+ borderBottom: '1px solid green',
+ },
+ },
+ },
+ Content: {
+ display: 'none',
+ padding: 24,
+
+ selectors: {
+ // General sibling combinator: when a radio is checked,
+ // show the corresponding content panel
+ '@Input:checked ~ &': { display: 'block' },
+ },
+ },
+ },
+
+ render({ props, parts: { Root, Input, TabBar, TabLabel, Content } }) {
+ return (
+
+
+ {props.tabs?.map((tab: any, i: number) => (
+ <>
+
+ {tab.label}
+ >
+ ))}
+
+ {props.tabs?.map((tab: any) => (
+ {tab.content}
+ ))}
+
+ )
+ },
+})
+```
+
+```tsx
+First panel },
+ { id: 'two', label: 'Tab 2', content: Second panel
},
+ { id: 'three', label: 'Tab 3', content: Third panel
},
+ ]}
+/>
+```
+
+### Selector reference
+
+| Selector | Meaning |
+|---|---|
+| `&:hover` | This element on hover |
+| `&[aria-current]` | This element when attribute is present |
+| `@Input:checked + &` | This element when adjacent Input is checked |
+| `@Input:checked ~ &` | This element when any preceding Input sibling is checked |
+| `@Input:disabled + &` | This element when adjacent Input is disabled |
+| `@Input:checked + &:hover` | This element on hover, but only when Input is checked |
+
+---
+
+## Scopes
+
+When building a family of related components, `createScope` prefixes all names automatically:
+
+```tsx
+import { createScope } from 'forge'
+
+const { define } = createScope('Button')
+
+const Button = define('Root', { // CSS class: "Button" (Root is special)
+ base: 'button',
+ padding: 20,
+})
+
+const ButtonRow = define('Row', { // CSS class: "ButtonRow"
+ display: 'flex',
+ gap: 16,
+})
+
+const ButtonIcon = define('Icon', { // CSS class: "ButtonIcon"
+ width: 20,
+ height: 20,
+})
+```
+
+`Root` is the special case — `define('Root', ...)` with scope `'Button'` produces class `Button`, not `ButtonRoot`.
+
+---
+
+## Themes
+
+Built-in CSS custom properties with type safety.
+
+### Define themes
+
+Create a theme file with your tokens:
+
+```tsx
+// darkTheme.tsx
+export default {
+ 'colors-bg': '#0a0a0a',
+ 'colors-bgElevated': '#111',
+ 'colors-fg': '#00ff00',
+ 'colors-fgMuted': '#888',
+ 'colors-border': '#222',
+ 'colors-accent': '#00ff00',
+ 'colors-accentDim': '#008800',
+
+ 'fonts-mono': "'Monaco', 'Menlo', monospace",
+
+ 'spacing-sm': '12px',
+ 'spacing-md': '16px',
+ 'spacing-lg': '24px',
+ 'spacing-xl': '32px',
+
+ 'radius-sm': '4px',
+ 'radius-md': '8px',
+} as const
+```
+
+```tsx
+// lightTheme.tsx
+export default {
+ 'colors-bg': '#f5f5f0',
+ 'colors-bgElevated': '#fff',
+ 'colors-fg': '#0a0a0a',
+ 'colors-fgMuted': '#666',
+ 'colors-border': '#ddd',
+ 'colors-accent': '#0066cc',
+ 'colors-accentDim': '#004499',
+
+ 'fonts-mono': "'Monaco', 'Menlo', monospace",
+
+ 'spacing-sm': '12px',
+ 'spacing-md': '16px',
+ 'spacing-lg': '24px',
+ 'spacing-xl': '32px',
+
+ 'radius-sm': '4px',
+ 'radius-md': '8px',
+} as const
+```
+
+### Register themes
+
+```tsx
+// themes.tsx
+import { createThemes } from 'forge'
+import darkTheme from './darkTheme'
+import lightTheme from './lightTheme'
+
+export const theme = createThemes({
+ dark: darkTheme,
+ light: lightTheme,
+})
+```
+
+`createThemes` returns a typed function. It only accepts keys that exist in your theme objects.
+
+### Use theme values
+
+`theme('key')` returns `var(--theme-key)`:
+
+```tsx
+import { theme } from './themes'
+
+const Card = define('Card', {
+ padding: theme('spacing-lg'), // var(--theme-spacing-lg)
+ background: theme('colors-bgElevated'), // var(--theme-colors-bgElevated)
+ color: theme('colors-fg'), // var(--theme-colors-fg)
+ border: `1px solid ${theme('colors-border')}`,
+ borderRadius: theme('radius-md'),
+})
+```
+
+### Switch themes
+
+Themes are controlled by the `data-theme` attribute:
+
+```tsx
+// Set initial theme
+
+
+// Switch at runtime
+document.body.setAttribute('data-theme', 'light')
+```
+
+Generated CSS:
+
+```css
+[data-theme="dark"] {
+ --theme-colors-bg: #0a0a0a;
+ --theme-colors-fg: #00ff00;
+ /* ... */
+}
+[data-theme="light"] {
+ --theme-colors-bg: #f5f5f0;
+ --theme-colors-fg: #0a0a0a;
+ /* ... */
+}
+```
+
+### Other theme APIs
+
+```tsx
+// Register a single theme
+import { createTheme } from 'forge'
+createTheme('dark', { bgColor: '#000', fgColor: '#fff' })
+
+// Extend existing themes with new tokens
+import { extendThemes } from 'forge'
+extendThemes({ dark: { 'colors-success': '#0f0' } })
+
+// Untyped fallback for dynamic theme keys
+import { themeVar } from 'forge'
+themeVar('colors-bg') // 'var(--theme-colors-bg)'
+```
+
+---
+
+## SSR and SPA
+
+### Server-side rendering
+
+Include ` ` in the `` to inline all CSS:
+
+```tsx
+import { define, Styles } from 'forge'
+
+const Page = () => (
+
+
+
+
+ ...
+
+)
+```
+
+Or write CSS to a file using `stylesToCSS()`:
+
+```tsx
+import { stylesToCSS } from 'forge'
+
+// Write to a .css file and serve it
+const css = stylesToCSS()
+Bun.write('public/main.css', css)
+
+// Then link it
+
+```
+
+### Single-page apps
+
+In browser environments, Forge automatically injects a `