# 14 · Blocks, Templates, and Block Styling

Read this when an extension ships visual-builder blocks, templates, presets, or
starter authored documents. It covers the builder contribution contracts, the
public block runtime contract, and the authoritative styling/theme-token
contract for block CSS.

## Current model

- Visual-builder contributions are Cedros extension plugin contributions.
- Runtime block types must be namespaced as `extension-id:block-id`; runtime
  template ids as `extension-id:template-id`.
- Manifest metadata exposes inventory and surface ownership; host-available JS
  runtime definitions provide actual rendering/editing.
- Blocks store portable authored-document props, not React snippets, CSS class
  payloads, DOM structure, raw HTML, script/style text, or custom runtime code.
- Block *visuals* live in the runtime definition, not the document: ship your
  block CSS in the archive and consume the host runtime theme tokens. The host
  styles nothing for you.
- Public blocks have two moments, not two products: the first HTML Cedros Data
  serves and the browser-running component after the extension code starts.
  Both moments must represent the same block contract — same root semantics,
  layout, visual hierarchy, labels, spacing, theme tokens, and supported
  actions.
- A styled static shell with inert buttons is only a loading or no-JS fallback.
  It is not the completed public block unless the live component keeps that
  same presentation while wiring the real behavior.
- Visual-builder contributions do not create arbitrary public routes. Pair
  templates with `pageSeeds` ([`13-public-pages-and-seo.md`](13-public-pages-and-seo.md))
  only when the page function belongs to the extension. Customer pricing and
  marketing pages should compose data APIs or optional blocks in site-owned
  authored content; they must not require customizing the extension.
- Builder-native component snapshots are currently handoff-only unless Cedros
  publishes a snapshot schema. Do not add snapshot manifest fields.

## What you declare and build

Manifest metadata ([field reference](04-manifest-reference.md)):

- `contentBlocks[]`, `contentTemplates[]`
- optional `authoredSurfaces[]`, `authoredTemplateUtilities[]`
- `surfaces.web.blockTypes`, `surfaces.web.templateIds`,
  `surfaces.web.authoredSurfaceIds`, `surfaces.web.templateUtilityIds`

Runtime bootstrap:

- content block definitions and content template definitions
- executable web block components for every declared public block type, either
  in `contentBlocks[].Component` or in `contentBlockRenderers[]`
- browser-action wiring for every interactive control declared by the block
- first-render fallback/shell components only when they are paired with the
  working browser component and documented as equivalent
- sample authored documents or presets
- native fallback or unsupported-state notes

If no custom ordering is needed, use `0` or omit `order` entirely. Both
`contentBlocks[].order` and `contentTemplates[].order` are optional: the host
registry normalizer defaults a missing `order` to `1000`.

Current runtime content types are `page`, `blog`, `docs`, `course`, `airdrop`,
`faq`, `directory`, and `collection`. `category` is a stable free string unless
Cedros publishes a category enum.

Prop schemas should be JSON object schemas using the subset supported by the
published builder SDK. Current safe JSON Schema subset for examples: root
`type: "object"`, `properties`, `required`, scalar property types (`string`,
`number`, `integer`, `boolean`), arrays with simple `items`, nested objects
with their own `properties`, and `enum` for small stable option sets. Treat
`$ref`, `oneOf`, `anyOf`, `allOf`, conditional schemas, custom formats, remote
schemas, and custom UI widgets as host-coordinated unless the builder SDK
publishes support for them.

Runtime registration shape:

```ts
export function registerCedrosWebSurface() {
  return {
    manifest,
    contentBlocks: [
      {
        type: "acme-demo:hero",
        displayName: "Hero",
        category: "marketing",
        Component: HeroBlock,
        defaultProps: { heading: "Welcome" },
        validateProps: validateHeroProps
      }
    ],
    contentTemplates: [
      {
        id: "acme-demo:landing",
        label: "Landing Page",
        contentTypes: ["page"],
        zones: [{ id: "main", label: "Main", order: 0 }],
        blocks: [{ type: "acme-demo:hero", zoneId: "main" }]
      }
    ]
  };
}
```

