---
name: bricks-build-page
description: Build a Bricks page from scratch end-to-end — create the post, populate its element tree, and configure page-level layout (full-bleed / blank canvas, disable header/footer, custom CSS, body classes, custom scripts). Activate when the user asks to create a new Bricks-rendered page (landing page, full-width page, blank-canvas page, marketing page, etc.) or when the user asks how to hide the site header/footer, make a page full-bleed, or apply common page-level Bricks settings.
---

# Building a Bricks page from scratch

This skill is for creating a new Bricks page end-to-end.

If the page already exists, do not default to this workflow:

- Use `novamira/bricks-patch-elements` for targeted settings changes on existing elements.
- Use `novamira/bricks-insert-content` to add a new section/container/subtree into an existing tree.
- Use `novamira/bricks-set-content` only when replacing the full tree.

This skill covers the canonical sequence for creating a new Bricks-rendered
page and configuring its page-level layout. For the element tree schema see
`novamira/bricks-list-elements`; for the full settings schema see
`novamira/bricks-list-settings` — this skill is workflow knowledge, not a
schema dump.

## Canonical sequence

1. **Inventory existing styles.** Call in parallel:
   - `novamira/bricks-list-theme-styles`
   - `novamira/bricks-list-variables`
   - `novamira/bricks-list-global-classes`
   - `novamira/bricks-list-color-palette`

   If theme_styles is non-empty: do NOT override its defaults in element settings unless the design intentionally diverges. If variables / classes / color_palette are non-empty: prefer references over hard-coded values.

   **Scale the inventory to the site.** On a mature site (an established
   design-system framework, hundreds or thousands of classes/variables) a full
   dump of every store wastes a large amount of context: stop reading a listing
   the moment it is clearly systematic (consistent prefixes like `at-*`,
   `fr-*`, a spacing/typography scale), note the naming convention, and re-list
   with the `category` filter (`bricks-list-global-classes` /
   `bricks-list-variables` both accept it) scoped to the categories the current
   section needs. `list-global-classes` already returns compact rows by
   default — never pass `include_settings: true` on a full listing; read one
   class with `bricks-get-global-class` when you need its settings. The full
   four-store inventory is for small or fresh sites where reading everything
   is cheap.

2. **Identify missing tokens (tokens-first).** Read the brief. Derive the design system the page needs:
   - Color palette (≤ 6 entries typically)
   - Spacing scale (4-6 steps)
   - Typography scale (4-6 sizes)
   - Reusable bundles (e.g. `.btn-primary`, `.card`)

   For each token that does NOT already exist, create it before building the tree:
   - `novamira/bricks-create-variable` for spacing/typography/raw color tokens (referenced as `var(--name)` in element settings)
   - `novamira/bricks-add-color-palette-entry` for branded colors that should appear in the palette picker
   - `novamira/bricks-create-global-class` for reusable bundles
   - `novamira/bricks-create-theme-style` to set element-type defaults (h1-h6, p, a, button) — see "High-impact actions" below before doing this.

   Naming: kebab-case, semantic (`brand-primary`, `space-md`, `text-xl`). NOT presentational (`blue-1`, `large`).

   **Manage these stores only through their dedicated abilities — never write their options directly via `execute-php`.** `bricks-create-variable` / `bricks-edit-variable` / `bricks-delete-variable` generate each variable's internal `id` server-side and reject duplicate names; hand-assembling the `bricks_global_variables` option (e.g. copying an existing entry as a template) risks giving two variables the same `id`, which Bricks renders as selecting one variable highlighting several — a corrupt state the customer then has to repair by hand. The same holds for the color palette, global classes, and theme styles: use their abilities, not a raw option write.

   **Native dark mode (Bricks 2.2+)** lives on the color palette, NOT on variables: give palette colors a dark variant via `bricks-add-color-palette-entry` / `bricks-edit-color-palette-entry` with the `dark` parameter (setting it on an existing color automatically migrates its value to the light/dark format the Color Manager uses). Once at least one color has a dark variant (`bricks-list-color-palette` reports `dark_mode_ready: true`), the builder offers the native `toggle-mode` element and the front end switches via the `data-brx-theme` attribute — no custom CSS or `[data-theme]` workaround needed. Global variables have no dark support in Bricks; put dark-aware colors in the palette instead.

