# `MishkaCmsCore.Runtime.PageBuilder.ElementCss`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/element_css.ex#L5)

CSS an author writes by hand for one element.

## Why this is not the style panel, and not a reusable class

The inspector writes a fixed vocabulary — padding, gap, colour — because a panel can only offer
what someone thought to add to it. `MishkaCmsCore.Runtime.PageBuilder.Classes` covers the twenty
cards that are meant to match. Neither answers "this one element needs a `border-image` and a
`:hover` transform", which is the request every builder eventually gets and which Elementor,
Bricks, Divi and WordPress all answer with a per-element CSS box.

## What the author writes

Declarations, and `&` where they mean the element itself:

    padding: 20px;

    &:hover { transform: translateY(-2px); }
    @media (max-width: 900px) { padding: 12px; }

`&` is plain CSS nesting rather than Elementor's `selector` keyword or Bricks' `%root%`. Three
reasons, none of them aesthetic: it is real CSS, so it can be checked by reading it rather than by
substituting into it; wrapping the text in `.pb-xxxxxxxx { … }` scopes it as long as every nested
selector starts with `&`, which is one rule to enforce; and Tailwind v4 already flattens nesting
and resolves `@apply` in the same pass, so there is no flattener to write.

## Where it lives

The element carries a minted class — `MishkaCmsCore.Runtime.PageBuilder.Tree.scope/2` — and the
rule body lives in a stylesheet row keyed by it. The class is in the markup, so it survives a save
and a re-parse and travels with the node on copy; the body is in a row, so it is cached CSS that
the compiler already knows how to build rather than a `<style>` block shipped to every visitor on
every request.

One row per document, rewritten whole from the scopes the document currently contains — which is
also what prunes the rule for an element somebody deleted, without anything having to notice the
deletion. That pruning is `live_rules/2`, and the builder runs it on save.

The row IS the live site: writing one recompiles the site's stylesheet. So a rule written while the
builder's stage says Draft is not in it at all — it waits in the editing session beside the markup
that carries its class, as `place/3` rather than `put/5`, and the publish that writes that markup
folds it back in with `merge/2`.

Every write starts from the row as it is now — `put/5` reads it, and `merge/2` is given it — because
the row is SHARED. An error page's five statuses are five documents in one row, a second editor on
the same document is a second browser writing it, and a caller that wrote a map it read when it
opened would delete everything written since. That is one rule with no exceptions: nothing writes a
snapshot.

### The two places this could have lived instead

`meta` on the node loses the CSS on the first save: `meta` is derived at parse, so anything put
there that is not in the markup is gone the next time the document is read back.

A `data-` attribute holding the rule text round-trips perfectly and was still wrong: it ships to
every visitor on every request, uncached, and it forfeits `@apply` — the rule never reaches
Tailwind, so it cannot use the design system the rest of the page is built out of.

## Security

The row is written as the builder's own (`page_code: :builder` in the changeset context), so it
is written by whoever may open a builder rather than by the stylesheet area alone — see
`MishkaCmsCore.Runtime.Stylesheet`. `valid_body/1` refuses anything that closes the wrapper early
and escapes onto the rest of the site.

## Deliberately not layered

`Classes` wraps its rules in `@layer components` so a per-element utility still beats a shared
class. This is one tier further in: the author wrote it for this element specifically, so it is
emitted unlayered and therefore last, and it beats the inspector's utilities. That ordering is
what Bricks, Webflow and WordPress 7.0 all settled on.

# `declaration`

```elixir
@type declaration() :: %{
  bucket: :base | :hover | :focus | :active,
  property: String.t(),
  value: String.t(),
  text: String.t(),
  from: non_neg_integer(),
  to: non_neg_integer()
}
```

One hand-written declaration, and the bucket of the style panel it competes with.

`from`/`to` are byte offsets into the body it was read from, so `without/2` can take exactly the
text the author typed back out again — comments, spacing and all the rest left where they were.

# `advice`

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

What is worth telling an author about CSS that is valid and probably not what they meant.

`nil` when there is nothing to say. This never refuses anything: a nested selector is real CSS and
a descendant rule is a thing people write on purpose.

The one it catches is the mistake this box invites. The pane is titled with the element — `main —
this element's own` — so an author writes what looks like the rule for it:

    main { color: red }

Wrapped, that is `.pb-xxxxxxxx { main { color: red } }`: a `main` INSIDE the element, which for a
`main` is nothing at all. It saves, it is announced as applied, and the page does not change. The
author has no way to tell that from a feature that is broken.

