# Braun Haus — Precision Profile
## Motion handoff

Everything animation-related from the working prototype, pulled out of the
single-file build so it can be read, seen and ported without hunting through
1,929 lines of quiz logic, data sync and layout.

### Start here

**Open `motion-reference.html`** (double-click — no server, no build, works offline).

It runs all 19 animations live, isolated and labelled, with the exact CSS for
each one. It renders from `motion.css` itself, so it can't drift from the spec.

- **Hover** the interaction demos, or press **Trigger** to fire them without a mouse
- **Slow motion** (0.5× / 0.25×) scales playback rate, so relative timings stay true
- **Preview reduced motion** shows the accessibility fallback before you build it

Then read `motion.css`. It's 155 lines and in the same order as the page.

| File | What it is |
|---|---|
| `motion-reference.html` | Live demo of every animation. Open this first. |
| `motion.css` | The motion layer, verbatim: tokens, 9 keyframes, transitions, hover/focus states, and a reduced-motion block. 8 commented sections. |
| `motion.js` | The only JS that drives motion: scroll reveal, overlay enter/exit, toast. 62 lines. |
| `reference/lab-braun-precision-profile.html` | The complete original build, so nothing is lost and anything can be checked in context. |

**Live build:** https://earlyspring.nyc/lab/braun-precision-profile — click through it.
The prototype is the spec; this package is that motion isolated.

---

## The system in one paragraph

One easing curve, `cubic-bezier(.4, 0, .2, 1)`, exposed as `--ease` and used by
**every** transition and keyframe in the build. Interaction feedback runs
.2–.3s, entrances .34–.6s, ambient loops 3–11s and infinite. Nothing else.
The restraint is the design — it's what makes the whole thing feel like one
object rather than a pile of effects. Please keep it.

## Keyframe inventory

| Keyframe | Timing | Drives |
|---|---|---|
| `fade` | .34s | Every route change — the screen arrives as one object |
| `rise` | .4s | Content entering under a heading. 10px of travel |
| `pulse` | 3.4s ∞ | RFID bracelet at rest. Breathing, not blinking |
| `idlepulse` | 3.2s ∞ | NFC ring on the attract screen (also 1s for the agenda sync dot) |
| `idleg` | 6.4s ∞ | Glyph cycle, staggered by inline `animation-delay` |
| `idletube` | 3s ∞ | The Braun lighting motif — tubes warming to `#fff4e2` with a glow |
| `idledawn` | 7s ∞ | Slow dawn gradient. Opacity only; the gradient never moves |
| `idlering` | 4.2s ∞ | Expanding NFC ring, scale .35 → 3.4 with fade |
| `idledrift` | 11s ∞ | 12px drift so idle screens don't read as a frozen frame |

## Three things that will bite you

1. **The `requestAnimationFrame` in `motion.js` §2 is load-bearing.** Without it
   the browser batches the DOM insert and the `.show` class into one style pass
   and the transition silently never runs. It looks like dead code. It isn't.

2. **The exit `setTimeout(250)` must stay in sync with the CSS duration.** If you
   change `.blx`'s transition, change the timeout, or overlays will pop out.

3. **Transitions and their target states are separate rules.** `.choice` carries
   the `transition`; `.choice:hover` is the state it moves *to*. Section 4 of
   `motion.css` keeps each pair together — take both or nothing animates.

## Two calls for the production build

**`prefers-reduced-motion` was not implemented** in the prototype — zero
references. I've written the recommended block as section 8 of `motion.css`;
it ships as-is. The ambient loops run continuously and matter most here, so
they're stopped and held on a legible frame rather than merely shortened.
One design call remains: the attract screens may deserve a *designed* static
frame rather than a paused one. Worth asking rather than defaulting.

**The ambient loops are unthrottled.** That was correct for the actual
deployment — mains-powered iPads showing one screen — so it isn't a bug. But if
any of this is reused on a normal web page, pause on `visibilitychange`.

## Fonts and colour

Motion depends on neither, but for fidelity: the build uses `BraunLinear` with a
Helvetica/Arial fallback, and `--orange: #EA5B0C` as the single accent most
transitions resolve to. Both are in the `:root` block at the top of `motion.css`.
The demo page falls back gracefully if BraunLinear isn't installed locally.

---

## Addendum — transition space tube lights

Added after the original handoff, in response to a live question: the tablet in
the transition space is flanked by 5–6 vertical tube lights, and they need to do
something when a guest taps their NFC bracelet.

**`tap-lights.css`** holds the proposals. It is a separate file on purpose:
`motion.css` is the shipped layer, extracted verbatim, and nothing speculative
should ever end up in it. Both are loaded by the demo page; the tap options are
the last section.

**Live:** https://earlyspring.nyc/lab/braun-motion

### The rig

Tubes carry two custom properties, set at render time:

| Property | Meaning | Drives |
|---|---|---|
| `--i` | index, left to right, `0..n-1` | directional stagger (Strike, Sweep, Level) |
| `--c` | ring out from centre, `0..2` | symmetrical stagger (Bloom, Ripple) |