3. **Check Bricks setup.** Call `novamira/bricks-check-setup` and inspect the result. If `supported_post_types` does not include the post type you are about to create (typically `page`), fix it first by calling `novamira/bricks-set-post-types` with a flat `post_types` array that includes the required type — e.g. `{"post_types": ["page", "post"]}`. Do not use generic `bricks-set-settings` for this setup step. Without this, the post will save but the "Edit with Bricks" link will not appear in the admin bar, and Bricks will not render the page. Also check `theme_active` and `license_valid`; if either is false, warn the user before proceeding.

4. **Create the post.** Use `novamira/create-post` if available, otherwise `novamira/execute-php` calling `wp_insert_post([...])` with `post_type => 'page'`, `post_status => 'publish'`, and a `post_name` slug. Capture the returned `post_id`.

5. **Populate the element tree.** Call `novamira/bricks-set-content` with the `post_id` and the `elements` array. The tree must reference tokens, not raw values — see "Style hierarchy" below.

6. **Configure page-level layout.** Call `novamira/bricks-set-settings` with the same `post_id` and only the layout keys you need to override. Do NOT pass `templateType` on a regular page (that key is reserved for `bricks_template` posts and will break the "Edit with Bricks" link).

Steps 5 and 6 can be done in either order, but doing content first lets you verify the tree renders before touching layout.

## Style hierarchy

**Never create a stylesheet.** Do not dump CSS into page-level `customCss`, a global custom-CSS blob, or a file enqueued via `execute-php`, and then reference it from elements. That CSS is invisible and uneditable to the customer in the Bricks UI — it's exactly what they don't want. The editable unit of style is a **named global class** styled through Bricks fields. If a rule applies to more than one element, it's a global class: create it (`bricks-create-global-class`) and apply it via `_cssGlobalClasses`, don't write a stylesheet.

Tier 1 — Theme styles defaults: respect them. Don't override `_typography` on h1 if a theme style already defines h1 typography.
Tier 2 — Apply existing global class via `_cssGlobalClasses: ["clshov"]`.
Tier 3 — Reference existing variable in control values: `"color": {"raw": "var(--brand-primary)"}`.
Tier 4 — Create token via granular ability (Phase 2 above), then reference.
Tier 5 — Raw values in controls — only for non-systemic one-offs: animation timings, single transforms, clip-paths.
Tier 6 — `_cssCustom` — last resort, only for things no control covers (gradients, complex pseudo-element behavior).

Hex colors, px values on `_padding`/`_margin`/`_gap`, font sizes/weights/family, radii → NEVER raw. Always tokenize.

Layout, sizing and alignment all have **native controls** — never reach for `_cssCustom` (or page-level `customCss`) for them. The native keys: alignment → `_justifyContent` / `_alignItems` (flex; text alignment lives under `_typography`); gap → `_rowGap` / `_columnGap` (flex) or `_gridGap` (grid); width/height → `_width` / `_widthMin` / `_widthMax` / `_height` / `_heightMin` / `_heightMax`; spacing → `_padding` / `_margin`. Flex/grid keys require `_display: "flex"` (or `"grid"`) on the same element. If you don't know the key for a property, look it up with `bricks-list-elements element=<name>` — do not fall back to custom CSS for something a control already covers. Raw CSS is Tier 6, for properties no control exposes.

## Element hierarchy

**Build with native Bricks elements, not the `code` element.** Every piece of content and layout has a native element — `heading`, `text-basic`, `button`, `image`, `container`, `block`, `icon`, `nav`, and the rest (discover them with `bricks-list-elements`). The `code` element (raw HTML/CSS/JS) is a last resort for genuinely custom markup that no native element can express; never reach for it to render text, headings, buttons, or to hand-write HTML layout you could build natively. The write abilities enforce this: `bricks-set-content` / `bricks-insert-content` **reject** a tree containing a `code` element and ask you to confirm — pass `allow_code_elements: true` only after the user has explicitly said they want a raw code/HTML element.

