# Bricks 2.4.2 — Abilities API ignores “Custom CSS sync” being off and moves Custom CSS into style controls
**Version:** Bricks 2.4.2 (also checked against the identical files on a live 2.4.2 install)
**Area:** AI / Abilities API — `includes/abilities/element-style-normalizer.php`
**Severity:** High. The site owner has turned sync off, but the abilities still rewrite their Custom CSS. With nested CSS, the rewrite also corrupts it, and the save still reports success.
## Summary
Bricks has a setting called **“Bi-directional sync between Custom CSS and style controls”** (`builderCssSync`). When it is **off**, Custom CSS should stay as Custom CSS. It should not be moved into the element’s or class’s style controls.
The builder follows this setting. **The Abilities API does not.** Every `_cssCustom` written through the abilities goes through `Element_Style_Normalizer::map_base_custom_css_to_settings()`, whatever the setting is. Root declarations are taken out of the CSS and written into style controls (`row-gap` → `_rowGap`, `color` → `_typography.color`, `display` → `_display`, …). The remaining CSS is rebuilt from parsed rules. That covers page and template element saves, `update-element`, global class create/update and the design workspace.
So with sync turned off, the same CSS is:
- **saved from the builder:** stored verbatim in Custom CSS (correct)
- **saved through the abilities:** split into style controls and rebuilt CSS (wrong)
## Where the setting is ignored
- `builderCssSync` is only read in `includes/builder.php:1335` (passed to the builder) and `includes/assets.php:4459` (render-time exclusions).
- `includes/abilities/element-style-normalizer.php` never reads it. `normalize_element()` (line 102) and `normalize_global_class_settings()` (line 144) call `map_base_custom_css_to_settings()` unconditionally. The only skip is `$preserve_unchanged`, which applies only when the incoming CSS is identical to what is already saved.
- Callers: `abilities/save-pipeline.php:291`, `abilities/conversion.php:219`, `abilities/design.php:4010`, `4222`, `6858`, `8091`, `abilities/design-workspace.php:967`.
**Expected:** with sync off, `_cssCustom` written through the abilities is stored as-is, the same as a builder save (only `%root%` placeholder replacement applied).
**Actual:** root declarations are always moved into style controls, and `_cssCustom` is rebuilt from the parsed result.
## Why this matters: the forced conversion is lossy and corrupts CSS
Users often turn sync off because they write CSS the controls can’t represent, such as native nesting, `@container` inside rules, or custom properties holding shorthands. Since the abilities force the conversion anyway, that CSS goes through a parser that doesn’t support it.
### Nested CSS is corrupted
`Html_To_Bricks_Css_Parser::parse_rules_block()` passes a top-level rule’s whole block to `parse_declarations()` (`includes/html-to-bricks/css-parser.php:596`). That function:
1. splits on every `;` at function depth 0 (line 672), without tracking `{ }`
2. splits each part at its first `:` (line 691)
3. keys the result by property (line 701)
For nested rules, this creates fake declarations, drops closing braces and overwrites repeated properties.
Parser-only repro:
```php
$css = “.x{\n color: var(–a);\n\n &:hover{\n color: var(–b);\n }\n}\n”;
var_export( \Bricks\Html_To_Bricks_Css_Parser::parse_css( $css )[0][‘declarations’] );
```
```php
array (
‘color’ => ‘var(–a)’,
‘&’ => 'hover{
color: var(--b)',
)
```
End-to-end repro, with **sync off**: `bricks/update-element` (or `bricks/set-page-elements`) with this element CSS:
```css
%root%{
display: flex;
color: var(–_c);
&:hover,
&:focus-visible{
background: var(–_bg-hover);
color: var(–_c-hover);
}
@container (inline-size <= 600px){
flex-direction: column;
.x__child{ color: blue; }
}
}
```
Stored result (abridged):
```css
#brxe-abc123 {
&: hover,
&:focus-visible{
background: var(–_bg-hover);
}
```
- `display` is moved into `_display`, even though sync is off.
- `color` is moved into controls with the **hover** value. The nested `color: var(–_c-hover)` overwrote the outer `color` because declarations are keyed by property.
- `&:hover` becomes the fake declaration `&: hover…`.
- The `@container` block and its closing braces are lost or truncated, which leaves the stored CSS with unbalanced braces. The ability still returns `ok: true`.
The same CSS saved in the builder with sync off is stored exactly as written and works.
### `border: var(–x)` is stored as a border width (minor)
`Html_To_Bricks_Css_Value_Parsers::parse_border()` (`css-to-controls/parsers.php:438`) intentionally treats a lone `var()` as the width (“legacy width interpretation of `var(–border-width)`”). So `border: var(–card-border)`, where `–card-border: 1px solid …`, is written to `_border.width.{top,right,bottom,left}` and renders as an invalid width. With sync respected, this CSS would never reach the parser.
### Cascade order changes
Top-level at-rules are kept verbatim (`build_fallback_custom_css()`, line 219) but moved **above** all rebuilt rules. That changes cascade order for top-level `@container` blocks that override earlier rules of equal specificity.
## Workaround
Wrap the entire CSS in a single top-level at-rule (for example `@supports (display: grid) { … }`). This is currently the only way to get CSS through the abilities unchanged.
## Suggested fix
1. **Primary:** in `Element_Style_Normalizer`, only call `map_base_custom_css_to_settings()` when `builderCssSync` is enabled. When it is off, run only `replace_root_placeholder()` and store the CSS verbatim, matching the builder save path.
2. With sync **on**, never rebuild `_cssCustom` from a lossy parse. If the CSS contains nesting the parser can’t handle, keep the original text or fail the save rather than returning `ok: true` with corrupted CSS.
3. Make `parse_declarations()` / `parse_rules_block()` nesting-aware by tracking brace depth and treating nested blocks as child rules.