For six tubes `--c` is `2,1,0,0,1,2`; for five it is `2,1,0,1,2`. The `<b>` inside
each tube is the fill element and is only used by Level.

### The eight options

| # | Name | Timing | Stagger | Reads as |
|---|---|---|---|---|
| 1 | Strike | 1.9s | 90ms, L→R | mechanical ignition |
| 2 | Bloom | 2s | 130ms per ring | the wall answering the hand |  
| 3 | Sweep | 2s | 55ms, L→R | reading the bracelet |
| 4 | Charge | 1.7s | none | one decisive beat |
| 5 | Level | 2.1s | 70ms, L→R | something is loading |
| 6 | Ripple | .95s ×2 | 110ms per ring | the signal made visible |
| 7 | Reject | 1.1s | none | bracelet not read |
| 8 | Composed | 2.6s | three stages | detect → read → confirm |

### Recommendation

**Bloom** if only one ships. The light starts behind the tablet, where the hand
is, and travels out, so the wall reads as responding to the guest rather than
running a cue. **Charge** is the sharper alternative. **Composed** is what to
build if the lookup latency is real and visible: it is the only option that
covers all three moments of a tap rather than one.

Two cautions. **Strike** is the most characterful and the most dangerous, since
a stutter reads as a broken tube as easily as a flourish, and it will be judged
by whoever walks past mid-cycle. **Level** invents a wait unless something is
genuinely loading.

**Reject ships whichever option wins.** A tap that fails silently is the one
failure guests will actually report, and it has to be unmistakable from an
accept state at ten feet, hence the accent orange and the much shorter duration.

### Open calls

1. **Does the wall hold after a tap, or return to idle?** Every option above
   returns, because the tubes are shared by everyone in the room and a held
   state would belong to whoever tapped last. If the space is genuinely
   one-guest-at-a-time, holding is better and the release keyframes come out.
2. **What is the real lookup latency?** It decides between a fixed acknowledgement
   (options 1–7) and the composed sequence. In the composed version, hold stage
   `s2` until the response lands rather than timing it out, or a slow read will
   settle before the data arrives.
3. **Reduced motion is written and ships as-is.** The fallback keeps the
   feedback and removes the travel: no stagger, no stutter, no overshoot, every
   tube cross-fades to full together and holds. This is the one place where
   simply pausing the animation would be wrong, since the tap still has to
   answer.


---

## Addressable LED delivery

Bloom and Reject are cut as files. The tubes are addressable, so there are two
different jobs and two different files for each:

| File | Size | Codec | For |
|---|---|---|---|
| `braun-bloom-pixelmap-6x144-60fps.mov` | 6×144 | QuickTime RLE | playback |
| `braun-bloom-preview-1920x1080-60fps.mov` | 1920×1080 | H.264 | approval |
| `braun-reject-pixelmap-6x144-60fps.mov` | 6×144 | QuickTime RLE | playback |
| `braun-reject-preview-1920x1080-60fps.mov` | 1920×1080 | H.264 | approval |

Bloom runs 3.00s, Reject 1.80s, both at 60fps.

### The pixel maps

One column per tube, one row per LED. **144 rows is an assumption** — it is
60 LEDs/m over 2.4m, a common tube build. Say the real count and the tube
length and these re-render in under a minute; the geometry is parameterised,
nothing is redrawn by hand.

Three things are deliberate:

1. **No glow.** The `box-shadow` in `tap-lights.css` is how a halo is faked on
   a screen. An addressable tube does not emit one, and diffusion happens in the
   physical extrusion. Baking a halo into the pixel data would light LEDs that
   should be dark. The preview files keep the glow; the maps do not.
2. **QuickTime RLE, not ProRes.** RLE is lossless RGB, so the values survive
   exactly, verified by decoding the file back and hashing it against the source
   frames. ProRes 4444 would round every value through YUV, which shows up as
   banding at the low end. If a media server insists on ProRes, it is a one-line
   re-encode, but check the low end afterwards.
3. **The segment length is real.** Bloom's retraction to 86% is not a screen
   effect, it is fewer lit LEDs at each end of the tube. On the pixel map you can
   see the top rows of the outer tubes go to black during the inhale.

### Both files are loop-safe

First and last frames are byte-identical, and both equal the tube's resting
state, `#241a10` at full length. That is what the `scaleY(1)` note in
`tap-lights.css` section 2 is protecting: any keyframe that starts or ends at a
different length snaps the moment the tap begins or releases. So these cut
straight against an idle loop with no jump at either end.

### Two things to check on the real fixture

- **`#241a10` is roughly 14% brightness.** On cheap drivers that low a PWM value
  bands or flickers, and the idle state is the one guests stare at longest. Test
  the ember before committing to it, and raise it if it is not clean.
- **Gamma.** These are sRGB. Most controllers apply their own curve, so the
  ramps will look different on the wall than on a monitor. Bloom's read depends
  on the ramp between `#1a1209` and `#fff4e2`, so check it in the tube, not in
  the file.
