# `MishkaCmsCore.Runtime.Compilers.TailwindCompiler`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/compilers/tailwind_compiler.ex#L5)

Production-ready Tailwind CSS v4 compiler for runtime CSS generation with multi-site support.

The `TailwindCompiler` provides comprehensive CSS compilation capabilities for the MishkaCms
ecosystem, integrating with `AssetsState` for efficient site-specific CSS management and
caching. It supports dynamic template scanning, custom variants, Phoenix LiveView integration,
and production-optimized asset compression.

## Features

* **Multi-Site CSS Compilation**: Generate and manage CSS for multiple sites independently
* **Runtime Template Scanning**: Dynamically scans templates and JavaScript assets for Tailwind classes
* **Phoenix LiveView Integration**: Built-in support for LiveView loading states and lifecycle
* **Asset Compression**: Automatic Brotli and Gzip compression for optimized delivery
* **AssetsState Integration**: Seamless integration with MishkaCms asset management system
* **Production Optimization**: Minified output with configurable source paths
* **Dark Mode Support**: Custom dark mode variant using data attributes
* **Custom Variants**: Pre-configured Phoenix-specific loading state variants

## Static base (compile time) vs runtime DB content

Tailwind classes reach this CMS from two timelines:

  * **Compile time** — the host app's `lib/**`, the extensions
    (`__extensions/**`, later `deps/mishka_*`), and `assets/css/app.css`.
    These don't change after a release is built, so Tailwind's own
    scanner reads them ONCE via `mix mishka.compile_static_css`, which
    writes `priv/static/temp/static_base.css`. That file ships in the
    release and is prepended verbatim to every site's CSS. No regex — the
    scanner reads every `class={[...]}` list, `&&` conditional, macro and
    sigil shape natively.

  * **Runtime** — DB-stored `Component` / `Page` / `Layout` / `Stylesheet`
    rows. These change when an admin saves, so they're scanned by Tailwind
    at boot and on each per-resource Oban job, then concatenated on top of
    the static base.

In **development** the `static_base.css` file is absent, so the compiler
scans the live source tree directly (see `host_app_web_sources/0`).
Tailwind has the real files, so nothing is missed. The precompiled file
exists only for **releases**, where the source tree isn't on disk.

## Theme tokens (`@theme`) — the one trade-off to know

A custom utility such as `bg-brand-500` only generates CSS if its token
(`--color-brand-500`) is defined in an `@theme {}` block present in the
SAME Tailwind compile. Tokens can live in two places:

  * `assets/css/app.css` (or a kit theme file) — present at **compile** time.
  * A `Stylesheet` DB row of `kind: :theme` — present only at **runtime**.

This produces one rule you must respect:

| Where the class is used | Where its token lives | Result |
|-------------------------|-----------------------|--------|
| DB content (component/page) | DB `:theme` row       | ✅ both meet at runtime |
| DB content                  | `app.css`             | ✅ token present at runtime too |
| **Static code** (`lib/`, ext) | `app.css`           | ✅ token present at compile time |
| **Static code**             | **DB `:theme` only**  | ❌ dropped — see below |

The failure case: a custom color used in **static code** whose token lives
**only in the DB**. At compile time the token doesn't exist, so the static
base never gets the rule; at runtime the class isn't in DB content, so it's
never generated. It silently produces no CSS.

### Rule: custom color in a static file ⇒ define its token in a static file

To use `bg-brand-500` in a LiveView/component under `lib/` or an extension,
declare the token in `assets/css/app.css`:

    /* assets/css/app.css */
    @theme {
      --color-brand-500: oklch(0.62 0.21 12);
    }

Now the token is present at compile time, `static_base.css` resolves
`bg-brand-500`, and it ships in the release. A per-site DB theme can still
override the *visual value* at runtime through the `:custom`/`:theme`
stylesheet layers — it just can't introduce a brand-new token that static
code depends on.

## Basic Usage

    # Compile CSS for a specific site
    case TailwindCompiler.compile("my_site") do
      {:ok, css} ->
        IO.puts("CSS compiled successfully")
        IO.puts("Size: #{byte_size(css)} bytes")

      {:error, reason} ->
        IO.puts("Compilation failed: #{reason}")
    end

