v0.19.0

Released June 9, 2026 · All releases

Polish, composability & rollups. Where v0.18.0 hardened the rune output contract, v0.19.0 builds on that stable base along several threads: visual polish of the Lumina theme, making rune composability a supported and validated surface (), turning bento into a proper composition substrate (), and improving the cross-page aggregation runes that the plan plugin leans on.

Changes

  • Chart theming contract (SPEC-083 / WORK-353): the rf-chart SVG renderer no longer hardcodes its palette or geometry. Every paint + geometry value is now an --rf-chart-* custom property Lumina ships on .rf-chart — a dedicated categorical series palette (distinct from the semantic status tokens), bar/point/ line geometry, and typography/grid. The renderer emits only tagged elements (.rf-chart__bar[data-series], __point, __line, __axis, __label) that chart.css paints from the props, and reads layout geometry via getComputedStyle — so a theme retones a chart by setting --rf-chart-* alone, and a future canvas/ d3 provider reads the same vocabulary. Adds a sentiment colouring mode: data cells carrying data-meta-sentiment colour by the semantic token (positive→success, negative→danger, caution→warning, neutral→muted).
  • Also fixes aggregate layout="chart" to emit the chart rune's field channel, so a non-bar chart-type survives the identity transform and the .rf-chart class isn't doubled.
  • Per-group sentiment projection in aggregate (SPEC-076 / WORK-357). aggregate now projects $item.sentiment onto the per-group template (and tags chart data cells with data-meta-sentiment), looked up from a `(type → field → value →
  • Add a pair of opt-in text-zone knobs to bento, settable as a grid-level default or a per-cell override (cell wins; the grid default is the only lever for heading-sugar grids):
  • content-height (sm | md | lg → 3 / 5 / 7rem) — pins the text area on column cells (top/bottom media) so they line up vertically; the media zone absorbs the remaining row-track height.
  • media-ratio (1/3 | 2/5 | 1/2 | 3/5 | 2/3) — pins the media zone's share of the width on beside cells (start/end media); the content absorbs the rest.
  • The two act on perpendicular axes (a cell is either a column cell or a beside cell), so they never collide. Both default to the existing behavior (natural text height / 42% media split) and revert to natural height on the mobile stack.
  • Add a row-height attribute to the bento rune (sm | md | lg | xl) for control over the uniform grid row-track height in grid mode (8 / 12 / 16 / 20rem; md matches the previous default). Falls back to the theme's --rf-bento-row-height when unset, and is overridden by the stack's auto rows on mobile.
  • Bento substrate (SPEC-085) — v0.19.0 batch C.
  • Bento is a grid primitive, not a page-section. Dropped the eyebrow/title/blurb preamble; every heading is now a cell. A titled bento is a composition (wrap it in feature/section). Content before the first heading renders as loose content above the grid.
  • Cell adopts card's zone contract. A bento-cell's content splits on a top-level --- into media / body / footer zones (data-section), mirroring card. The media zone is clipped/sized by the name-agnostic WORK-339 selector (no bento-specific per-guest CSS) so a showcase bleed peeks. The cell background is tint-deferrable, and the leading heading becomes a uniform-level <h3> title contributing to the outline.
  • Proportional sizing model. A 6-column default for both authoring modes; size presets resolve as fractions of the column count (small ⅓, medium ½, large ⅔ × 2 rows, full = all), and cols / rows give precise per-axis spans that override the preset. Uniform fixed row tracks (grid-auto-rows: var(--rf-bento-row-height), never column-tied). Author-controlled collapse="sm|md|lg|never" plus automatic progressive column reduction with min(span, current-columns) auto-capping.
  • Size-derived media placement + link tiles. media-position (top|bottom|start|end) is author-controllable per cell with a size-derived default (large/full → beside, smaller → on top); an optional href makes a whole cell a link.
  • Explicit {% bento-cell %} authoring. A bento whose children include bento-cell tags uses them directly — full per-tile control (the dashboard case) — short-circuiting heading conversion (explicit wins, no mixing). The legacy span attribute is removed (subsumed by cols). cols / rows author as unquoted numbers (cols=4 rows=2), matching columns.
  • Rewrote the bento rune reference docs for the new substrate.
  • Even column ladder + landscape 2-up. Reduction now steps 6 → 4 → 2 → 1, skipping the odd 3-col step where a small cell can't pair. On tablets (≤1024) medium cells drop to a half so two pair per row. At ≤768 the grid is a 2-up auto-row stack: small cells take half a row (two pair up), medium/large/full span full width, and media reflows to an aspect-ratio banner so wide cells no longer crop their image to a thin strip.
  • Collapse is a stack, not a shrunken grid. At a single column the fixed row track is dropped (grid-auto-rows: auto) so cells size to their content and text is never clipped; media reflows to an aspect-ratio banner (--bento-media-aspect, default 16/9) on top. Cells are text-first in grid mode too — the body keeps its height and the media zone absorbs the leftover track and crops.
  • Composability foundation (SPEC-084) — v0.19.0 batch B.
  • Media-zone guest adaptation. Replaced Lumina's name-enumerated media allow-list with one name-agnostic rule: any visual rune dropped into a [data-section="media"] slot is sized, clipped, rounded, and given a container-query context (so intrinsically responsive guests like mockup auto-scale). Guests that manage their own bleed (preview, juxtapose, a bleeding showcase) self-declare an opt-out. Covers card/feature/hero/recipe with one rule.
  • requiresParent nesting validation. New self-declared RuneConfig.requiresParent (distinct from the advisory parent). The identity transform validates it at build time — a rune that opts in must have the named parent as its nearest ancestor rune, else it's reported: an error for structurally-meaningless children (accordion-item, tab, step, tier, map-pin, …), a warning otherwise. Opt-in, so standalone-capable runes (track) are never flagged. No container-side allow-list.
  • Context-modifier audit. Removed the one nonsensical pairing (Hero → in-feature); every remaining context modifier has CSS coverage. refrakt inspect --audit reports context-modifier coverage, and inspect now surfaces a rune's requiresParent.
  • Docs. A new composability authoring guide documents the open-world contract.
  • Lumina polish: token hygiene, dark parity, theme-aware chrome, and a11y (v0.19.0 batch A).
  • Token hygiene. Reconciled seven phantom colour tokens that were painting stale literal fallbacks (the out-of-place blue and cold-gray muted text): text-muted→muted, heading→text, accent→primary, background→bg, border-light→border, warning-fg→warning. Added a derived --rf-color-primary-bg (tracks primary in both modes) and a mode-flipped --rf-color-on-primary (fixes invisible white text on the light dark-mode primary).
  • Breaking (theme authors): the misnamed --rf-color-primary-50…950 ramp and the PrimaryScale type are removed — it was a warm-neutral scale mislabelled "primary" with almost no consumers. Set --rf-color-primary directly; primary-bg derives from it.
  • Dark parity. Verified every absolute token has a dark override; the remaining four are intentionally shared (derived from mode-aware tokens), now annotated.
  • Theme-aware chrome. Branded ::selection, token-driven scrollbars, and color-scheme per mode so native chrome (including the main scrollbar) follows the theme.
  • Accessibility. A uniform :focus-visible ring across all interactive elements and a global prefers-reduced-motion reset.
  • Two-tier surface rounding + inset media on mobile.
  • New semantic radius tokens: --rf-radius-container (→ lg, 16px) for outer surfaces and --rf-radius-media (→ md, 10px) for media guests nested inside them. Cards, the card/inset surface groups, feature items and bento cells move to the container tier; the shared [data-section="media"] zone and figure / recipe images move to the media tier, so a guest never reads as more rounded than its container. SPEC-086's frame will override these per-instance.
  • Media in a data-media-position="top" container no longer becomes a full-bleed square banner when the layout collapses on mobile — it stays inset within the card padding and fully rounded, matching its desktop framed look.
  • Top media now bleeds out past the body padding to a small, consistent margin on its top/left/right edges (new --rf-media-margin, defaulting to --rf-spacing-sm), figure-style, so the image isn't over-inset in containers with generous padding (e.g. bento cells). The figure image drops its box-shadow to match the unshadowed media elsewhere.
  • Bento media/responsive fixes:
  • Container-query responsiveness — the bento grid is now a query container (container-type: inline-size) and its progressive-reduction/collapse rules use @container instead of @media, so it reduces columns and stacks based on its own width. Grids in doc previews, sidebars, or narrow tracks now break correctly instead of only at viewport breakpoints.
  • Unwrap paragraph-wrapped images — images in a bento cell's media (and body) zone are unwrapped from their <p>, so the media zone holds a bare <img> and layouts size it directly.
  • Neutralize the global media-zone block margin — [data-section="media"] no longer applies a var(--rf-spacing-sm) top/bottom margin that misaligned media in flex/beside layouts; media spacing now comes from each layout. Affects all media zones (card, recipe, realm, faction, split, bento).
  • Surface/media polish:
  • More generous vertical spacing (--rf-spacing-md) between a top media banner and the content below it, in card/recipe/bento and on mobile stacks.
  • A code-group nested in a media zone now rounds at the media radius tier so its border lines up with the zone's clip instead of the larger container radius.
  • Dark-mode code background was a near-neutral grey that clashed with the warm surface; it's been warmed to the surface hue (#1e1c19, also applied to the derived --rf-syntax-background).
  • A card with no href no longer shows a hover background — the hover is now gated on the presence of the stretched card link.
  • The semantic --rf-radius-container / --rf-radius-media / --rf-media-margin aliases moved from the generated token contract into the styles layer (they reference the scale rather than holding raw values).
  • Unify the media-zone corner radius with the card radius. Media slots ([data-section="media"], shared by card, recipe, bento cell, feature, hero, realm, faction) rounded at --rf-radius-lg while their containers round at --rf-radius-md, so images looked more rounded than the card holding them. The media zone now uses --rf-radius-md, and recipe's redundant lg image override is removed (figure was already md).
  • Decompose plan-progress into sugar over the aggregate rune (SPEC-076). It now composes one aggregate per entity type — a type heading ("Work", "Specs", …) above a progress bar labelled with that type's achieved status ("Done", "Accepted", …) plus a per-status badge row — resolved by the shared resolveAggregates. Mixing types under a single ratio was misleading (work done and bug fixed measure different things). Plan defaults are baked in (type="work,bug", achieved-status per type, group="status", milestone= scoping); the bespoke plan-side render path is removed. A bare {% plan-progress /%} scopes to work,bug; widen with type=/show=. (Per-status badge colour is deferred — see WORK-357.)
  • Surface containers (card, the card/inset surface groups, feature items, bento cells) now carry a subtle 1px border in addition to their surface fill, pairing with the inset media framing.
  • The dark neutral surface family was browner than the rest of the palette: the page background and inline-code background are warm-grays (R=G) but surface, its hover/active/raised steps, border, and code-bg carried a red lift (R>G ≈ 36° hue). That red lift is removed so the whole dark neutral ladder shares one warm-gray character (kept warm via a blue deficit, just no longer brown), and code-bg / --rf-syntax-background follow it.
  • Fix dark surface staying brown in a scoped scheme override (e.g. a preview rune toggled to dark, or a sandbox). tint.css re-declares the palette under [data-color-scheme="dark"] — a hand-kept third copy that the token coverage test didn't reach — and it still held the pre-de-brown surface/border/code values. Synced it to the neutralized palette, and added a coverage test that checks every --rf-* key tint.css's [data-color-scheme] overrides share with the canonical token CSS, so this copy can't silently drift again.
  • Add layout="chart" to the aggregate rune (SPEC-076 chart layout): grouped counts render as a chart — one bar/point per group — by emitting the chart rune's <rf-chart> + data-name="data" table pipeline (SVG with a no-JS table fallback). chart-type (bar default / line / area / pie) and chart-title are configurable; a value sub-filter adds a second series; the axis honors domain-aware group ordering; an empty query renders the empty fallback rather than a broken chart.
  • Modernize backlog to compose over the bar rune (SPEC-084 / WORK-342). Its default item is now a card whose top strip is a bar — the identifier on the left, a sentiment-coloured status badge on the right, title below — built from a universal projection that works for every plan type. New layout attribute (cards default · list · table) is forwarded to collection. A type chip appears only for a mixed set; a single-type backlog also surfaces that type's key field (work→priority, bug→severity). The $item projection gains identifier (id || name, so milestones slot in), sentiment, and mixed, shared by every collection/aggregate rollup.
  • Expose group metadata on $item in a grouped collection (SPEC-070 / WORK-344): the per-item template can now read $item.group (its group key) and $item.groupCount (the group's size), so a grouped collection can render group context inline without dropping to aggregate. Ungrouped collections are unaffected (group empty, groupCount 0).
  • card now recognizes its title as the first heading in the body rather than only the first child, so a composed header (e.g. a {% bar %} strip before the title) no longer leaves the title carrying a prose-sized top margin. Fixes the gap between a bar header and the title in backlog cards.
  • Fix escapeFenceTags desyncing on code fences with info strings. A titled/attributed fence (e.g. ```yaml title="config.ts") was not recognised as a fence opener, so fence tracking lost sync and {% %} tags following the fence were wrongly escaped — corrupting document structure (for instance a {% /codegroup %} after a titled fence leaking as literal text). The opener now matches the full info string.
  • Add a levels attribute to the bento rune's heading-sugar path: an author-defined footprint ladder, indexed by relative heading depth, where each rung is a column count W (× 1 row) or a footprint WxH — e.g. levels="6,5,4,3,2,1" (uniform-height, width-by-depth; the former span mode) or levels="4x2,3x1,2x1". Depth is measured from the auto-detected base (shallowest heading), so the shallowest is always rung 0; ladders shorter than the heading depth clamp to the last rung. Omitting levels keeps the default tiered sizing unchanged, and explicit {% bento-cell %} grids ignore it.

