Motivation
Two limits collide:
bg hosts only URL/CSS fills. The engine builds the bg layer from bg-* metas (engine.ts §1f): --bg-image: url(…) / a gradient, plus a hand-built <video> for bg-video, an overlay, and a scrim. Every fill is a passive resource the engine can synthesize from a string. A sandbox is not — see below.cover consumes the single media zone. SPEC-089 cover repurposes a rune's media guest as the backdrop ("media fills the interior, content overlays"). That is why a rune can't have a cover backdrop and a positioned subject media at once — there is only one media zone and cover eats it (SPEC-101 got away with it because the sandbox was the sole media).
The surface model already distinguishes the two intents (SPEC-087 §4): the subject media (in flow, framed/chromed, placed by media-position) vs decorative ambiance behind content (bg, out of flow). A live visualizer behind a positioned mood image is the textbook case of ambiance + subject — so the visualizer belongs in bg, the image stays a media guest, and they stop competing for one slot.
The decisive constraint: a sandbox can't be synthesized from a string
bg video="url" works because the engine can hand-build a <video>. A sandbox cannot be built this way: resolving it means reading its source directory, sanitising, and applying the security policy — all of which happen inside the sandbox rune's transform, at page-transform time, using the config.variables.__sandboxReadFile / __sandboxListDir readers (tags/sandbox.ts). The identity engine runs later, on serialized tags, with no file access. So a bg sandbox="scene" attribute the engine expands is a dead end. The guest must be rendered by the real sandbox rune — i.e. it must arrive as authored content (a body) or be expanded into one before transform.
Design
1. The bg guest body
bg (today a directive with no body, tags/bg.ts) gains an optional, constrained body holding a single media guest:
{% hero media-position="start" %}
{% bg %}
{% sandbox src="midnight-waves" framework="three" dependencies="three" /%}
{% /bg %}
 <!-- positioned subject media -->
