Skip to content

1.5 · Make it yours: one layout system

Status: design, building. Milestone: 1.5.

Why

Every scene in 1.4 decides where its words go in its own way, and what a site can change stops at colours and fonts. The engine is loose where it should be firm (seven placement systems that do not agree) and fixed where it should be open (three layouts, seven caption spots, no spacing or motion tokens, no way to add your own markup or CSS). People copy the examples because the examples are the only layouts there are.

1.5 makes one model for where things go and when they show, used by every scene, with presets for the common cases and room to do anything else. Old specs keep building the same page.

What is loose today (measured, see the audit below)

  • Seven placement systems. layout + side (all scenes), Step.place (7 corner strings, or type in the world), Step.at windows (overlay only), Step.length (split/cards only), JourneyStop.side/form/surface (measured from the picture), InsideScene.offset (the subject's side, not linked to the copy's), diorama place.side. Light, code, film (state.beat) and inside each fill at with their own rules.
  • A fixed menu. overlay | split | cards; captions in a 680 x 340 px box at one of 7 spots.
  • Two caption templates that disagree. Overlay captions render button and surface but drop fact and hint; split/cards cards render fact and hint but drop button and surface.
  • Copy is one shape. A step is kicker / title / body / fact. Stats, a product card, a gallery cannot sit at a point in a scene; only the one-take journey takes blocks.
  • Tokens stop at colour and type. --gutter, --max, --panel-r, --ease, caption width, scrim strength and motion timings are hard-coded in runtime/src/styles.css. shade: strong and shade: auto have no CSS at all.
  • Three phone breakpoints. CSS 900 px, isMobile() 760 px, journey 700 px.
  • No escape hatch. No css:, no project templates, no project styles; styles cannot touch scene markup (the scenes.html.j2 hook exists, no style uses it). Style options do not exist (issue #2).
  • No test pins the layout. Assertions are scattered; nothing proves a refactor builds the same page.

The design

1. One placement model: at

Every element a scene shows (a step, a block, the HUD, the cue) can say where it sits:

# docs: no-validate (planned for 1.5, not built yet)
at: { x: 0.07, y: 0.58, w: 0.34, align: left }      # a box in the stage, 0-1 (y: the box's top; h is optional)
at: { grid: "9 / span 4", row: bottom }                 # or a slot on the theme's 12-column grid
at: left | right | center | top-left | ... | auto        # or a named spot (today's corners) or auto (see 7)

Today's fields become presets that compile into it: layout: overlay | split | cards and side are named arrangements of the stage and the copy column; Step.place: top-right is at: top-right; InsideScene.offset and side derive the subject's side from where the copy is (and the other way round), so they cannot disagree. place: {kind: behind | surface | sky} (type in the world) stays as it is: it is a different thing (lettering set inside the picture) and already has its own coordinates.

2. One timeline: show, enter, exit

Every element shows over a window of its scene's progress, the same way in every layout:

# docs: no-validate (planned for 1.5, not built yet)
show: [0.30, 0.46]          # replaces Step.at; split/cards `length` compiles into windows
enter: rise                 # fade | rise | wipe | type-on | scale | none (theme durations and curves)
exit: fade

Scene builders that place copy (inside, light, code, film beats, diorama, journey) keep doing so, but they write show windows through one function instead of mutating Step.at each their own way. The runtime has one element engine (windows, enter/exit, active index by step, not DOM order). This is also the seam where 1.6 lets something other than scroll drive the progress (drive: clock | visits | drag | tilt | pointer).

3. Any block, anywhere

A step can be any block: block: stats | quote | product | cta | features | gallery | faq | ... with that block's own fields, placed with at and timed with show. The kicker/title/body step is one block among them (caption). One caption template renders every field (kicker, title, body, fact, hint, button) in every layout.

4. Tokens all the way down

theme gains the structure that is hard-coded today, each with a default equal to today's value:

# docs: no-validate (planned for 1.5, not built yet)
theme:
  space: { gutter: 6vw, max: 1240px, caption: 680px }     # layout
  type: { scale: 1.25, display: 1.0, body: 1.0 }           # sizes relative to today's
  shade: { strength: 0.55, kind: gradient | scrim | none } # what `shade` means, now real for auto and strong
  motion: { enter: 0.7s, ease: "cubic-bezier(.2,.7,.2,1)", follow: 0.14 }
  grain: 0

Styles declare options: in style.yaml (typed, with defaults and docs); a site sets them in theme.options. Particles first (count, shape, colour, size, speed), then every style (issue #2).

5. Your own pieces

  • css: on a site, a section or an element: scoped to it, checked to parse.
  • blocks/<type>.html.j2 in the project: override a block's markup or add a new block type (with a small schema file), same contract as a style's blocks.html.j2.
  • styles/<name>/ in the project: a whole style of your own, found before the built-in ones.

Everything is validated at build time: a typo fails with the field it meant, never a silently broken page.

6. Phones by rule

One breakpoint, set in the theme (default 760 px), used by CSS and runtime alike. Any at takes a phone: override; every picture takes a focus point so a phone crop keeps the subject.

7. Placement from the picture, for every scene

The journey's measurement (find the calmest region of the frame, pick light or dark lettering, raise a scrim until the text passes WCAG AA) becomes at: auto for any scene with a picture behind its words, measured over the frames the element is shown on.

8. Guardrails: studio layout

studio layout <project> (and the MCP tool layout_check) renders every scene at its key moments with the grid, the safe areas and every element's box drawn on, and reports: text over the subject, contrast under AA, elements that overlap or leave the frame on a phone, windows that never show. Freedom without guardrails makes broken pages.

Compatibility

Every 1.4 field keeps working and compiles into the new model. The release gate: every example builds to the same placement (classes, windows, positions) before and after, checked by a snapshot of each example's compiled page (tests/test_layout_snapshots.py), written first.

Order of work (milestone 1.5, tracked in #1)

  1. 12 Layout snapshots of every example, and #8 CI on every push: the safety net.

  2. 13 The model: at, show, enter/exit in the spec; old fields compile into it; one runtime element engine.

  3. 14 One caption template; any block as a step.

  4. 15 Theme tokens: space, type, shade, motion, grain, one breakpoint.

  5. 2 Style options, particles first.

  6. 16 Your own pieces: css:, project blocks, project styles.

  7. 17 Phones: phone: overrides, focus points.

  8. 18 at: auto from the picture, for every scene.

  9. 19 studio layout + MCP layout_check.

  10. 20 Docs, a migration guide, and one example built three different ways from the same assets.

Speed, alongside the layout work:

  • 21 Faster storyboarding: measured stages, draft stills, only what changed, results streamed into the editor.

  • 4 An inference engine for the local GPU: models kept resident, FP8 where quality holds, faster attention and

    compiled kernels, step-distilled drafts, overlapped CPU and GPU work, each speedup gated on quality.
  • 9 Cache keys by content, not file times.

After 1.5

1.6 · Beyond scroll: drive: on any scene (the clock, return visits, drag and tilt, the pointer as a light or lens, a phone as a controller), explorable spaces, branching paths, sound, editions, a page you can talk to.