Rune composability contract

SPEC-084shipped

Rune composability contract

Runes are designed to nest — sandbox inside juxtapose, recipe inside preview, a map inside a card media slot. The engine already propagates parent context (parentRune), applies contextModifiers, and cascades childDensity. What's missing is a contract: a declared, validated, and documented statement of how a rune composes — designed so it holds up in an open plugin ecosystem where third parties add runes we know nothing about, and compose with (or inside) ours.

composabilityenginerunesdx

Overview

The governing principle: dependency asymmetry

Composition dependencies point one way. The composing party always knows what it composes with; the composed-with party never needs to know its consumers. A child knows its parent; an open container should know nothing about its children.

Therefore every composability fact is self-declared by the party that holds the knowledge, pointing outward at a named rune (or, later, a capability) it depends on. The framework keeps no global "who may nest in whom" registry, and an open container never enumerates its allowed children. This is the only model that survives third-party runes on both sides of a nesting.

The existing fields already get the direction right:

  • parent / requiresParent is declared on the child (accordion-item → accordion) — self-knowledge.
  • contextModifiers is declared on the adapting rune (hint says "when in hero, add --in-hero") — self-knowledge.

(An earlier draft of this spec proposed container-side allowedParents / forbiddenParents enumeration. That is a closed-world model and is explicitly rejected: a container cannot enumerate children it has never heard of.)