## Integrated Workflow with AssetsState

    # Compile and automatically store in AssetsState with compression
    case TailwindCompiler.compile_and_store("production_site") do
      {:ok, css} ->
        # CSS is now available in multiple formats:
        # - Raw CSS
        # - Brotli compressed
        # - Gzip compressed
        IO.puts("CSS compiled and cached successfully")

      {:error, reason} ->
        Logger.error("Failed to compile CSS: #{reason}")
    end

    # Retrieve compiled CSS from cache
    css = TailwindCompiler.get_compiled_css("production_site")

    # Get compressed version for optimal delivery
    brotli_css = TailwindCompiler.get_compiled_css("production_site", "brotli_css")
    gzip_css = TailwindCompiler.get_compiled_css("production_site", "gzip_css")

## Asset Management Operations

    # Check compilation status
    status = TailwindCompiler.get_css_status("my_site")
    # Returns: %{
    #   site: "my_site",
    #   compiled: true,
    #   updated_at: ~U[2024-01-01 12:00:00Z],
    #   css_size: 125_000
    # }

    # Refresh CSS (recompile and update cache)
    {:ok, fresh_css} = TailwindCompiler.refresh_css("my_site")

    # Check if CSS exists
    if TailwindCompiler.css_exists?("my_site") do
      IO.puts("CSS is available")
    end

    # Clean up CSS for a site
    :ok = TailwindCompiler.delete_compiled_css("my_site")

## CSS Source Configuration

The compiler automatically configures Tailwind CSS v4 with comprehensive source scanning:

    # static_base.css (compile-time) prepended verbatim — host app + extensions
    @source "runtime/my_site/js_hook/**/*.template"     # Reachable by no walk, so globbed
    @source "runtime/my_site/stylesheet/**/*.template"
    @source "runtime/__global__/js_hook/**/*.template"
    @source "runtime/__global__/stylesheet/**/*.template"
    @source "runtime/my_site/page/home.template"        # One line per REACHABLE template:
    @source "runtime/my_site/layout/main.template"      # the site's pages and error pages, the
    @source "runtime/__global__/component/alert.template" # layouts they use, the components
    @source "runtime/__global__/component/icon.template"  # they name — see `closure_roots/2`
    @source "js_assets/js/**/*.js"                   # Runtime JS hooks (NOT vendor/ — see below)
    @source "js_assets/js/**/*.ts"                   # TypeScript files

## Custom Variants and Phoenix Integration

    /* Phoenix LiveView loading states */
    @custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
    @custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
    @custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);

    /* Data attribute-based dark mode */
    @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

## Usage in Phoenix Controllers/LiveViews

    defmodule MyAppWeb.PageController do
      use MyAppWeb, :controller

      def show(conn, %{"site" => site_name}) do
        # Ensure CSS is compiled for the site
        case TailwindCompiler.compile_and_store(site_name) do
          {:ok, _css} ->
            render(conn, :show, site: site_name)

          {:error, reason} ->
            conn
            |> put_flash(:error, "Failed to load site styles")
            |> redirect(to: "/")
        end
      end
    end

    defmodule MyAppWeb.AdminLive do
      use MyAppWeb, :live_view

      def handle_event("refresh_css", %{"site" => site}, socket) do
        case TailwindCompiler.refresh_css(site) do
          {:ok, _css} ->
            {:noreply, put_flash(socket, :info, "CSS refreshed successfully")}

          {:error, reason} ->
            {:noreply, put_flash(socket, :error, "Failed to refresh CSS: #{reason}")}
        end
      end
    end

## Performance Optimization

    # Batch operations for multiple sites
    sites = ["site1", "site2", "site3"]

    compiled_sites =
      Task.async_stream(sites, &TailwindCompiler.compile_and_store/1,
        max_concurrency: 3,
        timeout: 30_000
      )
      |> Enum.map(fn {:ok, result} -> result end)

## Error Handling and Debugging

    case TailwindCompiler.compile("problematic_site") do
      {:ok, css} ->
        Logger.info("Successfully compiled #{byte_size(css)} bytes of CSS")

      {:error, reason} ->
        Logger.error("Tailwind compilation failed: #{reason}")

        # Check system requirements
        if !System.find_executable("node") do
          Logger.error("Node.js not found - required for Tailwind compilation")
        end

        # Verify template paths exist
        temp_dir = System.tmp_dir()
        Logger.info("Using temporary directory: #{temp_dir}")
    end