# `all`

```elixir
@spec all(map() | nil, String.t() | nil, String.t()) :: %{
  required(String.t()) =&gt; String.t()
}
```

Every rule this document holds, as `%{scope => body}`.

Read back out of the compiled row rather than kept anywhere else: the stylesheet IS the record, so
a rule edited by hand on the Stylesheets page is still the rule here.

# `css`

```elixir
@spec css(%{required(String.t()) =&gt; String.t()}) :: String.t()
```

The CSS for a set of rules. Public because it is the whole contract with the compiler.

# `declarations`

```elixir
@spec declarations(String.t() | nil) :: [declaration()]
```

Every declaration a body applies to the element itself, in the order they were written.

Read with a scanner rather than a regular expression for the reason `parse/1` is: the body nests,
and a pattern that stops at the first `}` would read `&:hover { color: red }` as a top-level
declaration of `color`. Nested selectors that are not one of `@pseudo_buckets` — `& > .badge`, a
media query — are skipped entirely: they style something else, or at a width the panel cannot name.

# `live_rules`

```elixir
@spec live_rules(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), %{
  required(String.t()) =&gt; String.t()
}) ::
  %{required(String.t()) =&gt; String.t()}
```

The rules a tree still refers to. Anything else in the row belongs to an element that is gone.

# `merge`

```elixir
@spec merge(
  %{required(String.t()) =&gt; String.t()},
  %{required(String.t()) =&gt; String.t()} | nil
) :: %{
  required(String.t()) =&gt; String.t()
}
```

Edits laid over the rules as they stand, a blank body clearing the rule it names.

Per element rather than as one map, and that is the whole point: the publish that folds a draft's
held CSS into the row is told which elements the author touched, so everything else in the row —
the four statuses they were not looking at, the rule a second editor wrote a minute ago — is left
exactly as it is.

# `parse`

```elixir
@spec parse(String.t() | nil) :: %{required(String.t()) =&gt; String.t()}
```

Reads a row back into `%{scope => body}`.

Brace-counted rather than matched with a regular expression, because a body nests — `&:hover { … }`
and `@media (…) { … }` are the point of the feature — and a pattern that stops at the first `}`
would truncate every rule that used them. Counted by the same scanner the declarations are read
with, so a brace inside a string or a comment is text: `content: "}"` ends nothing.

# `place`

```elixir
@spec place(%{required(String.t()) =&gt; String.t()}, String.t() | nil, String.t() | nil) ::
  {:ok, %{required(String.t()) =&gt; String.t()}} | {:error, term()}
```

One element's CSS placed in a set of rules, checked but not written anywhere.

The builder's draft stage holds its rules in the editing session instead of writing them — the row
is the live site — and it has to refuse exactly what `put/5` refuses, or the box takes CSS in one
stage that the publish then throws out.

# `put`

```elixir
@spec put(map() | nil, String.t() | nil, String.t(), String.t(), String.t()) ::
  {:ok, %{required(String.t()) =&gt; String.t()}} | {:error, term()}
```

Writes one element's CSS, or clears it when the body is blank.

Returns the whole map so a caller can hold the current state without reading again.

# `reaching_selectors`

```elixir
@spec reaching_selectors(String.t() | nil) :: [String.t()]
```

The nested selectors in a body that reach past the element itself — `& > .badge`, `& .title`.

On a plain tag that is ordinary CSS about the author's own markup. On a COMPONENT it is a rule
aimed at markup the kit owns and the author did not write, which the next version of the kit is
free to rename; the panel says so rather than refusing it, because reaching in is sometimes the
only way to style something and an author who is told can decide for themselves.

A pseudo bucket is not one of these: `&:hover` is still this element.

# `row_name`

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

The reserved row a document's element CSS lives in.

# `sync`

```elixir
@spec sync(map() | nil, String.t() | nil, String.t(), %{
  required(String.t()) =&gt; String.t()
}) ::
  {:ok, %{required(String.t()) =&gt; String.t()}} | {:error, term()}
```

Rewrites the row to hold exactly `rules`, which is what prunes a deleted element's CSS.

# `without`

```elixir
@spec without(String.t() | nil, non_neg_integer()) :: String.t()
```

The body with one declaration taken out — what **Move to controls** leaves behind.

`index` is a position in `declarations/1`. Out of range, the body is returned untouched, because a
stale click from a panel rendered against an older body must not delete a different rule.

---

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