For typical content sections, Bricks pages follow a three-tier nesting: **section → container → block-level element**. This is the default the Bricks editor uses when inserting new layout, and it's the shape that picks up theme-style spacing predictably.

- `section` — full-width layout band, top-level child of the document root. Its typical direct child is a `container`.
- `container` — centered/constrained wrapper, conventional direct child of `section` and conventional parent of block-level elements (`heading`, `text-basic`, `button`, `image`, …).
- `block` — block-level grouping wrapper for clustering related elements inside a `container`. For an entire layout band prefer `container`; use `block` when you specifically need a nested grouping.

Typical skeleton for a content section (parent references; `bricks-set-content` computes `children` from them):

```json
[
  {"id": "sec1",  "name": "section",   "parent": 0,      "settings": {}},
  {"id": "ctn1",  "name": "container", "parent": "sec1", "settings": {}},
  {"id": "head1", "name": "heading",   "parent": "ctn1", "settings": {"text": "Hello"}}
]
```

**Skip the container when the intent calls for it**, not as a shortcut. Legitimate cases:

- **Full-bleed hero** — the section spans edge-to-edge and the content sits directly inside without horizontal constraint.
- **Edge-to-edge media** — a gallery, slider, or image strip that must touch the viewport edges.
- **Section already constrained via theme style** — if the theme style on `section` already sets a sensible `max-width` and `padding`, adding a `container` inside double-constrains the layout.

If the user's request is "build me a content section" without a specific full-bleed intent, default to the section > container > block-level shape.

## Static vs. query loop on repeated content

WordPress sites are CMS-driven, so **repeated content on a page is a query loop, not N hand-typed cards.** Default to a loop whenever a section shows more than one of "the same thing" — blog posts, articles, projects, case studies, portfolio items, products, services, team members, events, testimonials, rentals, locations, jobs, listings, anything the customer will add or edit over time. The customer expects to manage these from the WordPress admin; static markup on the page traps the data inside Bricks and breaks every other surface that should reference it (search, archives, single-post pages, related strips).

Static is the exception, reserved for **fixed UI sections with bespoke copy** authored for this page:

- Pricing tiers (multi-column comparison curated for this landing)
- Feature pillars / USP grid
- "How it works" steps
- Hero / sub-hero
- Brand or partner logo strip
- Hand-picked testimonials tied to this page
- Small fixed FAQ inline with the marketing copy

Three-question sanity check when in doubt:

1. Would each card have a dedicated detail page (`/projects/xyz`, `/villas/santorini-cliff-house`)?
2. Would these entities appear anywhere else on the site (search, archive, related strips, single pages)?
3. Will the customer add, remove, or edit them over time without a developer?

A "yes" to any of the three means the section is loop territory. The number of items at build time is irrelevant — the CPT can be empty or have ten thousand entries, the page tree is identical either way.

### Discover the target post type before building

Before wiring the loop (and before proposing to create a new CPT), list what's already registered on the site and match against the user's intent — they may already have set up the type, especially on a mature site. The same calls are the right answer when the user explicitly asks "what items do we have under X here?".

Call the post-types list ability of whichever field-plugin specialization is active in this session (e.g. `novamira/pods-list-pods`, `novamira/acf-list-post-types`, and the equivalent in any other specialization the abilities list shows). Match the user's wording to the returned slug or label; account for plural/singular and reasonable variation — the customer's spoken term may not be the verbatim slug. Use any reasonable match instead of proposing a new CPT; only fall through to "No matching CPT yet" when nothing in the listing matches.