## Production Deployment Considerations

* **Asset Precompilation**: Consider pre-compiling CSS during deployment for faster startup
* **Memory Management**: CSS assets are cached in memory; monitor memory usage with many sites
* **Concurrent Compilation**: Use Task.async_stream for bulk compilation operations
* **File System Access**: Ensure proper permissions for temporary directory operations
* **Node.js Dependency**: Requires Node.js runtime for Tailwind CLI execution

## Asset Compression Details

All compiled CSS is automatically compressed using multiple algorithms:
* **Brotli**: Best compression ratio, supported by modern browsers
* **Gzip**: Universal compatibility, good compression ratio
* **Raw CSS**: Uncompressed for development and fallback scenarios

The compiler calculates MD5 hashes for cache invalidation and stores metadata
including compilation timestamps and asset sizes.

# `author_css`

```elixir
@spec author_css(String.t()) :: {String.t(), [String.t()]}
```

The CSS an author wrote, without the at-rules that make the Tailwind binary read a file or run
code on the server, and the names of the ones left out: `{css, ["@import", …]}`.

A stylesheet row is pasted into the file Tailwind reads. There `@import "../x.css"` copies any
local CSS file into the page's public CSS, `@source "../x"` scans any local file for class names,
and `@plugin`, `@config` and `@reference` load a local file — a plugin is JavaScript Tailwind runs.
Left out:

  * `@import`, unless it names an `http://` or `https://` address (a font stylesheet);
  * `@source`, unless it is `@source inline(…)`, which only lists class names;
  * `@plugin`, `@config` and `@reference`;
  * any at-rule whose name is written with a backslash escape.

Everything else — `@theme`, `@apply`, `@media`, `@layer`, `@custom-variant`, `@utility` — is kept
as it was written.

# `closure_roots`

```elixir
@spec closure_roots(String.t(), keyword()) :: [String.t()]
```

The templates a visitor can reach on `site_name` — the roots of its CSS closure.

A site's pages and error pages are roots because a visitor can ask for either by URL. Its LAYOUTS
are not: a layout is chosen by a database relation rather than named in markup, so a site with
thirty layout templates and five pages was scanning twenty-five it can never render, and every
global layout sat in every site's stylesheet. The layouts this site's pages actually name are read
from `pages.layout_id` and added here — which is also what makes editing a global layout a rebuild
of the sites that use it rather than a fact about all of them.

Read here rather than inside `MishkaCmsCore.Runtime.Compilers.CssClosure`, which walks a directory
and deliberately knows nothing about the database. If the read fails the layouts fall back to the
whole directory, on the same principle as `:unbounded` — correct-and-large, never small-and-broken.

# `compile`

Compiles Tailwind CSS from dynamic templates and assets for a specific site.

This optimized version:
1. Uses cached shared base CSS (global templates, js_assets) - avoids rescanning N times
2. Only compiles site-specific templates
3. Combines shared base + site-specific CSS

## Parameters

* `site_name` - String identifier for the site (used for template scanning and caching)

## Return Values

* `{:ok, css}` - Successfully compiled CSS as a minified string
* `{:error, reason}` - Compilation failed

## Performance Optimization

With 20 sites, instead of scanning global templates 20 times:
- Shared base compiled ONCE (~225ms)
- Each site only scans its own templates (~50-100ms)
- Total: ~225ms + 20×75ms = ~1.7s instead of 20×225ms = ~4.5s

There used to be a second way to answer this — `compile_full/1`, a `use_shared_base: false` mode
that assembled the whole stylesheet in one Tailwind run. Nothing called it outside its own tests,
and it restated every decision made here, so the two drifted apart with each change to what gets
scanned. A stale shared base is dropped with `delete_shared_base_css/0` instead, which is what
every writer already does.

# `compile_and_store`

Compiles Tailwind CSS and stores all compressed variants in AssetsState for optimized delivery.

This function combines compilation with intelligent caching by automatically generating
and storing multiple compressed formats (raw, Brotli, Gzip) along with metadata for
efficient asset serving. It provides the complete workflow for production CSS management.

## Parameters

* `site_name` - String identifier for the site

## Return Values