Block definition fields the host reads: `type`, `displayName`, `category`,
`Component`, `defaultProps`, `builder`, `validateProps`, and `preview`.
`type`, `displayName`, and `category` are hard-required: the host registry
normalizer **silently drops** any block definition missing one of them — no
error is raised, the block simply resolves as "unavailable" everywhere.
`schema` is not a field the host reads; do not put one in the registration
shape.

Template definition fields (`CedrosDocumentTemplateDefinition`): `id`, `label`,
`contentTypes`, `zones`, and optional `blocks`. `label` is the canonical
runtime field (`displayName` is accepted as a fallback). Always declare
`zones: [{ id, label, order }]` — a missing `zones` defaults to `[]`, leaving
starter blocks and authors no zone to land in. `blocks` is the optional
starter-block seed list of `{ type, props?, zoneId? }` entries placed into the
named zone.

For public web blocks, `contentBlocks[].Component` is the working component the
Cedros page runtime loads for that block. It is not just an admin preview
helper. If the SDK surface uses `contentBlockRenderers[]` instead, each
renderer must point to the same working component contract. Do not ship
metadata-only `contentBlocks[]` for a block type that appears on public pages
unless the release packet explicitly marks the block as non-interactive and
visually complete without browser code.

What your component receives:

- `contentBlocks[].Component` is called with exactly `{ props }` — the authored
  document props and nothing else. The host injects **no** feature flags,
  runtime base URLs, or provider client ids into the component. A
  `Component`-based block that needs the site color scheme uses the DOM/event
  seams below (or a `contentBlockRenderers[]` entry, which receives
  `colorMode`).
- `contentBlockRenderers[].render` is called with
  `{ block, siteData, onNavigate, colorMode }` and takes precedence over
  `contentBlocks[].Component` for the same block type. `colorMode` is the
  resolved site color scheme (`"light"` or `"dark"`, with the visitor's
  "system" preference already resolved against the OS); public hydration
  re-invokes `render` with the fresh value whenever the visitor toggles. It is
  `undefined` during SSR and on hosts that predate it.
- An extension that needs runtime config must fetch it from its own
  `/extensions/{id}/...` endpoints, as cedros-login does via
  `/extensions/cedros-login/features`.

Use only real SDK type names and exports that are available in the target SDK;
otherwise mark the missing helper as host-coordinated.

## Public block runtime contract

Use this section for every block that can appear on a public page, especially
forms, product cards, carts, checkout widgets, account controls, wallets, media
uploaders, search, filters, or any block with buttons/inputs.

### Ownership

Cedros Data owns the public page row, URL, page shell, authored document, theme
tokens, extension package loading, and public runtime base path. The extension
owns the block that appears inside the page. That includes:

- the first visible markup for the block
- the browser-running React component
- the block stylesheet
- provider/client SDK loading inside that block
- fetching its own runtime config — feature flags, runtime base URLs, provider
  client ids, checkout settings — from the extension's `/extensions/{id}/...`
  endpoints (the host injects none of these into the component)
- all loading, disabled, error, empty, and success states
- accessible names, labels, focus order, and keyboard behavior

Do not rely on Cedros Data to style extension-owned classes, invent missing
provider config, wire button clicks, or preserve a placeholder's appearance
after the working component starts. If the extension needs host data, declare
exactly which prop or runtime endpoint supplies it.

### Runtime bundle and import constraints