There is no vanilla `novamira/wordpress-list-post-types` ability yet, so a CPT registered by anything other than a Novamira-supported field-plugin specialization (any of the many other CPT-management plugins, a theme, JetEngine's own UI, raw `register_post_type()` in custom code, etc.) is invisible to the specialization-aware discovery path. When you suspect the type exists but no specialization owns it, fall back to a read-only `novamira/execute-php` call — `return get_post_types(['_builtin' => false, 'public' => true], 'objects');` returns every non-built-in public post type registered on the site, regardless of how it was declared. This is exactly the use case `execute-php` exists for: a small, read-only WP introspection with no side effects.

### Loop shape in Bricks

**Bricks repeats the element that carries the loop.** So the element that
defines the grid must NOT be the looped one: put the grid styles
(`_display: grid`, `_gridTemplateColumns`, …) on a plain `container`, and set
`hasLoop: true` + the `query` block on its **direct child** (a `block` — the
card). Bricks then renders ONE grid whose repeated card children flow into the
columns. Dynamic-data tokens (`{post_title}`, `{post_excerpt}`,
`{featured_image}`, …) resolve inside the looped card because Bricks switches
the global query post for each iteration.

```json
[
  {"id":"sec1","name":"section","parent":0,"settings":{}},
  {"id":"grid","name":"container","parent":"sec1","settings":{
    "_display":"grid","_gridTemplateColumns":"repeat(3, 1fr)","_gridGap":"32px"
  }},
  {"id":"card","name":"block","parent":"grid","settings":{
    "hasLoop":true,
    "query":{"objectType":"post","post_type":["post"],"posts_per_page":6,"orderby":"date","order":"DESC"}
  }},
  {"id":"img","name":"image","parent":"card","settings":{"image":{"useDynamicData":"{featured_image}"}}},
  {"id":"hd","name":"heading","parent":"card","settings":{"text":"{post_title}","tag":"h3"}},
  {"id":"ex","name":"text-basic","parent":"card","settings":{"text":"{post_excerpt}","tag":"p"}}
]
```

- **Never put `hasLoop` on the grid-defining container.** That renders N
  separate one-card grids stacked vertically — the columns never fill, and the
  page reads as a list no matter what `grid-template-columns` says. (Verified
  on the front end: the broken shape renders repeated
  `<div class="brxe-grid" data-brx-loop-start="grid">` wrappers with one card
  each; the correct shape renders one `id="brxe-grid"` wrapper with N
  `brxe-card` siblings inside.) A loop on the section is even worse: N
  full-bleed bands.
- The looped `block` is the per-item template and the natural card boundary
  for styling; everything inside it repeats per post.
- Native WordPress tokens (`{post_title}`, `{post_url}`, `{post_excerpt}`,
  `{featured_image}`) need no extra skill — use them directly. Load the
  `dynamic-data-binding` skill only when the per-item template touches a
  custom field (ACF / Pods / JetEngine / Meta Box / …) or a token does not
  resolve.
- Pagination and Bricks Query Filter elements must target the element that
  actually carries the loop (the card), not the grid wrapper.
- For CPTs registered by a field plugin (Pods / ACF / JE / MB / ACPT / ASE), use the CPT slug in `query.post_type` — discover slugs via the active specialization's list ability (`pods-list-pods`, `acf-list-post-types`, `jetengine-list-post-types`, …) or `wordpress-list-post-types`.
- For taxonomy or user queries use `query.objectType: "term"` or `"user"` respectively.

#### Query Loop: Supported objectType and per-item data binding

Three objectType values are supported in the `query` block of a looped element:

- **`"post"`** — Query by post type. Use `query.post_type: ["slug"]` to target one or more registered post types (built-in: `["post"]`, `["page"]`; CPTs: any slug from Pods / ACF / JetEngine / Meta Box / ACPT / ASE).
- **`"term"`** — Query by taxonomy terms.
- **`"user"`** — Query by user account.

The remaining `query` sub-keys (taxonomy selection for `"term"`, role/role filters for `"user"`, pagination, ordering, etc.) are Bricks-native and are not enumerated by the abilities — the `query` block is passed through to Bricks verbatim by `bricks-set-content`. Confirm the exact key names against a working Bricks loop in the editor before relying on them; do not guess attribute names.

**Per-item data binding**: The looped element and its children use Bricks' `{token}` syntax to access iteration-scoped data (e.g., `{post_title}`, `{post_excerpt}`, `{featured_image}` on a post loop). Tokens are embedded directly in element settings: `{"text":"{post_title}"}` or `{"image":{"useDynamicData":"{featured_image}"}}`. Before deploying, verify token existence with `novamira/bricks-list-dynamic-data` and test resolution on a sample post with `novamira/bricks-resolve-dynamic-data` — unknown tokens render as their literal `{text}`.
- Verify a token resolves on a representative post via `novamira/bricks-resolve-dynamic-data` before assuming `{acf_*}` / `{pods_*}` / `{je_*}` are right — each provider has its own format and silent-fails differently. Load the `dynamic-data-binding` skill the moment the loop's per-item template touches a custom field.

#### Query Filters & pagination (front-end filtering of a loop)

Bricks ships filter elements (`filter-select`, `filter-checkbox`,
`filter-radio`, `filter-search`, `filter-range`, `filter-datepicker`,
`filter-submit`, `filter-active-filters`) and `pagination` that filter a query
loop via AJAX or URL parameters. They have three failure modes that all look
like success, so follow this order strictly:

1. **Check the prerequisite FIRST.** `bricks-check-setup` returns
   `query_filters_enabled`. If false, ask the user to turn on "Enable query
   sort / filter / live search" in Bricks → Settings before you write any
   filter element. Order matters: a filter saved while the feature is OFF is
   never registered for indexing and stays inert even after the toggle is
   flipped — if that happened, re-save the page/template (any content write)
   to register it.
2. **Every filter and the pagination element must set `filterQueryId`** to the
   id of the element that CARRIES the loop (the card in the recipe above,
   never the grid wrapper). An empty `filterQueryId` is a dead element: it
   saves fine, renders, and does nothing. Place filters OUTSIDE the looped
   element (e.g. a sibling of the grid in the same section), and never inside
   a component — component instances remap element ids, which severs the
   `filterQueryId` link.
3. **Sources**: `filterSource: "taxonomy"` + `filterTaxonomy: "<tax>"` for
   term filters; `"wpField"` for WP core fields; `"customField"` needs the
   separate "Custom fields integration" toggle in the same settings screen.
4. **Filters run on a background index.** Index jobs are queued when the
   content saves and processed by the `bricks_indexer` cron (every 5
   minutes). Until indexed, the filter renders its options disabled with
   `(0)` counts and filtering is a no-op. If that state persists, the index
   simply has not run yet — wait for the cron or ask the user to open the
   Bricks settings' filters tab which processes it.
5. **Verify from the front end, not from the save response.** Every filter
   listens on the GET parameter `brx_<filter-element-id>` (term SLUG as the
   value; `filterNiceName` renames the parameter). Fetch the page URL with
   e.g. `?brx_flt1=news` and confirm the loop renders only the matching
   items, and that the filter options carry real counts — that one fetch
   proves enablement, target wiring, and index in a single pass.

### No matching CPT yet

The CPT may not exist when you start. Discovery first: list installed plugins (the `<plugin>-check-setup` ability of each field-plugin specialization tells you whether that plugin is active — call the ones you have).

- **A field-plugin specialization is active** (Pods / ACF Pro / JetEngine / Meta Box / ACPT / ASE Pro): propose creating the CPT through that plugin's specialization, then wire the loop on top. An empty CPT plus a wired loop is the right state to ship; the customer fills the content from the admin afterwards.
- **No field-plugin specialization is active**: there is no CPT-management UI on this site. Stop and tell the user — vanilla WordPress does not expose CPT registration to non-developers, and the section cannot loop against real content until one of those plugins is installed. Do not register the CPT via raw `register_post_type()` calls in agent-generated code; that would create a CPT the customer can't manage afterwards.

**Do not fall back to a static grid because the CPT is missing.** The whole point of the loop is that the content lives in the CMS — a static grid is the failure mode this section exists to prevent.

## High-impact actions need explicit confirmation

Before any of these, ask the user:
- Creating a theme_style (any scope): "Site-wide o solo questa pagina? Preferisci scope ristretto se non chiaro."
- Creating an initial set of tokens upfront on a sparse store — concretely: 3+ new variables, 2+ new global classes, or any combination on top of an empty/near-empty store. Show the user the proposed names and values before persisting. On a site with an established design system the agent reuses existing tokens (Step 1 inventory) and rarely hits this threshold.
- Modifying any existing variable / class / theme_style.
- Deleting anything.

Phrasing: "Sto per [azione]. Confermi?"

## Tool choice

Use the smallest tool that matches the edit:

| Intent | Tool |
| --- | --- |
| Create a new Bricks page from scratch | `create-post` + `bricks-set-content` |
| Update settings on existing elements | `bricks-patch-elements` |
| Insert a new subtree into an existing page | `bricks-insert-content` |
| Replace the entire existing tree | `bricks-set-content` |

## Blank canvas / full-bleed recipe

The most common landing-page intent. Bricks already defaults to a wide
(full-width) layout, so the "full-bleed" work is mostly about *removing*
chrome rather than turning wide mode on.

```json
{
  "post_id": 17,
  "settings": {
    "headerDisabled": true,
    "footerDisabled": true,
    "contentMargin": { "top": 0, "right": 0, "bottom": 0, "left": 0 }
  }
}
```

Notes and gotchas:

- **`siteLayout` is already `wide` by default.** Only set it explicitly
  (`"siteLayout": "wide"`) if a theme style or previous save set it to
  `boxed`. Setting it redundantly is harmless but noisy.
- **`contentMargin` shape is a spacing object** (`top`/`right`/`bottom`/`left`,
  optionally with a `unit`). Drill into it with
  `novamira/bricks-list-settings key=contentMargin` for the authoritative
  shape before writing it.
- **There is no `hideTitle` key.** Bricks-rendered pages do not inject a
  theme page-title banner at all — the Bricks template replaces the theme's
  content area. If a title is appearing, it is coming from the first element
  in your tree, not from a setting.
- **`customCss` resets are rarely needed.** Try `headerDisabled` +
  `footerDisabled` + `contentMargin: 0` first; only add `customCss` if
  something is still leaking through from the active theme (not Bricks).
- **Bricks theme vs. WordPress theme.** These keys only apply to the Bricks
  rendering path. If the active theme is not Bricks and the page is rendered
  through a non-Bricks template, none of this has any effect.

## Common element gotchas

- **`icon-box` content is a single `content` key with raw HTML.** There are
  no separate `title` / `text` keys — both the heading and body live in one
  `editor`-type field. Passing `title` or `text` saves silently but renders
  nothing.

```json
{
  "content": "<h4>Card heading</h4><p>Body text goes here.</p>"
}
```

  Use `typographyHeading` / `typographyBody` to style the two parts independently.

- **`icon-box` internal layout uses shared flex controls.** The element has
  its own `direction` and `gap` controls for positioning the icon relative to
  the content, but the validator currently flags them; see
  `issues/bricks-icon-box-validator-false-positive.md`. Until that is fixed,
  the safe recipe for a vertically stacked icon-box card is:

```json
{
  "_display": "flex",
  "_direction": "column",
  "_rowGap": "16px"
}
```

  Shared flex controls (`_direction`, `_rowGap`, `_columnGap`,
  `_justifyContent`, `_alignItems`) require `"_display": "flex"` on the
  same element. Without it, Bricks strips the flex props and the element falls
  back to block layout.
- **`div` as a grid wrapper may need explicit width.** A common features-grid
  wrapper is `_display: "grid"` + `_gridTemplateColumns: "repeat(3, 1fr)"` +
  `_gridGap: "32px"`. Inside a flex-column parent, add `_width: "100%"` when
  the wrapper is not stretching to full width by default.
- **`button` outline styling has a native control.** Prefer the Bricks
  `outline` checkbox over manually zeroing `_background` and adding `_border`
  unless you specifically need a custom variant.
- **`button` link shape uses `type: "external"` for plain URLs — not
  `"url"`.** The Bricks link control writes `{"type":"external","url":"..."}`
  when the editor picks "URL" in the dropdown. Saving `{"type":"url","url":"..."}`
  is accepted by `set-content` but the button renders with no `href`
  (the element tag falls back to `<div>` behaviour). Other link types:
  `internal` with `postId`, `taxonomy` with `term`, `meta`, `lightboxImage`,
  `lightboxVideo`, `media` — see `set_link_attributes()` in Bricks for the
  full list. This applies to every element with a link control, not just buttons.
- **`svg` from the media library: omit `source`, do not send `source: ""`.**
  The "File" option's value in the schema is the empty string, but Bricks
  renders the file only when `source` resolves to `"file"` — a literal `""`
  fails that check and the SVG comes out blank. Send just
  `{"file":{"id":<attachment-id>}}` and leave `source` out (the ability strips
  an empty `source` for you). Use `source: "code"` only for inline SVG markup
  (see next point).
- **Avoid the `code` element — it is gated and needs site code execution.** The
  `code` element (and `svg` "Code" source) is raw HTML/CSS/JS and a last resort:
  prefer a native element for anything you can express natively.
  `bricks-set-content` / `bricks-insert-content` reject a tree containing a
  `code` element unless you pass `allow_code_elements: true`, which you set only
  after the user confirms. Even when allowed, it needs code execution: Bricks
  signs code server-side and refuses to render an unsigned payload, so it only
  renders when *Bricks → Settings → Custom code → Code execution* is on **and**
  the calling user has the Bricks "Execute code" capability. When both hold the
  write signs it; when they don't, it saves but renders empty (a warning is
  returned) — surface that to the user. For a plain media-file SVG you never need
  any of this; prefer the `file` source above.