Two relationships, one of them validates

1. Strict structural coupling — accordion-item, tab, tab-panel, breadcrumb-item, juxtapose-panel, bento-cell, definition, step, tier, map-pin, itinerary-day, itinerary-stop. These runes are meaningless outside their parent, and they always ship in the same plugin as that parent — so the child genuinely knows its parent and validation is a closed sub-world within one author's package. The child self-declares requiresParent, and that is the one thing the validator checks. A third party declaring their own child→parent pair (including a child that requires our published parent) works identically, with zero knowledge of our ecosystem.

2. Open composition — hint-in-hero, map-in-card, widget-in-feature. Containers are open by default: any rune may flow in, and there is nothing to validate. The only thing on offer is optional, additive adaptation (styling) — never permission.

Validation reduces to a single rule: a rune is checked only if it declares a constraint about itself. No self-declaration → no check. This makes the "vice versa" cases resolve cleanly:

CaseWho declaresCross-knowledge needed?
Our map-pin in our mappin: requiresParent: mapnone (same plugin)
Their my-pin in our maptheir pin: requiresParent: mapthey know our published map
Our hint in their sidebarnobody — open compositionnone
Their child in their parenttheir childwe're not involved

Styling follows the same asymmetry

We cannot style a third-party rune inside our container (we don't know it), but a container can adapt its slot, name-agnostically: [data-section="media"] > * gives baseline sizing/clipping to any guest. That is the open-world-correct way to make a map (or anything) sit cleanly in a card media slot — the container adapts the zone, not the specific guest. A rune that wants special adaptation still opts in via its own contextModifiers.

Media zones are a first-class container contract. A container that hosts visual guests exposes a media zone via the shared ----split convention (as card does): the first part is data-section="media", the rest is body. Every such zone is styled by the one name-agnostic selector, so guests compose identically across containers. Containers that lack a zone but should host visuals get one introduced (e.g. bento-cell — WORK-345); the selector itself and the verified guests are WORK-339, and the resulting patterns are catalogued in WORK-346.

Capabilities — the future cross-ecosystem generalization

Concrete rune names cover every real case today (all strict children co-ship with their parents). The generalization, deferred beyond this milestone, is capability tokens: a container declares provides: ['tab-container'], a child declares requires: 'tab-container', and the match is by capability, not name — so anyone's container can satisfy anyone's child. Named requiresParent ships first; capabilities are a later, additive layer.

The contract

  1. requiresParent (self-declared, on the child). Formalizes the existing parent field into a validated constraint. The child names the rune it must live inside. This is the only nesting fact the validator consults.
  2. Open containers. No container declares allowed children. Containers adapt slots via zone selectors and offer childDensity as today.
  3. Context modifiers as the adaptation hook. Every declared contextModifier must describe a meaningful visual adaptation and have CSS coverage (the css-coverage test already asserts this).
  4. Validation severity. A requiresParent violation is a warning by default; for the structurally-meaningless set (the strict children above) it is an error. Non-breaking otherwise.
  5. Documentation + tooling. A composability patterns guide under site/content/extend/rune-authoring/, and a CLI audit reporting requiresParent violations for a content tree and declared-but-unstyled context modifiers.

Non-goals

  • Container-side allowedParents / forbiddenParents enumeration (rejected — closed-world).
  • forbiddenParents ("must not be inside X") — sound as self-knowledge but no concrete need yet; omitted to keep the surface small.
  • Capability tokens — deferred (named requiresParent first).
  • Runtime depth limits or lazy rendering (no evidence of a perf problem).

Acceptance Criteria

  • RuneConfig carries a self-declared requiresParent (formalizing parent), documented in the theme-authoring config API, with the open-world rationale.
  • The pipeline validates requiresParent and surfaces violations — warning by default, error for the structurally-meaningless set — with tests including a third-party-style child requiring a known parent.
  • No container-side child enumeration exists or is documented; the guide states containers are open by default.
  • Every declared contextModifier has CSS coverage; dubious modifiers (Hero in-feature) are removed rather than styled.
  • Container slots adapt name-agnostically via zone selectors (proven by map-in-card).
  • A composability patterns guide ships in the rune-authoring docs; refrakt inspect/audit reports requiresParent violations and missing context-modifier CSS.

References

  • Engine context propagation: packages/transform/src/engine.ts
  • Context modifier + parent declarations: packages/runes/src/config.ts, plugins/marketing/src/config.ts, plugins/design/src/config.ts
  • Coverage carve-outs: packages/lumina/test/css-coverage.test.ts
  • Realised by WORK-336, WORK-337, WORK-338, WORK-339, WORK-345, WORK-346

Bento as a composition substrate

SPEC-085accepted

Bento as a composition substrate

bento should be a foundational, versatile rune — driving everything from marketing magazine sections to dashboards. Today it is heading-only, a cell has no real structure (only an icon is recognized as a visual; everything else is one undifferentiated body), its sizing vocabulary overlaps confusingly, and it is modelled as a page-section. This spec gives bento a solid foundation by converging the cell onto card's zone contract, a precise sizing model, and two clean authoring front doors — and by reducing bento to a pure grid primitive.

bentocomposabilitymarketingrunes

The model: a bento cell is a card in a grid track

A bento-cell adopts card's structure rather than inventing its own:

  • Same zones — data-section="media" / body / footer, split from cell content by the shared --- convention (SPEC-084 media-zone contract).
  • Same media placement — the existing data-media-position vocabulary (top | bottom | start | end), reused verbatim from card.
  • Same guest styling — the name-agnostic media-zone selector (WORK-339) applies unchanged, so any visual rune (chart, map, gallery, …) sits cleanly.
  • Same link affordance — an optional href makes the whole cell a link.

The cell adds only what is genuinely bento's: grid placement and the heading sugar. The payoff is enormous reuse — one zone model, one CSS dimension surface, one media contract — and a cell that is already composition-ready.

Bento is a grid primitive, not a page-section

Bento is only a grid of cells; it does not carry page-section chrome (eyebrow / title / blurb). The current sections + pageSectionAutoLabel preamble is removed — it forced a bento title to be an # h1 (page-title level inside a section) and consumed the top heading instead of letting it size a cell.

A titled bento is a composition: wrap it in a page-section rune (feature / hero / a section) or put a heading above it. Section chrome belongs to the section runes; bento supplies the grid. Bento keeps its universal block dimensions (width / spacing / inset) — those are not tied to the preamble — it just stops being a contentSection.

Consequence: with no preamble eating the top heading, every heading is a cell.

Sizing model

One grid, one vocabulary. The grid defaults to 6 columns (highly divisible — halves 3+3, thirds 2+2+2, 1+2+3, 4+2), the same for both authoring modes, author-overridable via columns.

  • size — a named preset, expressed as a proportion of the column count so it holds its ratio at any columns value:

    sizeproportion@ 6 colstiles with
    small⅓2 × 1small+small+small, small+medium+?
    medium½3 × 1medium+medium
    large⅔ × 2 rows4 × 2large+small
    fullall6 × 1—

    (The widths 2/3/4/6 all tile cleanly into 6.)

  • cols / rows — precise per-axis grid spans (grid-column: span cols; grid-row: span rows). Fully expressive (any W×H). Override size per-axis when both are present (size="large" rows="3" → 4 cols × 3 rows).

The old span cell attribute and the parent sizing="span" mode are removed — span is subsumed by cols, and the per-cell column control it provided now lives in cols/rows. (No content uses explicit cells today, so this is a free cleanup, not a migration.)

Tiered heading → size (sugar)

In the heading-sugar path, the input heading level controls tile size only. The base level is auto-detected (the shallowest heading present), and relative depth maps to a size preset:

heading (relative)size preset@ 6 cols
base (e.g. h2)large4 × 2
base + 1 (h3)medium3 × 1
base + 2 and deeper (h4–h6)small2 × 1

Three coarse tiers are deliberate — heading depth is a blunt signal; finer or taller layouts are what explicit cols/rows cells are for. (Differentiating deep headings by row span is explicitly rejected: rows = height = prominence, so it would invert the hierarchy, and small is already at the 1-row floor.)

Titles are uniform, not scaled by input level

A cell title renders as a real heading at one uniform, auto-detected outline level (cells are siblings in the grid), with uniform visual size via the headline zone typography — never the global h2/h3/h4 type scale. The input heading level is consumed purely for sizing and is not reflected in the rendered title's level or size. (Today the title is emitted as a bare <span>, invisible to the document outline / TOC; promoting it to a uniform heading is an accessibility upgrade, and it falls out of the card-zone headline for free.)

Two authoring front doors (no mixing)

A grid is authored one of two ways; they do not mix within a single bento:

  1. Heading sugar (default). Each heading becomes a cell; its level sets the tile size (tiered, above), its text becomes the cell title. Ergonomic, low-syntax — marketing and magazine layouts.
  2. Explicit cells. A bento whose children are {% bento-cell %} tags uses them directly, short-circuiting heading conversion. Full control per tile: cols/rows (or size), media-position, href, and a ----split media zone. This is the dashboard path.

Detection: if the grid contains explicit bento-cell tags, it is explicit-mode; otherwise headings are converted. (Mixing the two in one grid is out of scope.)

Media placement

media-position is author-controllable per cell via the shared data-media-position attribute (top | bottom | start | end; start/end place the media beside the body for wide cells). The default is derived from cell size — small cells stack media on top; large/full cells place it prominently / beside — and the explicit attribute overrides.

Responsive collapse

Collapse is author-controlled at the grid level, mirroring split: collapse="sm | md | lg | never" sets the breakpoint at which the grid drops to a single stacked column (sensible default), replacing the current hard-coded !important collapse. Between full grid and stacked, the column count steps down progressively and every span auto-caps via min(span, current-columns), so wide cells degrade gracefully at each step with no per-cell config — and a tied row height is rejected precisely because it explodes vertically as columns reduce.

Per-cell breakpoint control is intentionally not a per-cell collapse trigger (cells collapsing at different breakpoints go ragged); the coherent primitive, deferred to WORK-354, is responsive spans (cols="4 lg:2 sm:full").

Signature compositions (drive the design + the docs)

Two compositions define the bento "feel" and are first-class demo targets — both already have partial infrastructure:

  • showcase bleed in a cell. The cell media zone is a clipping container (overflow: hidden), so a showcase guest (wrapping an image/mockup/sandbox) can bleed/offset to show a partial, peeking view — the classic bento screenshot-in-the-corner look. showcase already declares a bento-cell → in-bento-cell context modifier, and showcase.css already keeps the bleed clipped inside bento cells (vs overflow: visible elsewhere); this just needs the cell's clipping media zone (WORK-345) and end-to-end verification.
  • tint per cell. A {% tint %} first child (or tint-mode attribute) gives an individual cell its own background + text colour, so a grid can be multi-coloured. tint is parent: '*' and paints via zero-specificity :where(), so it works provided the cell does not hard-set its own background — the cell must defer its background to the tint-bridged token.

These are requirements on the cell structure, not just demos: the media zone clips, and the cell background is tint-deferrable.

Non-goals

  • Mixing heading-sugar and explicit cells in a single grid.
  • Literal card nesting inside cells — the cell adopts the same contract (zones / media-position / selector), it does not embed a card rune.
  • Page-section chrome on bento itself (use composition for a titled section).
  • Capability-token composition (SPEC-084 future layer).

Acceptance Criteria

  • Bento is no longer a page-section: the sections preamble (eyebrow/title/blurb) is removed; bento keeps width/spacing/inset block dimensions. A titled bento is documented as a composition (wrap in feature/section).
  • A bento-cell exposes card's zone contract: data-section="media"/body/footer via ---, data-media-position, optional href; the WORK-339 media-zone selector styles guests in a cell unchanged.
  • Sizing: columns defaults to 6 for both modes; size presets resolve as proportions of the column count; precise cols/rows per-axis spans are supported and override size. The span attribute and sizing="span" mode are removed.
  • Tiered sugar maps auto-detected relative heading depth → size preset (base=large 4×2, +1=medium 3×1, +2+=small 2×1); the input heading level controls tile size only.
  • Cell titles render as a uniform, auto-detected heading level with uniform visual size (not a <span>, not the input level) — contributing to the document outline.
  • Explicit {% bento-cell %} authoring is first-class and short-circuits heading conversion; heading sugar is unchanged when no explicit cells are present.
  • The icon-as-visual path and the doc-vs-code image/emoji gap are reconciled under the new zone model; the media zone clips and the cell background is tint-deferrable.
  • Dashboards and marketing sections are both demonstrable (compositions docs, WORK-346).

References

  • plugins/marketing/src/tags/bento.ts, plugins/marketing/src/config.ts
  • card zone model: packages/runes/src/config.ts (Card), packages/lumina/styles/runes/card.css, split.css (data-media-position)
  • Media-zone contract: SPEC-084
  • Realised by WORK-345, WORK-347, WORK-348