A public block runs from the ZIP-contained public web chunk, loaded by the same
host module loader as admin runtimes
([`06-runtime-sdk-reference.md`](06-runtime-sdk-reference.md), "ZIP runtime
loader rules"). The author-facing contract resolves only four bare imports — `react`,
`react/jsx-runtime`, `react/jsx-dev-runtime`, and `cedros:admin`.
Everything else must be bundled into the chunk.

`react-dom` is one common trap, because in a public chunk it is neither shimmed
nor safely bundleable. It is not the only source of this failure: Motion,
animation libraries, editors, and any bundled dependency can retain executable
`process.env` or another Node-global reference, and the same rule applies to
admin chunks.

- Bundling `react-dom` pulls `process.env.NODE_ENV` branches into a browser
  chunk that has no `process`, breaking the chunk. So the public web build must
  externalize `react-dom`.
- But an externalized `react-dom` ships as a bare specifier the loader cannot
  resolve. A static `import ... from "react-dom"` fails the whole load with
  "is not bundled"; a dynamic `import("react-dom")` survives to the browser and
  throws at mount time — a blank or broken public page rather than a clean load
  error.

So a host-mounted public chunk must never import `react-dom`,
`react-dom/client`, or `createPortal` at all — directly or transitively. The
host owns mounting; the block only needs `react`. This usually regresses
through a transitive import from a portal-based UI library: `sonner` and the
Radix primitives (`@radix-ui/react-dialog`, `-select`, `-popover`, `-tooltip`,
`-dropdown-menu`, `-portal`) all pull in `react-dom`. Use host-provided
primitives or portal-free alternatives in public blocks.

Verify before release: externalize `react-dom` in the public web build config,
then grep the **built** public chunk (not just source) for any `react-dom`
import. Separately validate every resolved admin/web ESM chunk for Node globals
and import the packaged entrypoints in a browser with `globalThis.process`
absent. Wire both checks into packaging so they cannot silently regress.

### One component or equivalent shell/live pair

Prefer one component that can render safely for the first server/public render
and then attach browser behavior. If browser-only dependencies make that
impractical, use a shell/live pair:

- the shell must use the same root element intent, block type attributes,
  spacing, labels, provider ordering, and theme-token CSS as the live component
- the live component must not switch to a different design system, card model,
  button size, typography scale, icon set, palette, or layout unless the
  product owner has approved that visual change
- the shell may disable controls or prevent form submission while waiting for
  browser code, but those controls must become functional in the live
  component without changing their visible identity
- any `Suspense`, lazy import, wallet SDK, payment SDK, OAuth SDK, or provider
  loader must resolve to the same styled UI, not to a standalone package
  default with unrelated styling
- no "real form" may be less styled than the placeholder it replaces

This is a hard gate: if a screenshot review passes because it sees only the
shell, but the live component looks or behaves differently, the extension is
not release-ready.

### Data and action flow record

Every interactive public block must include a short data/action table in the
feature brief or release packet.

| Control | Source data/config | Runtime endpoint/provider | Expected request/action | Success state | Error state |
| --- | --- | --- | --- | --- | --- |
| Login email submit | block props + `/extensions/cedros-login/features` | `/extensions/cedros-login/login` | POST credentials | session cookie set + redirect/account state | inline auth error |
| Google login button | `/features.googleClientId` | Google GIS + `/extensions/cedros-login/google` | provider token exchange | session cookie set | provider unavailable/auth error |
| Product add-to-cart | product id prop + catalog/cart runtime state | `/extensions/cedros-shop/cart/items` | POST item quantity | cart count/line item updates | inventory/price/error message |
| Checkout button | cart id + payment settings | checkout/payment route | create checkout session/order | checkout handoff or confirmation | unavailable/payment/setup error |

Use concrete ids and paths for the extension being authored. The examples
above are patterns, not generic placeholders.

### Public acceptance checks

For every interactive block on a seeded public page:

- open the actual public route created by the page seed, not only an isolated
  component story
- confirm the first visible shell and final browser-running component are
  visually equivalent at mobile and desktop widths
- confirm the final component retains the intended root block semantics and
  accessible control names
- click every enabled provider/action button and submit every form mode
  included in the release scope
- verify the expected network request or provider SDK call happens
- verify loading, disabled, error, and success states are visible and styled
- verify required runtime config (fetched from the extension's own endpoints)
  is present before enabling provider buttons; hide or disable unavailable
  providers with a clear state
- verify no provider, checkout, wallet, or form control is shown as enabled
  when its required runtime config is missing
- verify browser refresh, first load, and no-JS/static fallback do not produce
  a misleading usable-looking but inert control

### Ecommerce and multi-block flows

Commerce extensions must treat the complete buyer path as the unit of
readiness, not each block in isolation. The minimum public flow is:

- product/listing block renders live catalog data or a declared fixture state
- add-to-cart works from every displayed product/card variant
- cart summary updates without a page reload when the runtime supports that
- quantity changes and removal work, including empty-cart state
- checkout handoff creates the expected checkout/order request
- payment/provider setup gates are visible before the buyer reaches a dead end
- order confirmation or failure state is styled and reachable
- account/login requirements are clear and use the same visual contract as the
  login extension blocks

If product cards, cart, checkout, account, and order confirmation are separate
blocks, test them together on the seeded public page. A block-level screenshot
is not evidence that the commerce extension is ready.

## Authored document rules

Allowed:

- serializable JSON props
- stable block/template ids
- media and link props as extension-defined conventions that the block
  component interprets itself
- strings, booleans, numbers, arrays, and objects that can be validated

Block props are passed opaquely to your component: the host attaches no
semantics to any prop shape or `kind` value, and there are no host-defined
media/link wire kinds. The host's media mechanism is the document-level media
reference set with slot ids — store a slot id (or another convention you
define) in your props and resolve it in your component.

Valid authored document example:

```json
{
  "type": "acme-demo:hero",
  "props": {
    "heading": "Welcome",
    "body": "Edit this copy before publishing.",
    "imageSlotId": "acme-demo:hero-image",
    "primaryLink": {
      "href": "/contact",
      "label": "Contact us"
    }
  }
}
```

`imageSlotId` and `primaryLink` here are extension-defined prop conventions,
not host fields; your component is the only thing that gives them meaning.

Not allowed:

- JSX or component source in document data
- raw HTML, script, style, or CSS text
- DOM structure payloads
- secret values or API credentials
- machine-local paths
- hidden runtime configuration

## Styling ownership model

The extension owns its blocks' presentation end to end. The host contributes
exactly three things and nothing else:

1. **Theme tokens** — a stable set of CSS custom properties in scope wherever a
   block renders (the table below).
2. **A presentation-neutral wrapper** — each block renders inside
   `<section class="cedros-runtime__block cedros-runtime__block--extension">`
   carrying four data attributes: `data-block-type`, `data-block-state`,
   `data-block-source`, and `data-block-availability` (there is no broader
   `data-block-*` family). For installed, ready extension blocks that wrapper
   has **no** card, padding, border, radius, or shadow of its own. Your
   component root controls the whole box.
3. **Delivery** — newer hosts inject your archive stylesheet on both the
   public site and the admin builder preview, but your compiled entry must
   also self-inject its CSS so it works on every host version (see *Delivery*
   below).

The presentation-neutral `--extension` wrapper applies to **any** installed,
ready extension block whose preview layout is `unsupported` (the default) —
the host classifies it as layout `unsupported`, `source.kind` `extension`, and
`availability` `installed`, with or without a component. A metadata-only block
(no component) therefore renders as an invisible empty wrapper — a placeholder
hydration target — until a renderer hydrates it, **not** as the default card.
A block mapped to a core layout (hero, prose, etc.) or one that is not yet
installed instead renders inside the host's default `.cedros-runtime__block`
card (padding `1.25rem`, `1px` border, `1.25rem` radius, panel background,
drop shadow). Design for the neutral wrapper, but do not assume it when your
block can render in those other states.

**Layout the host owns.** Your block renders inside a host zone
(`.cedros-runtime__zone`) within a width-constrained page container the host
controls. Style only your own box: avoid `100vw`, full-bleed page backgrounds,
or large negative insets that fight the host container — page width, page
background, and zone gap are host-owned. Express block width as
`min(100%, ...)` rather than a fixed value, because the host may apply
template-scoped layout to specific block types via `[data-template]` and
`[data-block-type]` selectors (for example cedros-login centers and
width-clamps its auth blocks). When two extension blocks stack in the same
zone the host removes the inter-block gap (a negative `margin-top` of one zone
gap on the second `--extension` block), so an extension that ships multiple
stacked blocks must provide its own vertical rhythm.

The host does **not** ship CSS for your block classes, and does not know your
DOM. If your block is unstyled, either your archive shipped no stylesheet or
your bundle relied on host archive-CSS delivery on a host that predates it —
it is never the host overriding you. Diagnose in the live document: check
whether your `<style>` element actually exists in `<head>`, not whether the
`.css` file exists in the zip.

## Theme-token contract

> **Scope:** these tokens govern visual-builder **blocks** (the public site
> and the admin builder preview), which render inside `.cedros-runtime`
> wrappers. To style an admin **page** or **dashboard card**, use the `--cd-*`
> admin tokens in
> [`12-admin-pages-and-dashboard-cards.md`](12-admin-pages-and-dashboard-cards.md);
> the `--cedros-runtime-*` tokens below are undefined on those surfaces.

Style colors and theme-sensitive values through these tokens. They are defined
with built-in neutral default values on `.cedros-runtime`; in the admin builder
preview a block keeps those defaults. On public pages the host overrides the
same tokens by rebinding them to the live site theme (`--cds-*`), so blocks
recolor and support dark mode automatically.

| Token | Meaning | Public binding |
| --- | --- | --- |
| `--cedros-runtime-bg` | Page/runtime background | `--cds-bg` |
| `--cedros-runtime-panel` | Raised surface / card fill | `--cds-panel` |
| `--cedros-runtime-border` | Hairline borders | `--cds-border` |
| `--cedros-runtime-text` | Primary text | `--cds-fg` |
| `--cedros-runtime-muted` | Secondary / muted text | `--cds-muted` |
| `--cedros-runtime-accent` | Accent / strong link | `--cds-link-strong` |
| `--cedros-runtime-accent-soft` | Soft accent / hover wash | `--cds-link-soft` |
| `--cedros-runtime-action-bg` | Primary action background | `--cds-primary` |
| `--cedros-runtime-action-fg` | Primary action foreground | `--cds-primary-fg` |
| `--cedros-runtime-warning` | Warning semantic color | not site-bound |
| `--cedros-runtime-error` | Error semantic color | not site-bound |

Unlike the rows above, `--cedros-runtime-warning` and `--cedros-runtime-error`
are not rebound to the site theme (`--cds-*`), so a site's theme manifest
cannot recolor them. They are not constant, however: their base values flip
between light and dark mode, so they still follow dark mode on every surface.

Guidance:

- **Prefer tokens over literals** for any color so theming flows through.
  Layout, spacing, type scale, and shape are yours to hardcode.
- **Add fallbacks** — `var(--cedros-runtime-text, #111)` — so a block degrades
  gracefully if it is ever rendered outside a themed runtime.
- **Dark mode is automatic** when you consume the tokens; do not hardcode a
  dark palette, and do not key dark styles off `prefers-color-scheme` (next
  section).

## Visitor color-scheme contract

The public site has a visitor-controlled color mode: **light**, **dark**, or
**system** (the default, which follows the OS). The site footer ships a toggle
that cycles the three states. Because the visitor can override the OS, the OS
preference is **not** a valid theming signal on public pages:

- **Never use `@media (prefers-color-scheme: dark)` in block CSS.** It only
  matches the site while the visitor is in system mode; a manual light/dark
  choice on the site will not reach your media queries and your block will
  visibly disagree with the page around it.
- The host resolves the mode and applies it as a **`cedros-dark` class on
  `<html>`, before first paint**. Tokens already follow it. If you need
  dark-only styling beyond the tokens, scope it on that ancestor class:

  ```css
  /* wrong: ignores the site's manual override */
  @media (prefers-color-scheme: dark) {
    .my-block { background: #18181b; }
  }

  /* right: follows the visitor's actual site mode */
  .cedros-dark .my-block { background: #18181b; }
  ```

For block **code** (not CSS), the host maintains this state:

| Seam | Value | Use for |
| --- | --- | --- |
| `<html class="cedros-dark">` | present iff dark is the resolved mode | CSS scoping, imperative reads |
| `<html data-cedros-color-mode="…">` | the preference: `light` \| `dark` \| `system` | UI that reflects the preference |
| `cedros:color-mode-change` CustomEvent on `window` | `detail: { preference, resolved }` | JS change notifications |
| `colorMode` render prop | `"light"` \| `"dark"` (system already resolved); `undefined` when unknown | React block renderers |
| localStorage `cedros-color-mode` | persisted preference; absent = system | **read-only** — never write it |

`contentBlockRenderers[].render` receives `colorMode`, and public hydration
re-invokes your renderer with the fresh value whenever the visitor toggles (or
the OS changes in system mode) — no subscription needed in React renderers.
`colorMode` is `undefined` during SSR and on hosts that predate the contract,
so treat mode-dependent markup as light-by-default and let your
`.cedros-dark`-scoped CSS own the first paint; that also avoids hydration
mismatches. Do not toggle the `cedros-dark` class or write the localStorage
key yourself — the site owns mode changes.

## Delivery

Ship your CSS two ways, and treat only the first as guaranteed:

1. **Self-injected from your compiled entry module** (required). Inline the
   stylesheet into the JS bundle (Vite `?inline`) and append an id-guarded
   `<style>` at module evaluation — see the pattern in
   [`12-admin-pages-and-dashboard-cards.md`](12-admin-pages-and-dashboard-cards.md).
   This works on every host version. A bare `import "./auth-blocks.css"` is
   **not** sufficient: Vite library mode extracts it into a separate `.css`
   asset and strips the import, so the compiled entry carries no injection.
2. **As `kind: "style"` files inside the runtime archive**
   ([`22-packaging-and-upload.md`](22-packaging-and-upload.md)), which newer
   hosts deliver per surface as described below.

Host archive-CSS delivery shipped in June 2026. A deployed host can predate it
while still reporting an up-to-date admin version — version strings do not
prove the capability — so a bundle that relies only on archive `.css` files
renders unstyled there. On hosts that have it:

- **Public pages** — critical-CSS inlining is conditional: it fires only on
  pages whose first render contains at least one installed extension block
  (the `data-cedros-public-extension-block` marker). A page with no extension
  blocks gets **no** extension CSS at all. When it fires, **all** installed
  extensions' archive stylesheets are inlined together in the page `<head>` —
  not just the ones used on that page — ahead of the blocking site stylesheet
  `<link>`. Keep the total lean (the host warns past 512 KiB of CSS string
  length and never truncates). The inlined critical `<style>` carries no dedup
  attribute, so your entry module's self-injected copy appears twice
  (harmless); the import-vs-inline dedup only applies in the admin document.
  The critical-CSS path is why you still ship the `.css` archive files even
  though the entry self-injects: it styles the first paint before any JS
  evaluates.
- **Admin builder preview** — the host injects archive stylesheets into the
  document, deduped against your entry module's self-injected copy only when
  its `data-cedros-extension-style` attribute value is exactly
  `<react packageName>::<archive css path>` (for example
  `@acme/acme-stats-react::packages/react/dist/styles.css`). Any other value —
  including an id-guarded `<style>` without that attribute — means both copies
  are injected (harmless duplication).

Public hydration loads the **web** entry and filters archive files to that
entry's `dist/` prefix, excluding `/dist/admin/**` and `/src/**` — web
chunks/CSS outside that prefix load in admin but fail on public pages.

## Shell/live visual parity

Many public blocks need a first-render shell because the real browser
component loads provider SDKs, wallet adapters, payment SDKs, search clients,
or other browser-only code. That pattern is supported, but the shell and live
component must use one visual contract.

Required:

- Use the same root block class or an explicitly equivalent root class for the
  shell and the live component.
- Keep the same visible layout, spacing, border/radius, button sizes, text
  hierarchy, icons, provider order, and focus order.
- Keep the same theme-token CSS. The live component must not fall back to a
  package default stylesheet that ignores `--cedros-runtime-*` tokens.
- Style loading, disabled, empty, setup-required, error, success, provider
  unavailable, and no-wallet/no-payment-method states.
- Include browser-running screenshots or visual fixtures for the final working
  component, not only the first static shell.
- Test at mobile and desktop widths after the component starts running in the
  public page.

Not acceptable:

- Styling `AuthFormShell` while the real `LoginForm` keeps unrelated standalone
  styles.
- Styling a product card placeholder while the live product card changes grid
  sizing, image aspect ratio, price typography, or button treatment.
- Styling an empty cart shell while the live cart line items use a different
  table/card system.
- Styling checkout setup copy while the live checkout button or provider
  handoff appears as a generic unthemed button.

If the live component intentionally has a different appearance from the shell,
record that as a product/design change and get explicit approval before
release. The default expectation is no visible jump when browser code starts.

## Interactive state styling

Interactive public blocks must style every state the user can reach through
the live flow:

| State | Required styling |
| --- | --- |
| Loading/provider boot | Keeps layout stable; controls indicate waiting without shifting size. |
| Disabled/setup missing | Clearly disabled or hidden; never looks clickable when required config is absent. |
| Validation error | Inline, scoped to the field/control, accessible to screen readers. |
| Provider error | Names the provider/action without exposing secrets or raw tokens. |
| Success | Shows the confirmed next state: signed in, item added, checkout created, order placed, etc. |
| Empty | Uses the same block skin and does not collapse the page layout. |
| Retry/recovery | Provides a clear retry/back path where the flow supports it. |

For provider-backed blocks, a button may only look enabled when the required
runtime configuration is present. Examples: Google login requires a client id,
Apple login requires its client id/config, Solana requires a wallet/provider
path, checkout requires payment setup, and add-to-cart requires a valid
product and cart runtime.

Official Wallet and Compliance blocks add domain-specific state rules
(fail-closed rendering, neutral compliance states, strict never-log lists) on
top of these generic states — see
[`contracts/wallet-compliance-seams.md`](contracts/wallet-compliance-seams.md).

## Scoping and hygiene

- **Scope every selector** under your block roots (e.g. `.cedros-auth-form`).
  The host injects your CSS globally; an unscoped element selector or reset
  will leak into the entire site and the admin.
- **Keep specificity low.** Wrapping selectors in `:where(...)` (zero
  specificity) lets site authors override individual instances.
- **No `@import` of remote URLs** and no `</style>`-bearing content; the host
  escapes the closing tag but remote imports defeat the critical-CSS
  guarantee.

## Native rendering and fallback

A custom block can render on mobile, or fall back when it is web-only:

- **Render on native (self-serve).** Add a `contentBlockRenderers` entry (a
  `CedrosExtensionBlockRenderer` for the block `type`) to the extension's
  react-native bootstrap. The host app loads it with
  `loadCedrosNativeExtensions` and passes
  `createCedrosNativeExtensionBlockRenderer(installedExtensions)` as the
  `renderBlock` prop, so the block renders on mobile instead of the
  unsupported-block placeholder. Native screens, stacks, and providers ship in
  the same executable package and are composed by hosted App Builder releases;
  the app selects its primary stack in Mobile metadata. See
  [`21-host-coordination-and-native.md`](21-host-coordination-and-native.md).)
- If a block or template is web-only: document native fallback text or
  omitted-state behavior.
- Keep authored props platform-neutral; avoid browser-only imports in shared
  code.

## Theme, media, link, and form guidance

- Use Cedros theme tokens and runtime components where the SDK exposes them.
- Do not hard-code colors or spacing that should come from the host theme.
- Reference host media through the document-level media reference set (slot
  ids), not remote credentials.
- Validate external links and provide safe empty states.
- Form integrations should reference declared form templates or host form
  helpers, not hidden form-posting code.

Cross-surface fixtures: web render fixture, admin builder preview fixture,
markdown/export fallback fixture, native unsupported/fallback fixture.

## Reference skin

[`examples/cedros-login-auth-blocks.reference.css`](examples/cedros-login-auth-blocks.reference.css)
is a complete, token-driven block skin (the cedros-login `auth-page-copy` and
`auth-form` blocks). It demonstrates the ownership, token, and scoping rules —
base, hover, focus, disabled, loading, status, success, and inline error
states scoped with `:where(...)`. Treat it as a styling baseline, not a
component API: your block still needs real accessible labels, live provider
wiring, and extension-specific error copy.

## Rules

- Keep block props platform-neutral; keep unknown props harmless.
- Do not add raw script/style/html escape hatches.
- Keep ids stable and namespaced.
- Store copy/content as authored document data, not hidden executable
  payloads.
- Do not duplicate manifest constants by hand across packages without a
  generation or comparison step.
- Do not ship separate "pretty static" and "working live" visual systems for
  the same public block.
- Do not mark a public interactive block complete until the browser-running
  component performs the advertised action.

## Acceptance checks

- Manifest metadata and runtime definitions name the same block/template ids.
- Templates render with core and extension blocks.
- Starter documents contain only portable authored-document props.
- Missing runtime definitions fail visibly during authoring, not silently.
- Public block `Component`/renderer exists for every declared public block
  type that appears in a page seed.
- Static shell and browser-running component match visually and semantically.
- Core user actions work on the actual public route and produce the expected
  network/provider calls.
- Provider/config-dependent controls hide or disable correctly when required
  runtime config is absent.
- The built public chunk contains no `react-dom` import (direct or
  transitive).
- No raw script/style/html escape hatches are introduced.
- Native fallback behavior is documented for web-only definitions.
- Accessibility and media/link smoke fixtures exist for important blocks.

Next: [`15-content-starters-and-onboarding.md`](15-content-starters-and-onboarding.md).