## Interactions (animations, show/hide, scroll-to, …)

Per-element interactions — hover effects, click handlers that toggle an
offcanvas or open a popup, entry animations, scroll-triggered behavior —
live on the element's `settings._interactions`. Do **not** edit that array
by hand with `set-content`/`patch-elements`: use the dedicated abilities.

1. `novamira/bricks-list-interaction-events-and-actions` — call once to
   discover the valid `trigger` keys (click, mouseover, enterView, scroll,
   formSubmit, …) and `action` keys (show, hide, toggleOffCanvas,
   startAnimation, scrollTo, setAttribute, javascript, …), plus the
   per-field `required` dependencies that spell out which extra keys apply
   to which action (e.g. `actionAttributeKey`/`actionAttributeValue` for
   setAttribute; `animationType` for startAnimation).
2. `novamira/bricks-add-interaction` — append one. The server assigns the
   id; pass the rest of the Bricks-native shape.
3. `novamira/bricks-list-interactions` / `novamira/bricks-edit-interaction`
   / `novamira/bricks-delete-interaction` — read, merge-patch, delete.

Bricks calls the event "trigger" everywhere in its code and UI — use
`trigger` (not `event`) when building the interaction object.

## Dynamic data tokens

Bricks embeds dynamic values with `{tokens}` (e.g. `{post_title}`,
`{featured_image}`, `{acf_headline}`). Unknown tokens render as their literal
text — Bricks does not error — so blind guessing ships pages with
`{acf:headlien}` printed on them.

