# GUIDE — Motion register

**Status:** law — v1, 2026-08-31
**Applies to:** everything that moves on a Relay surface — estate pages, marketing, product-UI
concepts, decks; email is static and stays static
**Parent:** [`GUIDE.interface-principles.md`](GUIDE.interface-principles.md)
**Pulse family owner:** [`../briefs/SPEC.pulse-motion.md`](../briefs/SPEC.pulse-motion.md) —
ratified; its rules are incorporated **by reference** here, never restated as a second copy
**Live lab:** [`../brand/motion-lab.html`](../brand/motion-lab.html) — the *feedback* tier
**Live specimens:** [`../concepts/kinetic-type-v1.html`](../concepts/kinetic-type-v1.html) —
the *flourish-on-type* tier: rules 1, 3, 4, 5, 6, 7 and 9 each demonstrated as a working
specimen, including rule 5's rejected pattern kept live so the rule has a face

Motion on Relay surfaces runs in two layers, and the register keeps them apart:

- **Feedback** — the ≤0.18s product transitions that answer input (hover, focus, press weight,
  sheet slide). Governed by pulse hard rule 2 and interface-principles rule 4
  (*answers immediately*). Feedback is acknowledgement, not decoration.
- **Flourish** — everything decorative: pulse moments, ambient loops, entrances, eggs. This
  document is the law for where flourish may live and where it may never go.

---

## The pulse family — owned elsewhere, binding here

[`SPEC.pulse-motion.md`](../briefs/SPEC.pulse-motion.md) is the ratified owner of the pulse
family and its **four hard rules** — one pulsing element per view maximum; product transitions
≤0.18s; everything disables under `prefers-reduced-motion`; `#00D9C8` never loops on product
surfaces. They apply in full wherever this register applies. Amendments happen **there**
(the ratification path is in [`BRIEF.interaction-motion.md`](../briefs/BRIEF.interaction-motion.md));
this document only builds around them. If a rule below ever appears to conflict with that spec
on a pulse behaviour, the spec wins and this document gets amended.

---

## The register rules

Nine rules. A page that fails one is not finished.

### 1. CTAs and form inputs never animate and never move

Flourish goes **around** interactive elements, never on them. A button, link, field, or
toggle may carry feedback (≤0.18s state transitions) — it may not drift, shimmer, loop,
pulse, or change position. The visitor's target holds still; a moving target is a missed
click and a broken promise. Decorative motion that frames an input — ticks, rings, seams
**adjacent** to the control — is the sanctioned pattern.
*Demonstrated:* motion-lab **Touch** section — press weight and hover answer the hand and
stop; the flourish lives in the arrival and waiting demos around them.

### 2. Play never gates content

Every page is fully readable **at rest, mid-animation, and with JS off**. No content,
navigation, or conversion sits behind a motion moment: nothing waits for an entrance to
finish, nothing appears only if a script ran. Motion is applied over finished content,
never used to deliver it.
*Demonstrated:* motion-lab — every demo fires on demand over content that is already there;
the eggs gallery (`/concepts/eggs/`) carries the same constraint ("non-essential" is an egg
admission rule).

### 3. Reduced motion gets a complete resting frame — never a blank

`prefers-reduced-motion` is already law without exception
([`GUIDE.interface-principles.md`](GUIDE.interface-principles.md), standing rules). This rule
sharpens what "disables" means: the reduced-motion view is the **finished** frame — every
element present, opaque, in place — not a blank, not a half-assembled entrance state. An
element that fades in must exist at full opacity when motion is off. Pulse hard rule 3's
"the static mark must carry the brand alone" is this rule applied to the mark.
*Demonstrated:* motion-lab's reduced-motion block — "demos resolve instantly; loops freeze"
— rays and dots land at `opacity: 1`, decorative rings at `opacity: 0`.

### 4. One loud piece per page

The pulse spec caps pulses at one per view; this register caps **all** loud motion — of any
family — at one per page, and a pulsing element counts as the one. Loud means it draws the
eye without being asked: a loop, a large entrance, a hero companion. Everything else on the
page stays at whisper tier or fires only on interaction.
*Demonstrated:* motion-lab fires demos on demand "for exactly that reason" (its own words) —
a page of simultaneous loops would violate the law it teaches.

### 5. Page-header headlines stay static

The headline is orientation, not theatre. Kinetic type belongs in callouts, section moments,
and brand surfaces — never on the line that tells the reader where they are. Provenance is a
same-day rejection on the source estate: entrance motion was tried on page headers 2026-08-27
and pulled within hours — *"the text plops in on these headers is too much"* (Erik,
2026-08-27, Execution Space register record).

### 6. Scroll-driven over time-driven on informational surfaces