* `{:ok, css}` - Successfully compiled and stored CSS (returns the raw CSS string)
* `{:error, reason}` - Compilation or storage failed with error message

## Storage Process

1. **CSS Compilation**: Generates minified CSS using `compile/1`
2. **Multi-format Compression**: Creates Brotli and Gzip compressed variants
3. **Metadata Generation**: Calculates MD5 hash and timestamp for cache invalidation
4. **Batch Storage**: Atomically stores all formats in AssetsState
5. **Logging**: Records successful operations for monitoring

## Stored Asset Formats

* `"css"` - Raw minified CSS for development and fallback
* `"brotli_css"` - Brotli compressed CSS (best compression, ~20-30% smaller)
* `"gzip_css"` - Gzip compressed CSS (universal compatibility, ~15-25% smaller)

## Examples

    # Basic compilation and storage
    case TailwindCompiler.compile_and_store("ecommerce_site") do
      {:ok, css} ->
        IO.puts("CSS compiled and cached")
        IO.puts("Available formats: css, brotli_css, gzip_css")

      {:error, reason} ->
        Logger.error("Failed to compile ecommerce CSS: #{reason}")
    end

    # Use in a Phoenix plug for automatic CSS generation
    defmodule EnsureCssPlug do
      def init(opts), do: opts

      def call(conn, _opts) do
        site = get_site_from_conn(conn)

        if !TailwindCompiler.css_exists?(site) do
          case TailwindCompiler.compile_and_store(site) do
            {:ok, _css} -> conn
            {:error, reason} ->
              conn
              |> put_status(500)
              |> Phoenix.Controller.text("CSS compilation failed: #{reason}")
              |> halt()
          end
        else
          conn
        end
      end
    end

    # Bulk compilation for multiple sites
    sites = ["blog", "shop", "portfolio"]

    results =
      Task.async_stream(
        sites,
        &TailwindCompiler.compile_and_store/1,
        max_concurrency: 2,
        timeout: 60_000
      )
      |> Enum.map(fn {:ok, result} -> result end)

## Asset Metadata

Each stored asset includes comprehensive metadata:
* `hash` - MD5 hash for cache busting and integrity verification
* `updated_at` - UTC timestamp for cache management
* `value` - The actual CSS content (raw or compressed)
* `type` - Format identifier ("css", "brotli_css", "gzip_css")

## Integration with HTTP Serving

    defmodule CssController do
      def serve_css(conn, %{"site" => site}) do
        content_encoding = get_best_encoding(conn)
        css_type = encoding_to_type(content_encoding)

        case AssetsState.get(site, css_type) do
          {:ok, asset} ->
            conn
            |> put_resp_content_type("text/css")
            |> put_resp_header("content-encoding", content_encoding)
            |> put_resp_header("etag", asset.hash)
            |> put_resp_header("cache-control", "public, max-age=31536000")
            |> send_resp(200, asset.value)

          {:error, :not_found} ->
            # Trigger compilation on demand
            case TailwindCompiler.compile_and_store(site) do
              {:ok, _css} -> serve_css(conn, %{"site" => site})
              {:error, _} -> send_resp(conn, 500, "CSS unavailable")
            end
        end
      end
    end

## Performance Benefits

* **Reduced Bandwidth**: Compression reduces transfer sizes by 15-30%
* **Faster Loading**: Pre-compressed assets eliminate server-side compression overhead
* **Cache Efficiency**: Hash-based versioning enables aggressive browser caching
* **CDN Optimization**: Multiple formats support optimal CDN distribution

# `compile_and_store_shared_base`

Compiles shared base CSS and stores it in AssetsState for reuse.

# `compile_preview`

Compile a ONE-OFF CSS bundle for a draft/preview: the shared base + the site's classes + the
draft's OWN template (so brand-new classes in an unpublished draft are styled). Writes the draft
template into an isolated temp namespace, scans it alongside the site, and removes it after — it
NEVER touches the live site bundle. Returns `{:ok, css}` | `{:error, reason}`.

# `compile_shared_base`

Compiles ONLY the CSS every site shares: the host shell, `app.css`, `js_assets`, and the global
templates no reachability walk can reach (JS hooks and stylesheet rows).

This should be called ONCE during boot. The result is cached in AssetsState
with id "__shared_base__" and reused by all site compilations.