Before embedding a token:

1. Call `novamira/bricks-list-dynamic-data` (filter with `provider` or
   `search`) to confirm the token is registered in this environment.
2. Call `novamira/bricks-resolve-dynamic-data` with the target `post_id` to
   verify it resolves non-empty on the page you're building.

Gotcha: the leading space before `@fallback` is required —
`{tag @fallback:'Untitled'}` works, `{tag@fallback:'Untitled'}` renders
literally. Same rule for `@fallback-image`. Bricks' parser splits arguments
on `\s+@`.

When binding a field-plugin token (ACF / Pods / JE / MB / ACPT / ASE), each
provider uses its own token format and silent-fails differently — activate
the `dynamic-data-binding` skill for the per-provider format reference and
the trap patterns to avoid.

## Intent → keys map

Each intent lists the `bricks-set-settings` keys that implement it. Values
are illustrative — always consult `bricks-list-settings key=<key>` for the
exact shape before writing.

| Intent | Keys |
| --- | --- |
| Disable site header | `headerDisabled: true` |
| Disable site footer | `footerDisabled: true` |
| Disable popups on this page | `popupDisabled: true` |
| Zero the content margin (full-bleed) | `contentMargin: {top:0,right:0,bottom:0,left:0}` |
| Force boxed layout | `siteLayout: "boxed"` |
| Force wide layout (usually already the default) | `siteLayout: "wide"` |
| Constrain container max width | `containerMaxWidth: {…number+unit…}` |
| Add classes to `<body>` | `bodyClasses: ["landing","dark"]` |
| Inject `<head>` scripts | `customScriptsHeader: "<script>…</script>"` |
| Inject opening `<body>` scripts | `customScriptsBodyHeader: "…"` |
| Inject closing `<body>` scripts | `customScriptsBodyFooter: "…"` |
| Page-scoped custom CSS | `customCss: "…"` |
| Disable lazy loading on this page | `disableLazyLoad: true` |

