SPEC-104
Setting up your dashboard 0 entities found · 12/40 branches scanned
ID:SPEC-104Status:accepted

Live sandbox guests in the bg backdrop layer

bg (SPEC-088) can fill its backdrop with an image, video, or gradient — resources fully specified by a URL or token reference. It cannot host a live program (a three.js / canvas visualizer in a sandbox). This spec adds a bg guest: a sandbox rendered into the bg layer as a bare, presentational, full-bleed backdrop — behind a rune's content, beneath its tint/substrate, never in flow. It also adds a named, transform-resolved sandbox preset so a reusable backdrop is applied by name (bg="midnight-waves") like any other preset.

This is the missing layer that lets a surface carry both an animated backdrop and a positioned subject media: the visualizer is the bg, the image/video is the in-flow media guest. It is the general fix behind the music-blog hero (a playlist whose cover is a live audio visualizer with a mood image), but it is a surface-model capability, not a media-specific one.

Target: next minor.

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 %}
![Late-night haze](./moods/midnight.jpg)   <!-- 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.

5. The sandbox preset — transform-resolved

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).

7. Reduced motion & performance

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 backdropsvideo/audio are subject media; they live in the media-guest slot, never bg (§3). A bare video backdrop uses bg video="…".
  • The audio bridge itselfaudio.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)

  1. 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.
  2. sandbox bg preset (§5) — BgPresetDefinition.sandbox, the transform-time expansion step (sandbox readers), schema + project-config home, memoisation.
  3. 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-088bg 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).