Global LAYOUTS, ERROR PAGES and COMPONENTS are deliberately absent — a site's stylesheet carries
what that site renders, so those are reached per site in `compile/1`.

## Performance Benefit
- static_base.css prepended once instead of re-scanned N times
- the shared global templates scanned once instead of N times
- js_assets/js/** scanned once instead of N times

## Why `js_assets/js/**` and not `js_assets/**`

`js_assets/vendor/` holds `tailwind-browser.js` — the page builder's in-browser Tailwind engine,
281 KB whose whole job is to know every utility name there is. Scanning it as a SOURCE meant
Tailwind read the editor's own name tables as if a template had asked for them, and generated real
rules into the base every site's stylesheet is built on: a measured 42 KB, of which the great
majority matched no template anywhere. The site's actual JS hooks live under `js_assets/js/`, so
the narrower glob keeps them and drops the engine.

## Return Values
- `{:ok, css}` - Successfully compiled shared CSS
- `{:error, reason}` - Compilation failed

# `compile_static_base`

Compiles the **static base** CSS at compile/release time and writes it to
`static_base_path/0`.

Scans the RUNTIME SHELL — the layouts module, its templates and `RuntimeLive` — plus the JS assets
and `app.css` (for `@theme` tokens), with Tailwind's own scanner. The result ships in the release
and is prepended to every site's CSS at runtime, because a release has no `.ex` files to scan.

Run from `mix mishka.compile_static_css` (wired into the release build).
See the moduledoc "Static base" and "Theme tokens" sections.

# `content_classes`

```elixir
@spec content_classes(String.t()) :: [String.t()]
```

The classes the stored content of the site `site_name` writes — a `class="bg-[#282828]"` in its
HTML, a marker's `class` and `*_class` attributes, and the attributes its component takes a class
string in (`MishkaCmsCore.Runtime.ContentComponents.site_classes/2`). No template holds them, so
no scan finds them: the site's stylesheet is built with each as an `@source inline(…)` candidate.

Answered in `classes` by the same `runtime:compile:content_components` event as
`content_components/1`; a token that could not be a class is left out, and at most 20000 are
kept. `[]` for a site `SiteState` does not know, and while the event cannot run.

# `content_components`

```elixir
@spec content_components(String.t()) :: [String.t()]
```

The component names the stored content of the site `site_name` uses — what a template drawing
that content reaches (`MishkaCmsCore.Runtime.Compilers.CssClosure`).

Asked of whoever stores content through `runtime:compile:content_components`, with
`%{site: name, site_id: id, components: [], icons: [], classes: []}`: a plugin adds the names its
content uses to `components`, the icon classes it names to `icons` (`content_icons/1`), and the
classes it writes to `classes` (`content_classes/1`). Read while a stylesheet is built, never in a
request. `[]` for a site `SiteState` does not know, and while the event cannot run.

    def call(%{site_id: site_id, components: names, icons: icons, classes: classes} = data) do
      {:reply,
       %{
         data
         | components: names ++ names_my_posts_use(site_id),
           icons: icons ++ icons_my_posts_use(site_id),
           classes: classes ++ classes_my_posts_use(site_id)
       }}
    end

# `content_icons`

```elixir
@spec content_icons(String.t()) :: [String.t()]
```

The icon classes the stored content of the site `site_name` names — a marker's
`icon="hero-inbox"`, a `<span class="hero-inbox">` in its HTML
(`MishkaCmsCore.Runtime.ContentComponents.icons/1`). No template holds them, so no scan finds
them: the site's stylesheet carries a rule for each a registered set ships, beside the icons its
templates name.

Answered in `icons` by the same `runtime:compile:content_components` event as
`content_components/1`. `[]` for a site `SiteState` does not know, and while the event cannot run.

# `css_exists?`

Checks if compiled CSS exists for a specific site in the cache.

This function provides a quick boolean check for CSS availability without
retrieving the actual content, making it ideal for conditional compilation
logic and cache validation.

## Parameters

* `site_name` - String identifier for the site

## Return Value

* `true` - CSS exists and is available in the cache
* `false` - CSS has not been compiled or has been removed

## Examples

    # Check before attempting to serve CSS
    if TailwindCompiler.css_exists?("blog_site") do
      css = TailwindCompiler.get_compiled_css("blog_site")
      send_css_response(conn, css)
    else
      # Trigger compilation
      {:ok, _css} = TailwindCompiler.compile_and_store("blog_site")
      redirect_to_css(conn)
    end

    # Batch check multiple sites
    sites = ["site1", "site2", "site3"]
    compiled_sites = Enum.filter(sites, &TailwindCompiler.css_exists?/1)
    missing_sites = sites -- compiled_sites

    # Use in a deployment health check
    defmodule HealthCheck do
      def css_status do
        required_sites = Application.get_env(:app, :required_sites)

        Enum.map(required_sites, fn site ->
          %{
            site: site,
            css_ready: TailwindCompiler.css_exists?(site)
          }
        end)
      end
    end

# `delete_compiled_css`

Deletes all compiled CSS variants for a specific site from the cache.

This function removes all cached CSS formats (raw, Brotli, Gzip) for a site,
effectively clearing the cache and forcing recompilation on the next request.
Useful for cache invalidation when templates or styles are updated.

## Parameters

* `site_name` - String identifier for the site

## Return Value

* `:ok` - CSS successfully deleted or didn't exist

## Examples

    # Delete CSS after template updates
    :ok = TailwindCompiler.delete_compiled_css("updated_site")

    # Next request will trigger recompilation
    {:ok, fresh_css} = TailwindCompiler.compile_and_store("updated_site")

    # Use in a cache management task
    defmodule CacheManager do
      def invalidate_site_css(site_name) do
        Logger.info("Invalidating CSS cache for site: #{site_name}")
        :ok = TailwindCompiler.delete_compiled_css(site_name)

        # Optionally precompile immediately
        Task.start(fn ->
          TailwindCompiler.compile_and_store(site_name)
        end)
      end
    end

    # Bulk cache clearing
    sites_to_clear = ["site1", "site2", "site3"]
    Enum.each(sites_to_clear, &TailwindCompiler.delete_compiled_css/1)

## Use Cases

* **Template Updates**: Clear cache when site templates are modified
* **Development**: Reset styles during development iterations
* **Deployment**: Clear cache as part of deployment pipeline
* **Memory Management**: Periodically clear unused site CSS
* **Error Recovery**: Clear potentially corrupted cached CSS

# `delete_shared_base_css`

Invalidate the cached shared-base CSS so the next compile rebuilds it
from scratch.

Why this exists: `compile/1` reuses the cached `__shared_base__` row in
`AssetsState` rather than rebuilding it per site. The shared base prepends
the precompiled `static_base.css`
(release) or live-scans the host app's `lib/**` + `__extensions/**` (dev,
via `host_app_web_sources/0`), so any class added to a LiveView will only
show up after this cache is cleared.

The dev `AssetsWatcher` calls this on every host-app file event so a
`<div class="grid-cols-3">` you just typed makes it into the next CSS
build. Production deployments don't need it — the shared base is
rebuilt at boot.

# `ensure_installed`

Ensures Tailwind binary is installed. Call once at boot time.

This avoids repeated install checks during `install_and_run/2` calls.
After calling this, use `run/2` instead of `install_and_run/2`.

# `get_compiled_css`

Retrieves compiled CSS from AssetsState cache for a specific site and format.

This function provides fast access to pre-compiled CSS assets without triggering
recompilation. It supports multiple compression formats for optimal delivery
based on client capabilities and performance requirements.

## Parameters

* `site_name` - String identifier for the site
* `type` - Asset format type (defaults to `"css"`)
  * `"css"` - Raw minified CSS
  * `"brotli_css"` - Brotli compressed CSS
  * `"gzip_css"` - Gzip compressed CSS

## Return Value

* `binary()` - The CSS content as a binary string
* Returns a CSS comment with error message if the asset doesn't exist

## Examples

    # Get standard CSS
    css = TailwindCompiler.get_compiled_css("my_site")

    # Get compressed format for optimal delivery
    brotli_css = TailwindCompiler.get_compiled_css("my_site", "brotli_css")
    gzip_css = TailwindCompiler.get_compiled_css("my_site", "gzip_css")

    # Use in Phoenix controller with content negotiation
    defmodule AssetController do
      def css(conn, %{"site" => site}) do
        format = case get_req_header(conn, "accept-encoding") do
          [encoding] when is_binary(encoding) ->
            cond do
              String.contains?(encoding, "br") -> "brotli_css"
              String.contains?(encoding, "gzip") -> "gzip_css"
              true -> "css"
            end
          _ -> "css"
        end

        css_content = TailwindCompiler.get_compiled_css(site, format)

        conn
        |> put_resp_content_type("text/css")
        |> maybe_put_encoding_header(format)
        |> send_resp(200, css_content)
      end

      defp maybe_put_encoding_header(conn, "brotli_css"),
        do: put_resp_header(conn, "content-encoding", "br")
      defp maybe_put_encoding_header(conn, "gzip_css"),
        do: put_resp_header(conn, "content-encoding", "gzip")
      defp maybe_put_encoding_header(conn, _), do: conn
    end

## Error Handling

    # CSS not found scenario
    css = TailwindCompiler.get_compiled_css("non_existent_site")
    # Returns: "/* CSS not found for site: non_existent_site */"

    # Handle missing CSS gracefully
    css = case TailwindCompiler.get_compiled_css("my_site") do
      "/* CSS not found" <> _ ->
        # Trigger compilation if CSS is missing
        case TailwindCompiler.compile_and_store("my_site") do
          {:ok, compiled_css} -> compiled_css
          {:error, _} -> "/* Compilation failed */"
        end
      valid_css -> valid_css
    end