Note: the actual Bricks keys are `headerDisabled` / `footerDisabled` —
**not** `noHeader` / `noFooter`. There is no `hideTitle` and no `customJs`.

## Permalink awareness

`wp_insert_post` accepts `post_name` for the slug, but the *displayed*
permalink depends on `get_option('permalink_structure')`. If that option is
empty (plain permalinks), the returned URL will be `?page_id=N` and the slug
is invisible in the URL. When handing a URL back to the user, either:

- verify `permalink_structure` is non-empty first, or
- note the caveat if you cannot verify, so the user is not surprised when
  `/<slug>/` doesn't resolve.

## CSS regeneration

In the default **inline** CSS mode you do not need to regenerate anything after
editing — Bricks builds the CSS at render time and the write abilities already
clear the page cache. Don't spend calls "regenerating" or hunting for a CSS
cache to clear; in this mode there isn't one.

Only when the site loads CSS as **external files** (Bricks → Settings →
Performance → "CSS loading method") do the generated `.css` files need
rebuilding after **global** style changes (theme styles, global classes,
colors, variables). Do it once, at the end of a batch of edits — not per edit:

- `novamira/run-wp-cli` with `{"args": ["bricks", "regenerate_assets"]}`, or
- `novamira/execute-php` calling `\Bricks\Breakpoints::regenerate_bricks_css_files()`.

If you can't tell which mode the site uses, running `regenerate_assets` once at
the end is harmless in inline mode (the rebuilt files just go unused), so it's
the safe default after global-style work.

## What belongs elsewhere

- **Element tree construction** — see `novamira/bricks-list-elements` for the
  element catalogue and per-element control shapes. The element tree is
  orders of magnitude larger than page settings and is not covered here.
- **Global Bricks settings** (site-wide, not page-scoped) — use
  `novamira/bricks-set-settings` with `post_id` omitted, and see
  `novamira/bricks-list-settings scope=global`.
- **Template posts** (`bricks_template` post type: headers, footers, popups,
  section templates) — these are created with
  `novamira/bricks-create-template` (not `create-post`) and have their own
  settings blob. A freshly-created template with no **conditions** will not
  render — headers and footers silently disappear from the site. Use
  `novamira/bricks-list-template-condition-schema` to learn the condition
  shape, then `novamira/bricks-set-template-conditions` to attach at least
  one condition (e.g. `[{"main": "any"}]` for entire-website scope). List
  existing conditions with `novamira/bricks-list-template-conditions`.
