# On-Demand CSS System — Replace Tailwind with Server-Driven Style Delivery ## Context The app recently moved from Tailwind CDN to a pre-built tw.css (92KB, Tailwind v4), but v4 broke styles since the classes target v3. Currently reverted to CDN for dev. Rather than fixing the v3/v4 mismatch, we replace Tailwind entirely with an on-demand CSS system: the server knows exactly which classes each response uses (because it renders sx→HTML), so it sends only the CSS rules needed — zero unused CSS, zero build step, zero Tailwind dependency. This mirrors the existing `SX-Components` dedup protocol: client tells server what it has, server sends only what's new. ## Phase 1: Minimal Viable System ### Step 1: CSS Registry — `shared/sx/css_registry.py` (new file) Parse `tw.css` at startup into a dict mapping HTML class names → CSS rule text. - **`load_css_registry(path)`** — parse tw.css once at startup, populate module-level `_REGISTRY: dict[str, str]` and `_PREAMBLE: str` (the `@property` / `@layer` declarations that define `--tw-*` vars) - **`_css_selector_to_class(selector)`** — unescape CSS selectors (`.sm\:hidden` → `sm:hidden`, `.h-\[60vh\]` → `h-[60vh]`) - **`lookup_rules(classes: set[str]) -> str`** — return concatenated CSS for a set of class names, preserving source order from tw.css - **`get_preamble() -> str`** — return the preamble (sent once per page load) The parser uses brace-depth tracking to split minified CSS into individual rules, extracts selectors, unescapes to get HTML class names. Rules inside `@media` blocks are stored with their wrapping `@media`. ### Step 2: Class Collection During Render — `shared/sx/html.py` Add a `contextvars.ContextVar[set[str] | None]` for collecting classes. In `_render_element()` (line ~460-469), when processing a `class` attribute, split the value and add to the collector if active. ```python # ~3 lines added in _render_element after the attr loop if attr_name == "class" and attr_val: collector = _css_class_collector.get(None) if collector is not None: collector.update(str(attr_val).split()) ``` ### Step 3: Sx Source Scanner — `shared/sx/css_registry.py` For sx pages where the body is rendered *client-side* (sx source sent as text, not pre-rendered HTML), we can't use the render-time collector. Instead, scan sx source text for `:class "..."` patterns: ```python def scan_classes_from_sx(source: str) -> set[str]: """Extract class names from :class "..." in sx source text.""" ``` This runs on both component definitions and page sx source at response time. ### Step 4: Wire Into Responses — `shared/sx/helpers.py` **`sx_page()` (full page loads, line 416):** 1. Scan `component_defs` + `page_sx` for classes via `scan_classes_from_sx()` 2. Look up rules + preamble from registry 3. Replace `