## Performance Notes

* This function performs a direct cache lookup with minimal overhead
* No disk I/O or compilation occurs - purely memory-based retrieval
* Consider using compressed formats for production traffic
* Falls back gracefully with informative error messages for missing assets

# `get_css_status`

Gets comprehensive CSS compilation status and metadata for a specific site.

This function returns detailed information about the CSS compilation state,
including availability, timestamps, and size metrics. It's useful for
monitoring, debugging, and administrative interfaces.

## Parameters

* `site_name` - String identifier for the site

## Return Value

Returns a map with the following keys:
* `site` - The site name (string)
* `compiled` - Boolean indicating if CSS exists
* `updated_at` - UTC DateTime of last compilation (or `nil` if not compiled)
* `css_size` - Size of CSS in bytes (or `0` if not compiled)

## Examples

    # Check single site status
    status = TailwindCompiler.get_css_status("my_site")
    # => %{
    #   site: "my_site",
    #   compiled: true,
    #   updated_at: ~U[2024-01-15 10:30:45Z],
    #   css_size: 125_430
    # }

    # Use in admin dashboard
    defmodule AdminDashboard do
      def site_status_list do
        Application.get_env(:app, :managed_sites)
        |> Enum.map(&TailwindCompiler.get_css_status/1)
        |> Enum.sort_by(& &1.updated_at, {:desc, DateTime})
      end
    end

    # Monitor compilation staleness
    status = TailwindCompiler.get_css_status("production_site")

    if status.compiled do
      age_hours = DateTime.diff(DateTime.utc_now(), status.updated_at, :hour)

      if age_hours > 24 do
        Logger.warning("CSS for #{status.site} is #{age_hours} hours old")
        TailwindCompiler.refresh_css(status.site)
      end
    else
      Logger.error("No CSS compiled for critical site: #{status.site}")
      TailwindCompiler.compile_and_store(status.site)
    end

    # Health check endpoint
    defmodule HealthController do
      def css_health(conn, _params) do
        critical_sites = ["main", "blog", "shop"]

        statuses = Enum.map(critical_sites, &TailwindCompiler.get_css_status/1)
        all_compiled = Enum.all?(statuses, & &1.compiled)

        total_size = Enum.sum(Enum.map(statuses, & &1.css_size))

        health_data = %{
          status: if(all_compiled, do: "healthy", else: "degraded"),
          sites: statuses,
          total_css_size: total_size,
          checked_at: DateTime.utc_now()
        }

        json(conn, health_data)
      end
    end