---
# Midnight Tape
{% /hero %}
- bg transforms its body normally (so the genuine sandbox rune runs, with full file resolution + sanitisation), then tags the rendered guest
data-bg-guest, forces height="fill" (the SPEC-101 host-owned-height mode — iframe height:100%, no auto-resize negotiation), and gives it the backdrop posture (§2 — force-mounted but non-interactive). - bg emits that tagged element alongside its existing
bg-* metas, exactly as the unwrapped metas land among the host's children today.
2. Engine relocation (mirrors bg-video)
When engine.ts §1f raises the bg layer, it additionally collects any data-bg-guest descendant of the host and pushes it into the bg <div>'s children — layered above the --bg-image boot frame and below the overlay/scrim, a structural sibling of the bg-video branch. The guest is marked consumed so it is not also rendered in the content flow.
Backdrop posture — mounted but inert (not SPEC-090 presentational). A bg sandbox must still run — a visualiser whose iframe never mounts is a blank backdrop — so it cannot take SPEC-090's presentational posture, which makes the enhancement layer skip the guest entirely (initRuneBehaviors bails on any [data-guest-posture="presentational"] descendant, leaving the static fallback). The bg guest instead takes a distinct data-guest-posture="backdrop": the enhancement layer does mount it — and forces eager activation regardless of the author's activation mode, so the scene boots without a user gesture — while the posture suppresses interaction only: pointer-events: none, no user-facing controls/poster, removed from the tab order. The contract is "enhance, then make inert," not "don't enhance." No sandbox logic moves into the engine; bg never has to understand sandboxes; the host rune never has to know either.
3. Bare-surface guardrail
The bg layer must stay bare and inert. The body therefore accepts a presentational guest only (a sandbox). A build warning (SPEC-084-style validation) fires if a chromed content rune is placed there:
bg backdrop guest must be presentational (a sandbox); a video carries player chrome (WORK-018) — place it in the media zone as a positioned guest, or use bg video="…" for a bare video backdrop.
This stops the "any media rune" over-generalisation from dragging controls/captions (video, audio, figure) into a decorative layer. The mechanisms stay right-sized: URL → attribute (bg src=/video=); rendered-bare → body (sandbox). A bare image/video backdrop keeps its existing attribute path; only the un-URL-able sandbox needs the body.
4. Boot-frame composition
The meta facets are unchanged and layer with the guest: a --bg-image (image or gradient) paints behind the guest as the SPEC-101 §5 boot frame (designed first paint while the scene initialises); overlay/scrim sit above it for legibility. When the host also carries a positioned image media guest, that image still flows to og:image / structured-data image via the host's normal media extraction (e.g. playlist's extractMediaImage) — the animated backdrop is decorative, the still image is the crawlable/share/reduced-motion representation.
BgPresetDefinition gains a structured sandbox descriptor, a sibling to the engine-resolved gradient/style:
interface BgPresetDefinition {
style?: Record<string, string>; // engine-resolved (CSS)
gradient?: { type?; direction?; stops: string[] }; // engine-resolved (CSS)
sandbox?: { src: string; framework?: string; dependencies?: string }; // NEW — transform-resolved
extends?: string;
}
- Two resolution sites, one name namespace. A new transform-time expansion step (where the sandbox readers live) sees
bg="name", finds a sandbox-typed preset, and injects the real {% sandbox %} guest into the bg body — producing the same data-bg-guest element the engine relocates (§2). The author sees bg="midnight-waves" behave like bg="brand-fade"; internally one resolves to CSS in the engine, the other to a rendered guest at transform time. - The preset is sugar over the §1 body, not a parallel system: the body is the primitive, the preset is a named shorthand the expander turns into it.
- Forced behaviours are not author-set.
height: fill, the backdrop posture (§2 — mounted but inert), and forced eager activation come from the bg-guest mechanism; the preset describes only what the scene is (src/framework/dependencies). A preset may also carry a gradient/style for the boot frame (§4) — both land in the same data-name="bg" div. - Config home: project config. A scene is content (its files live in the project, e.g.
site/examples/midnight-waves/), so a sandbox preset belongs in refrakt.config.json sites.<site>.backgrounds, following the project-vs-theme split SPEC-088 set for the escape hatch; project backgrounds merge over theme. The refrakt.config.schema.json gains the sandbox key.
// refrakt.config.json → sites.main.backgrounds
{
"surface-dark": { "style": { "border": "1px solid rgba(255,255,255,0.12)" } },
"midnight-waves": {
"sandbox": { "src": "midnight-waves", "framework": "three", "dependencies": "three" },
"gradient": { "type": "linear", "direction": "to-b", "stops": ["surface", "ishi"] }
}
}
6. Reuse: placement via the layout cascade, audio via SPEC-006
DRY reuse splits along two axes:
- Value reuse — the named preset (§5) defines the scene once.
- Placement reuse — the layout cascade applies it once. For "every blog post," set
bg="midnight-waves" in the blog-article layout's cover region (the route rule blog/* → blog-article already exists); every post inherits the backdrop with zero per-post authoring. - Per-page audio — the SPEC-006 audio↔sandbox bridge (
audio.onFrame, audio.onTrackChange, the DATA.tracks global, streaming lifecycle) binds each page's own player to the shared scene at runtime, so one named backdrop reacts to each post's mixtape. The bridge itself is SPEC-006's; this spec only ensures a bg-layer sandbox is a valid subscriber. - Memoisation — a named scene's assembled source is byte-identical across pages, so the expander may build the bundle once and clone per page (an optimisation only the named path affords).
A live sandbox backdrop is an always-running animation behind every page that carries it (it spreads by layout cascade, §6), so it must be gated like any other motion — consistent with the SPEC-105 reduced-motion baseline rather than a one-off.
- Reduced motion → boot frame only. Under
prefers-reduced-motion: reduce, the live scene is not mounted; the §4 boot frame (the preset/inline gradient/image still) stands in as the complete, static representation. This reuses the backdrop posture's forced-activation switch — the enhancement layer simply declines to mount the scene under reduced motion, exactly as it marks reveals in-view immediately (SPEC-105). - Off-screen / hidden → suspended. The backdrop pauses (or tears down) its render loop when scrolled off-screen or on a hidden tab, so a long article isn't driving a 3-D scene the reader can't see. The boot frame remains painted underneath, so suspension is invisible.
- No-JS / crawler already get the boot frame (the scene only exists once enhanced), matching the SPEC-105 "static page is always complete" rule.
Acceptance Criteria
bg accepts an optional, constrained body holding one bare guest (sandbox); bg transforms it (real sandbox rune — file resolution + sanitisation), tags it data-bg-guest, and forces height="fill" + the backdrop posture.- The engine (§1f) relocates a
data-bg-guest element into the data-name="bg" layer — above the --bg-image boot frame, below overlay/scrim, sibling to bg-video — and marks it consumed so it does not render in content flow. - Backdrop posture (§2): the bg guest gets
data-guest-posture="backdrop" — the enhancement layer mounts it and forces eager activation (the scene runs), while suppressing interaction only (pointer-events: none, no controls, out of tab order). It is explicitly not SPEC-090 presentational (which would skip enhancement and leave a dead backdrop); a test covers that a backdrop guest mounts while a presentational one does not. - A chromed content rune (
video/audio/figure) in the bg body produces a build warning redirecting to the media-guest slot or bg video="…"; bare image/video backdrops keep their existing bg src=/video= attribute path (unchanged). - Boot frame composes: a preset/inline
gradient/image paints behind the guest; overlay/scrim above it; a host's positioned image still flows to og:image / structured-data image. - Reduced motion & performance (§7): under
prefers-reduced-motion: reduce the live scene is not mounted and the boot frame stands in; the backdrop suspends its render loop when off-screen / on a hidden tab; no-JS/crawler render the boot frame. Consistent with the SPEC-105 reduced-motion baseline. BgPresetDefinition gains a sandbox descriptor (src/framework/dependencies), resolved at transform time by an expansion step that injects the §1 body guest — not in the identity engine; bg="name" reaches author-parity with a gradient preset.- A sandbox preset may also carry
gradient/style (boot frame); extends resolves as for other bg presets; refrakt.config.schema.json documents the sandbox key; project backgrounds merge over theme. - Docs: the
bg reference documents the guest body, the bare-surface guardrail, the sandbox preset (with the refrakt.config.json example), and the boot-frame layering; contracts regenerated (refrakt contracts --check green) and CSS coverage passes for any new bg-guest selectors.
Non-goals
- Chromed runes as backdrops —
video/audio are subject media; they live in the media-guest slot, never bg (§3). A bare video backdrop uses bg video="…". - The audio bridge itself —
audio.onFrame/onTrackChange, the streaming lifecycle, and the DATA global are SPEC-006's; this spec only makes a bg-layer sandbox a valid subscriber. - Playlist/hero cover parity — porting cover-overlay modifiers onto leaner runes is a separate layout item; this spec is the
bg layer only. - A first-party (non-sandbox) canvas backdrop — a built-in visualizer component that shares the AudioContext without an iframe is a distinct, later option; here the guest is an author-editable
sandbox. - Generalising the guest body to arbitrary content — one bare guest only (a
sandbox), gated by the §3 guardrail.
Work breakdown (provisional)
- bg guest body + engine relocation + guardrail (§1–§4, §7) — bg body,
data-bg-guest tagging + fill, the backdrop posture (mounted-but-inert + forced eager) and the reduced-motion/off-screen gating, engine §1f relocation, bare-surface validation, boot-frame layering. sandbox bg preset (§5) — BgPresetDefinition.sandbox, the transform-time expansion step (sandbox readers), schema + project-config home, memoisation.- Docs + showcase (§6) —
bg reference, refrakt.config.json example. The music-blog audio-reactive showcase is deferred until the SPEC-006 audio bridge is built (it is accepted but not yet implemented); a static/non-reactive live backdrop is demonstrable from item 1 alone, and the reactive showcase lands with SPEC-006.
References
- SPEC-088 —
bg gradients + escape hatch; BgPresetDefinition, engine §1f resolution (engine.ts), tags/bg.ts, bg.css. - SPEC-087 — surface-fill model; the subject-media-vs-ambiance boundary (§4) this builds on.
- SPEC-089 — cover layout (why cover consumes the single media zone); the subject/ambiance line.
- SPEC-090 — media-guest interaction posture; the bg guest defines a sibling
backdrop posture (mounted-but-inert), distinct from presentational (not enhanced) — see §2. - SPEC-101 — sandbox as a cover media guest (prior art):
height="fill" host-owned height, the boot-frame guidance, production posture; packages/behaviors/src/elements/sandbox.ts. - SPEC-006 — the audio↔sandbox bridge (
audio.onFrame/onTrackChange, streaming lifecycle) the backdrop subscribes to. - WORK-018 — the
video rune's player chrome (why it is a subject, not a backdrop). - SPEC-084 — rune validation (the bare-surface build warning).