Y `content-section.Media` pasa a seguir al texto por defecto.
DEFAULT: `.Media` era `width='wide'`, asi que una figura se salia de la columna
SALVO que dijeras lo contrario. Al reves de lo que hacen todas las referencias
(en `@tailwindcss/typography` una imagen dentro de `prose` queda capada a la
medida y salirse exige el truco del `translate`; en `markdown-body` de GitHub,
`max-width: 100%` de la columna). Ahora el default es `measure` — la figura
sigue al texto y el escape se PIDE. Cambiado en los cuatro sitios que decian lo
contrario: la parte, la ruta `preview`, el estado inicial del control de la demo
(una demo que pisa el default en silencio ensena algo que el block no hace solo)
y la doc. Verificado: 528 = ancho del texto exacto a 1011/1280/1920, y `wide` /
`full` siguen funcionando cuando se piden.
DOCUMENTACION: los 15 READMEs declaran ahora su posicion bajo §«Coordination»,
y el mapa real no es uniforme —
- Coordinan: `contact` (maquina + esquema + palabras) y `pricing` (el periodo
por contexto). Pero en pricing nada se puede BLOQUEAR, asi que no necesita
maquina ni palabras: coordinar no es siempre una maquina.
- Comparten CONFIGURACION, no estado: `team` (`align`) y `content-section` (la
medida).
- Costura bindable hacia el canon: `faq` (`value` → `Accordion`) y
`site-header` (`mobileOpen` → `Drawer`). Reenviar no es poseer.
- No poseen nada: banner · cta · feature-grid · feature-split · hero ·
site-footer · stats-band · testimonials. Decirlo explicitamente es el punto:
inventarle estado a un block que no coordina nada es el error contrario.
- `newsletter` queda como CANDIDATO ABIERTO: coloca un envio que puede quedar
bloqueado sin que nada diga por que — el `disabled` lo monta el app en el
punto de uso — que es exactamente el hueco que cerro `contact`. Es anterior a
la doctrina (block del 30-07, doctrina del 31-07). Registrado, no asumido: se
toca con decision del usuario.
El README del tier apunta a la doctrina y el handoff recoge el mapa completo.
Gates: blocks:check verde (15) · docs:check 0 · svelte-check sin errores en lo
tocado · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| entrance | `Motion` (`trigger="viewport"`, `scale-fade`) | the panel arrives WHOLE — it is one statement, not a list, so it does not stagger its parts |
| panel | `Surface` (always `solid` · `color` · `gradient` · `rounded`) | the look is the system's finish, not paint of the block's own |
| layout | `Stack` (center) · `Grid` 2-col (justified) | `justified` stacks on narrow, where one row would crush both halves |
| `eyebrow` | — (app: `Badge` / text) | above the title |
| `title` | `Heading` at `level` (default 2) | the block wraps the app's words and owns the landmark `id`; on-solid ink |
| `description` | `Text``as="p"` (`60ch` measure) | on-solid ink. A `span` would ignore `align` — `text-align` is inert on an inline box |
| `actions` | `Flex` (column → row at `sm`) | the app drops `Button`s / `Link`s |
| entrance | `Motion` (`trigger="viewport"`, `scale-fade`) | the panel arrives WHOLE — it is one statement, not a list, so it does not stagger its parts |
| panel | `Surface` (always `solid` · `color` · `gradient` · `rounded`) | the look is the system's finish, not paint of the block's own |
| layout | `Stack` (center) · `Grid` 2-col (justified) | `justified` stacks on narrow, where one row would crush both halves |
| `eyebrow`| — (app: `Badge` / text) | above the title |
| `title`| `Heading` at `level` (default 2) | the block wraps the app's words and owns the landmark `id`; on-solid ink |
| `description`| `Text``as="p"` (`60ch` measure) | on-solid ink. A `span` would ignore `align` — `text-align` is inert on an inline box |
| `actions`| `Flex` (column → row at `sm`) | the app drops `Button`s / `Link`s |
## Form: snippet slots, not sub-components
The tier's rule is that a compound API is earned when parts **repeat**
(`feature-grid.Item`) or **coordinate** (`pricing.Switch` ↔ `PlanPrice`). A CTA's
parts do neither: they are fixed positional slots the root arranges. So it takes
the same shape as `hero`. *(The plan sketched `.Title`/`.Description`/`.Actions`;
this is the same registered deviation as `hero`.)*
the same shape as `hero`. _(The plan sketched `.Title`/`.Description`/`.Actions`;
this is the same registered deviation as `hero`.)_
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** A closing panel places copy and actions; there is no state, no
data shape and no word of its own. Layout blocks do not coordinate, and giving
- **Adopted**: the centred panel (parity) **and the `justified` arrangement** —
copy at the inline-start, actions at the end — which the dossier names as the
gap to close.
- **The finish is the system's**: a CTA is distinguished by *treatment*, and
- **The finish is the system's**: a CTA is distinguished by _treatment_, and
treatment is already framework vocabulary. `Surface` with `gradient` on by
default (the one section that earns it), so a theme or palette swap carries the
panel along. The block paints nothing of its own.
@ -67,14 +75,14 @@ layout, colour, finish and direction, and the device widths served from
## Gaps
| Gap | Disposition |
| --- | --- |
| **Split with media** (panel with a screenshot beside the copy) | **deferred** — recurs in the refs; it is a third arrangement, and `Mockup` already exists for the media. Enters when a demo asks |
| **Full-bleed panel** (edge to edge, no container) | **app-land** — the app sets `container="full"` |
| **Dismissible / sticky CTA** | **out** — that is the `banner` block's job (F2.11), not this one |
| **A quiet BOUNDED panel** | **canon** — `Surface soft` does not bound (0.002 L from the page in light) and has no border; `Card outline` bounds but takes no gradient finish. No primitive covers "quiet CTA panel with an edge" |
| **`contrast` slot is white on every solid step** | **canon** — so a mid-L solid canvas (`neutral` 3.32 · `secondary` 3.30 · `slate` 3.30 · `teal` 3.07 in light) puts body copy below AA. The pairing guarantee holds for the dark canvases only. Measured with a luminance probe, not eyeballed |
| **On-solid chip treatment** | **canon** — `Badge` has no on-solid variant; the demo uses `variant="soft"`, whose near-white track happens to read against a saturated canvas. A `Badge` that knows it sits on a solid panel would be the real answer |
| **Split with media** (panel with a screenshot beside the copy) | **deferred** — recurs in the refs; it is a third arrangement, and `Mockup` already exists for the media. Enters when a demo asks |
| **Full-bleed panel** (edge to edge, no container) | **app-land** — the app sets `container="full"` |
| **Dismissible / sticky CTA**| **out** — that is the `banner` block's job (F2.11), not this one |
| **A quiet BOUNDED panel**| **canon** — `Surface soft` does not bound (0.002 L from the page in light) and has no border; `Card outline` bounds but takes no gradient finish. No primitive covers "quiet CTA panel with an edge" |
| **`contrast` slot is white on every solid step** | **canon** — so a mid-L solid canvas (`neutral` 3.32 · `secondary` 3.30 · `slate` 3.30 · `teal` 3.07 in light) puts body copy below AA. The pairing guarantee holds for the dark canvases only. Measured with a luminance probe, not eyeballed |
| **On-solid chip treatment**| **canon** — `Badge` has no on-solid variant; the demo uses `variant="soft"`, whose near-white track happens to read against a saturated canvas. A `Badge` that knows it sits on a solid panel would be the real answer |
## Found while composing
@ -85,6 +93,6 @@ layout, colour, finish and direction, and the device widths served from
until the consumer changes the element. Canon candidate.
- **`Surface soft` cannot bound a panel** — measured above; see Gaps.
- **`Group` does not stack.** The actions row needed `Flex direction={{ base:
'column', sm: 'row' }}`: in a `Group` at 420px the secondary action's label
'column', sm: 'row' }}`: in a `Group` at 420px the secondary action's label
breaks mid-phrase against the primary button. `hero` composes its actions with
`Group` too, so it has the same narrow behaviour — noted for its next pass.
| **Static 2/3-column list** (questions + answers laid out, no disclosure) | **scope-approval pending** — the dossier's FAQ gap: 6 of 7 in TW are NOT accordions. It is a different arrangement (a grid of Q&A, always open), so likely a `layout` prop or a sibling — a user decision, not a silent add |
| **"Still have questions?" tail** (a CTA to support below the list) | **app-land** — the app puts a `Text` + `Link`/`Button` after `.List`; a dedicated slot is only worth it if a demo asks |
| **"Still have questions?" tail** (a CTA to support below the list) | **app-land** — the app puts a `Text` + `Link`/`Button` after `.List`; a dedicated slot is only worth it if a demo asks |
| root | bare `<section>` + `Section` + `Container` + `Stack` | places the header and the grid |
| `.Header` | `Box` (measure-cap) + `Stack` | the app supplies the section `Heading` (h2) + `Text` |
| `.Items` | `AutoGrid` + `data-stagger` | fluid (`minChildWidth`, default `16rem`) or fixed `columns`; the stagger rhythm is structural |
| `.Item` | `Motion` (`trigger="viewport"`) wrapping a `Stack` | IS the reveal, so it must stay the grid's direct child to get its structural index; `align="start"` (default) or `center` |
| `.ItemIcon` | `Surface` (solid, accent) | the coloured chip — solid so it reads in light mode, where the soft tint is near-white; the app drops an `<Icon.X>` inside |
| `.ItemTitle` | `Heading` (level 3) | the feature name |
| `.ItemText` | `Text` (muted) | the feature description |
| root | bare `<section>` + `Section` + `Container` + `Stack` | places the header and the grid |
| `.Header`| `Box` (measure-cap) + `Stack`| the app supplies the section `Heading` (h2) + `Text` |
| `.Items`| `AutoGrid` + `data-stagger` | fluid (`minChildWidth`, default `16rem`) or fixed `columns`; the stagger rhythm is structural |
| `.Item`| `Motion` (`trigger="viewport"`) wrapping a `Stack` | IS the reveal, so it must stay the grid's direct child to get its structural index; `align="start"` (default) or `center` |
| `.ItemIcon`| `Surface` (solid, accent) | the coloured chip — solid so it reads in light mode, where the soft tint is near-white; the app drops an `<Icon.X>` inside |
| `.ItemTitle` | `Heading` (level 3) | the feature name |
| `.ItemText`| `Text` (muted) | the feature description |
## Why compound (and not snippet slots)
@ -31,6 +31,14 @@ parent's, the cells are the app's.
`.Items` is an explicit grid wrapper (like a list's `List`): it lets the header
sit outside the grid without a full-row `grid-column: 1 / -1` span hack.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** The items repeat but do not talk to each other — no context, no
state, no words. That is exactly why they are a compound without a context, and
why the rule is applied part by part, not block by block.
| **Feature-split / alternating** (a text block beside a screenshot, sides alternating) | **scope-approval pending** — the dossier's **#1 F2 gap**: "lo shippean las 5 refs y es la sección nº1 tras el hero". It is a different arrangement (alternating 2-column rows, not an icon grid), so likely its own block (`feature-split`) or a sibling — a user decision, not a silent add here |
| **Card variant** (each item boxed) | **deferred** — one prop away (wrap the `Item` body in a `Card`); built when a demo asks |
| **Stat / numbered variants** | **deferred** — the number replaces the icon chip; app-composable today |
| **Card variant** (each item boxed) | **deferred** — one prop away (wrap the `Item` body in a `Card`); built when a demo asks |
| **Stat / numbered variants**| **deferred** — the number replaces the icon chip; app-composable today |
@ -56,10 +64,10 @@ Mini-page in `FeatureSplitSite.svelte`, shared by both surfaces.
## Gaps
| Gap | Disposition |
| --- | --- |
| Feature-split with a **background band** per row (tinted section behind alternating rows) | **deferred** — the app wraps a `Row` in a `Surface`; a prop is sugar |
| **Sticky media** (the screenshot pins while the copy scrolls) | **deferred** — behaviour, would compose `Sticky`; built when a demo asks |
| Feature-split with a **background band** per row (tinted section behind alternating rows) | **deferred** — the app wraps a `Row` in a `Surface`; a prop is sugar |
| **Sticky media** (the screenshot pins while the copy scrolls) | **deferred** — behaviour, would compose `Sticky`; built when a demo asks |
| layout | `Grid` (split) · `Stack` (center) · positioned `Box` layers (background) | `split` is two columns only when there is `media` |
| entrance | `Motion` (`trigger="viewport"`) + `data-stagger` on the copy stack | the copy unfolds by structural index; media enters with `scale-fade` after it |
| `eyebrow` | — (app: `Badge` / text) | placed above the title |
| `title` | `Display` (`as={h{level}}`) | the hero typography primitive; the block wraps the app's words and owns the `id`; one size step down in `split`; on-solid ink under `background` |
| layout | `Grid` (split) · `Stack` (center) · positioned `Box` layers (background) | `split` is two columns only when there is `media` |
| entrance | `Motion` (`trigger="viewport"`) + `data-stagger` on the copy stack | the copy unfolds by structural index; media enters with `scale-fade` after it |
| `eyebrow`| — (app: `Badge` / text) | placed above the title |
| `title`| `Display` (`as={h{level}}`) | the hero typography primitive; the block wraps the app's words and owns the `id`; one size step down in `split`; on-solid ink under `background` |
| **Background-image / video cover** | **SHIPPED 2026-07-23** — the `background` layout (user scope-approval). Media full-bleed behind the copy, block-owned scrim + on-solid ink + cover-fit |
| **In-column media frames** (framed screenshot chrome · phone mockup) | **DONE 2026-07-24** — shipped as the canon `Mockup` (`chrome="browser" | "phone" | "plain"`), composed by the app into the `media` slot. The block still names no chrome of its own |
| **`form-in-hero`** (email sign-up in the hero) | **scope-approval pending** — recurs across refs; would be a `form` slot composing `Form` + `Field`. Deferred from v1 |
| **`logo-strip`** under the actions | **app-land for now** — the app can drop a row of logos into `children` (renders under the actions); a dedicated slot is only worth it if a demo asks |
| **Background-image / video cover**| **SHIPPED 2026-07-23** — the `background` layout (user scope-approval). Media full-bleed behind the copy, block-owned scrim + on-solid ink + cover-fit |
| **In-column media frames** (framed screenshot chrome · phone mockup) | **DONE 2026-07-24** — shipped as the canon `Mockup` (`chrome="browser" | "phone" | "plain"`), composed by the app into the `media` slot. The block still names no chrome of its own |
| **`form-in-hero`** (email sign-up in the hero) | **scope-approval pending** — recurs across refs; would be a `form` slot composing `Form` + `Field`. Deferred from v1 |
| **`logo-strip`** under the actions | **app-land for now** — the app can drop a row of logos into `children` (renders under the actions); a dedicated slot is only worth it if a demo asks |
| `.Switch`| `ToggleGroup`| writes the period; reads it back to stay in sync |
| `.Plans`| `AutoGrid` + `data-stagger`| fluid row of equal-height plan cards; the stagger rhythm is structural |
| `.Plan`| `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal (direct child of the staggered row); `featured` → accent colour; `badge` chip |
@ -54,12 +66,12 @@ the middle), the live monthly/annual toggle switching every price. Mini-page in
## Gaps
| Gap | Disposition |
| --- | --- |
| **Feature-comparison table** (features × plans matrix) | **scope-approval pending** — the dossier's recurring pricing gap. It is a table, not a row of cards, so likely a sibling (`pricing-table`) or a `.Compare` sub-block — a user decision, not a silent add |
| **Single-price variant** (one plan, centered) | **deferred** — one `.Plan` in `.Plans`; the layout already handles it |
| **Per-period savings note** ("2 meses gratis") | **app-land** — the app puts it in the `annualLabel` or a `.PlanDescription` |
| **Elevation on the featured plan** | **canon candidate** — `Card` exposes no elevation prop, and a block paints nothing of its own. Marking the recommended tier by depth (not just colour) needs the prop in `Card`; until then `featured` is colour only |
| **Feature-comparison table** (features × plans matrix) | **scope-approval pending** — the dossier's recurring pricing gap. It is a table, not a row of cards, so likely a sibling (`pricing-table`) or a `.Compare` sub-block — a user decision, not a silent add |
| **Single-price variant** (one plan, centered) | **deferred** — one `.Plan` in `.Plans`; the layout already handles it |
| **Per-period savings note** ("2 meses gratis") | **app-land** — the app puts it in the `annualLabel` or a `.PlanDescription` |
| **Elevation on the featured plan**| **canon candidate** — `Card` exposes no elevation prop, and a block paints nothing of its own. Marking the recommended tier by depth (not just colour) needs the prop in `Card`; until then `featured` is colour only |
| Scroll-hide (header retreats when scrolling down) | **canon candidate** — it is behaviour with contract surface (direction detection), so it goes to `sticky` first, never into this block (admission rule) |
| Skip-link | **shells own it** — it belongs to the page shell (`app-shell` / `docs-shell`, F3/F4), which knows what the main region is; putting it here would emit a second one on every shell page |
| Search field in the bar | **app-land** — it is a `Field`/`Command` the app composes into `actions`; the block does not need to know |
| Sub-navigation row under the bar | **out** — `children` renders under the bar; a second row of links is markup, not new API |
| Scroll-hide (header retreats when scrolling down) | **canon candidate** — it is behaviour with contract surface (direction detection), so it goes to `sticky` first, never into this block (admission rule) |
| Skip-link | **shells own it** — it belongs to the page shell (`app-shell` / `docs-shell`, F3/F4), which knows what the main region is; putting it here would emit a second one on every shell page |
| Search field in the bar | **app-land** — it is a `Field`/`Command` the app composes into `actions`; the block does not need to know |
| Sub-navigation row under the bar | **out** — `children` renders under the bar; a second row of links is markup, not new API |
| A NAVIGATING call-to-action that looks like a button | **CLOSED in the canon 2026-07-23** — surfaced while composing this block. It is served by composition, not by a new prop: `Button`'s `child` (asChild) form now hands over a `content` snippet, so `<a href {...props}>{@render content()}</a>` gets the full solid paint AND keeps the icon / label / spinner slots, and soma stops stamping `type` on an element it does not own. `Button` still has no `href` and `Link` still owns navigation — both signed decisions hold. See the eidos Button README §«CTA que navega» |
@ -8,12 +8,12 @@ the one section where the number itself is the content.
## Composition map
| Part | Composes | Notes |
| --- | --- | --- |
| root | bare `<section>` + `Section` + `Container` + `AutoGrid` + `data-stagger` | fluid by `minChildWidth` (default `12rem`) or fixed `columns`; the stagger rhythm is structural |
| `.Stat` | `Motion` (`trigger="viewport"`) wrapping `Metrics` | IS the reveal, so it must stay the band's direct child to get its index; `align="center"` by default (a band is centred figures, not dashboard tiles) |
| `.Value` | `Metrics.Value` (+ `CountUp` when `count` is set) | the figure |
| `.Label` | `Metrics.Label` | what the figure counts |
| root | bare `<section>` + `Section` + `Container` + `AutoGrid` + `data-stagger` | fluid by `minChildWidth` (default `12rem`) or fixed `columns`; the stagger rhythm is structural |
| `.Stat`| `Motion` (`trigger="viewport"`) wrapping `Metrics` | IS the reveal, so it must stay the band's direct child to get its index; `align="center"` by default (a band is centred figures, not dashboard tiles) |
| `.Value` | `Metrics.Value` (+ `CountUp` when `count` is set) | the figure |
| `.Label` | `Metrics.Label`| what the figure counts |
The KPI semantics (label / value / delta) are the canon `Metrics`'s — the block
adds none of its own. `Metrics` is surfaceless, which is exactly right here: a
@ -37,6 +37,14 @@ Omit `count` and the app renders the value itself through `children` (already
formatted, or composed with `FormatNumber`). **The block never formats a number
and never ships a word** (B-7).
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** The stats repeat and are independent, and the numbers are the
app's (`FormatNumber` / `CountUp` in the snippets). Nothing to derive, nothing to
| **Split-with-image** (the band beside a photo) | **deferred** — a different arrangement (2 columns), recurring in the refs; enters as a `layout` or a sibling when a demo asks |
| **Timeline / stepped** variant | **deferred** — the canon `Timeline` exists; it is a different section, not a band |
| **Delta / trend per figure** | **app-land** — `Metrics.Delta` composes into `.Stat` today; the block re-exposing it would only add surface |
| **Timeline / stepped** variant | **deferred** — the canon `Timeline` exists; it is a different section, not a band |
| **Delta / trend per figure**| **app-land** — `Metrics.Delta` composes into `.Stat` today; the block re-exposing it would only add surface |
| `.Items` | `AutoGrid` + `data-stagger` | fluid row of equal-height quote cards; the stagger rhythm is structural |
| `.Item` | `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal, so it must stay the grid's direct child to get its structural index; fills the row height |
| `.Quote` | `Text` (lg) | the quotation |
| `.Author` | `Group` + `Stack` | the `avatar` slot + name/role, pinned to the card's bottom |
| `.AuthorName` | `Text` (medium, sm) | |
| `.AuthorRole` | `Text` (muted, sm) | role / company |
| `.Items`| `AutoGrid` + `data-stagger`| fluid row of equal-height quote cards; the stagger rhythm is structural |
| `.Item`| `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal, so it must stay the grid's direct child to get its structural index; fills the row height |
| `.Quote`| `Text` (lg) | the quotation |
| `.Author`| `Group` + `Stack` | the `avatar` slot + name/role, pinned to the card's bottom |
| `.AuthorName` | `Text` (medium, sm) | |
| `.AuthorRole` | `Text` (muted, sm) | role / company |
## Why compound
@ -27,6 +27,13 @@ there is no context; the grid is the parent's, the cards the app's. `.Author`
pins to the card's bottom (`margin-block-start: auto`) so a row of uneven quotes
still lines its faces up.
## Coordination
_Position under the 2026-07-31 doctrine ([`architecture/blocks.md`](../../../../docs/architecture/blocks.md) §«Coordination»)._
**Owns nothing.** The items repeat without talking to each other — no context, no
state, no words. The face is the app's, in the `avatar` slot.
| **Single quote in spotlight** (one large quote, logo + avatar + author, centered) | **scope-approval pending** — the dossier's testimonials gap: it is the modal variant across the references, and the grid is the minority (2 of 8 in TW). It is a different arrangement (one big quote, not a grid), so likely a sibling (`testimonial-spotlight`) or a `layout` — a user decision, not a silent add |
| **Logo strip** (customer logos instead of quotes) | **out** — that is the `logo-cloud` category (deferred at F2 scope), not this block |
| **Carousel variant** (quotes in a scroller) | **deferred** — the canon `Carousel` exists; it enters when a demo asks |
| **Logo strip** (customer logos instead of quotes) | **out** — that is the `logo-cloud` category (deferred at F2 scope), not this block |
| **Carousel variant** (quotes in a scroller) | **deferred** — the canon `Carousel` exists; it enters when a demo asks |