## Monitoring Use Cases

* **Admin Dashboards**: Display CSS compilation status across all sites
* **Health Checks**: Verify critical site CSS is available
* **Performance Monitoring**: Track CSS size and compilation frequency
* **Alerting**: Trigger alerts for missing or stale CSS
* **Analytics**: Gather metrics on CSS usage patterns

# `get_or_compile_shared_base`

Gets the cached shared base CSS, or compiles it if not available.

# `host_app_web_sources`

# `refresh_css`

Refreshes CSS for a site by recompiling and updating the cache.

This function forces a complete recompilation of CSS for a site, regardless
of whether cached CSS already exists. It combines cache invalidation with
fresh compilation, ensuring the most up-to-date styles are available.

## Parameters

* `site_name` - String identifier for the site

## Return Values

* `{:ok, css}` - Successfully recompiled and cached fresh CSS
* `{:error, reason}` - Compilation failed with error message

## Examples

    # Refresh CSS after template changes
    case TailwindCompiler.refresh_css("blog_site") do
      {:ok, css} ->
        Logger.info("Blog CSS refreshed: #{byte_size(css)} bytes")

      {:error, reason} ->
        Logger.error("Failed to refresh blog CSS: #{reason}")
    end

    # Use in a Phoenix LiveView for admin functionality
    defmodule AdminLive do
      def handle_event("refresh_css", %{"site" => site}, socket) do
        case TailwindCompiler.refresh_css(site) do
          {:ok, _css} ->
            {:noreply, put_flash(socket, :info, "CSS refreshed for #{site}")}

          {:error, reason} ->
            {:noreply, put_flash(socket, :error, "Refresh failed: #{reason}")}
        end
      end
    end

    # Scheduled CSS refresh for active sites
    defmodule CssRefreshWorker do
      use Oban.Worker, queue: :css_refresh

      @impl Oban.Worker
      def perform(%Oban.Job{args: %{"site" => site}}) do
        case TailwindCompiler.refresh_css(site) do
          {:ok, _css} -> :ok
          {:error, reason} -> {:error, reason}
        end
      end
    end

    # Bulk refresh for multiple sites
    sites = ["blog", "shop", "docs"]

    results =
      Task.async_stream(sites, &TailwindCompiler.refresh_css/1)
      |> Enum.map(fn {:ok, result} -> result end)