Where someone is **reading**, motion keys to scroll position, not to a clock: it advances
when the reader advances and rests when they rest. Time-driven loops compete with reading
and never resolve. Brand surfaces (hero, sign-in, empty states — the pulse spec's loop
territory) may be time-driven, inside rule 4's cap.

### 7. Animate `transform` and `opacity` — nothing else, and no motion libraries

Compositor-only properties, so motion never causes layout or jank on a phone. No `width`/
`height`/`top`/`left` animation (the sheet contract in
[`SPEC.app-components.md`](../briefs/SPEC.app-components.md) — translate, never height — is
this rule component-scoped; it is hereby global). No canvas, no WebGL, no ambient-motion or
animation libraries — the estate is zero-dependency, no-build, and stays that way. A property
outside the pair needs a written exception in the owning spec (the pulse ratification path),
not a quiet exemption in CSS.
*Demonstrated:* `brand/relay-pulse.css` — scale and opacity keyframes, with one exception
that took exactly the sanctioned path: `rly-burst`'s mid-frame `stroke` set is the send-burst
colour settle that [`SPEC.pulse-motion.md`](../briefs/SPEC.pulse-motion.md) ratifies (its
hard-moment table: rays fire outward through the pulse colour, settle to teal) — a written
exception in the owning spec, not a quiet exemption in CSS. The motion-lab is not claimed
as compliant: three of its keyframes (`ml-unfold` animates `clip-path`, `ml-lift` `color`,
`ml-nudge` `border-color`) are lab explorations that sit outside the pair and hold no spec
exception — they either earn one before shipping to a live surface or ship rebuilt on
`transform`/`opacity`.

### 8. Width-dependent pieces are verified at both widths

Any piece whose behaviour depends on viewport width — marquees, rails, travel paths, hero
companions — is exercised at a desktop width **and** a phone width before it ships, not
whichever one the builder happened to have open. The source estate got burned exactly here:
*"we got burned by a marquee that only worked on phones"* (Erik, Execution Space handoff,
2026-08) — the desktop failure existed the whole time and only phone-width checks ever ran.
Enforcement is the render-verification ritual below.

### 9. Decorative layers are `aria-hidden` — the text layer stands alone

Interactive and decorative motion **carries** content, never replaces it. Every decorative
layer (SVG marks, rings, particles, kinetic duplicates of text) is `aria-hidden="true"`, and
the plain text layer underneath is complete without it — a screen reader, a text browser, and
a JS-off visitor all get the whole page. This is rule 2's assistive-tech half, and the
standing fix pattern in [`ACCESSIBILITY.md`](../ACCESSIBILITY.md) (decorative logo SVGs →
`aria-hidden`, not `aria-label`).
*Demonstrated:* motion-lab — the lockup SVG and demo ornaments are `aria-hidden`; toasts
announce via `aria-live`, not via their rings.

---

## Enforcement — render verification

The estate's checkers validate text and structure; **none of them can see motion**. The gate for
this register is rendering: before a motion change ships, load the page headless at a desktop
width and a phone width (1400 and 390 are the house pair) and verify — zero JS errors, zero
horizontal overflow, states exercised at both widths (rule 8), and the
`prefers-reduced-motion` frame checked for completeness (rule 3). This rides the
ship-a-bundle checklist in [`MAINTENANCE.md`](../MAINTENANCE.md) ("When a new page ships");
a motion change without a two-width render check is not done.

---

## Amending

Same contract as every guideline: rules earn their place by catching real defects. A motion
finding that fits no rule here is the signal to amend — change this document in the same PR,
cite the incident (EXP-nnn where one exists), add the [`CHANGELOG.md`](CHANGELOG.md) line.
Pulse-family behaviour amends in [`SPEC.pulse-motion.md`](../briefs/SPEC.pulse-motion.md),
never here. Don't add a rule for a hypothetical.

---

## Provenance

The pulse hard rules were ratified 2026-06-10 (v3.4.1, Session FYBFRY) and carried into v3.5
unchanged — [`SPEC.pulse-motion.md`](../briefs/SPEC.pulse-motion.md) is their record. Rules
1, 2, 5, 6, and 8 are adapted from the Execution Space estate's motion register — the
operating model, not the visual language — where each traces to a dated incident (the
2026-08-27 header pull, the phones-only marquee). Rule 3 sharpens existing law: "disables"
alone permitted a blank. Rules 7 and 9 make global what
[`SPEC.app-components.md`](../briefs/SPEC.app-components.md) and
[`ACCESSIBILITY.md`](../ACCESSIBILITY.md) already required in the small.

---

## Framework note

Everything here except the pulse-family reference is **brand-neutral**: the two-layer split,
the nine rules, and the two-width render gate apply to any estate with decorative motion —
`framework-candidate` material for Launchpad. The pulse family is Relay's alone; extraction
means swapping the owning spec, not untangling the rules.