## When to Use

* **Template Updates**: After modifying site templates or components
* **Style Changes**: When custom CSS or Tailwind configuration changes
* **Deployment**: As part of the deployment process to ensure fresh styles
* **Development**: During active development for immediate style updates
* **Maintenance**: Periodic refresh to clear any potential cache inconsistencies

# `reported_version`

```elixir
@spec reported_version(String.t()) :: String.t() | nil
```

Reads the version a `tailwindcss --help` output names, whether or not the binary painted it.

Pass the raw output. Returns the number as text (`"4.3.2"`), or `nil` when the output names no
version.

# `schedule`

```elixir
@spec schedule(String.t()) :: :ok | {:ok, Oban.Job.t()} | {:error, term()}
```

Enqueues the stylesheet build of the site `site_id` names — its `"css"` job, on the per-site path
that coalesces a burst of writes into one build. For a write that changed what the stylesheet is
built from without a component, page or layout being written: content that names a component, an
icon or a class it did not name before.

A switched-off or unknown site has nothing to build, and is `:ok`. So is every call under the
`:env_test?` flag, as for every other runtime job.

# `temp_dir`

```elixir
@spec temp_dir() :: String.t()
```

Where the compiled runtime templates and CSS live.

Public because `MishkaCmsCore.Runtime.Compilers.CssClosure` walks the same tree.

# `verify_css`

```elixir
@spec verify_css(String.t(), String.t() | nil) :: :ok | {:error, String.t()}
```

Whether a candidate stylesheet fragment compiles for a site, in Tailwind's own words when it does
not: `:ok` | `{:error, message}`.

For CSS that is ABOUT TO BE STORED as a row. Tailwind v4 answers `@apply` of a utility it does not
know with a non-zero exit, and a stored fragment that does that fails every later compile in its
scope — so a writer proves it before writing it rather than after.

Nothing is scanned: the fragment is compiled against the site's theme rows, `tailwind_preamble/0`
and the utility definitions alone, with no `@source` line, which is what makes this affordable on
a save. The preamble is the half that is easy to forget and the half that matters — a check
missing the plugin and the variants is STRICTER than the compiles it stands in for, and refuses
utilities this CMS ships. It also means this answers for the fragment and nothing else: a row
that is already broken cannot fail somebody's valid class.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
