类型:Site模板 · 收录:2026-11-05 01:12:10 · 源码:会员可见
# Recreate this site as a single HTML file: Relay
You are an expert creative front-end developer. Produce a **single self-contained `index.html`**
that reproduces the project below **exactly** — same layout, ten sections, visuals, motion and
interaction. Pure HTML/CSS/JS in one file: no build step, no framework, no bundler. Use ES modules
with a CDN importmap for **three.js 0.186.1** (and its `addons/postprocessing` passes) and
**Lenis 1.3.26**; load the three Google Fonts with one `<link>`. Hardcode every value given here as
a fixed constant. Every GLSL block below is the project's own source and is to be carried over
**verbatim**; every TypeScript snippet is the project's logic — keep the numbers and the order,
drop only the type annotations and the React plumbing. Rebuild each section described below as a
clearly delimited block of the one file, in the order given.
## What it is
The landing page of **Relay**, a workflow-automation product ("The handoffs run themselves"):
a black page with one lime accent, drawn as four Figma frames (1440 / 1024 / 768 / 390) on a rem
grid, and threaded end to end by **one page-long particle stream** — a three.js ribbon of lime
motes and glowing filaments, drawn on a fixed viewport canvas blended with `screen`, laid out in
document coordinates against the sections, pushed on down its curve by the scroll, and read
through a frosted slab that four of the sections sit on. It opens behind a preloader: a lime line
runs round the screen edge as real load progress, floods the screen, and folds — screen → square →
the 76px logo tile — to uncover the hero, whose glass product window boots a refund workflow and
then runs it live forever while the whole window tilts toward the pointer. Then: a fixed nav, an
integrations row, three product feature rows, a lime plate that opens out of the logo and folds
back into it mid-page, a runtime hub and a regions card, a Tuesday dashboard, three counted-up
proof stats, a pricing meter you drag, and an aurora footer. Every title comes out of a blur behind
a mask with one Instrument Serif italic word; every readout types itself in Fragment Mono; every
block hands over to the next by a scroll-scrubbed fade-and-drift; a lime cursor ring glides after
the pointer. Springs only — `{tension 280, friction 32}` and `{120, 26}` — with a 70 ms stagger.
The sections, in order (each has its own block below): **Preloader · Backdrop · Particle stream ·
01 Navigation · 02 Hero · 03 Integrations · 04 Product · 05 Statement · 06 Scale · 07 Signals ·
08 Proof · 09 Pricing · 10 Footer**, with the block-to-block transitions described under
"Transitions". The page column is `<main>` centred at the frame's width; Product → Proof sit inside
one frosted slab wrapper bled to the viewport edges.
## Page shell & libraries
One `index.html`. Libraries by CDN importmap; nothing else is installed:
```html
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/",
"lenis": "https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.mjs" } }
</script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Instrument+Serif:ital@1&family=Fragment+Mono&display=swap">
```
- `<title>Relay — the handoffs run themselves</title>`, meta description "Relay runs the steps between your apps and only pings a person when it matters. Approvals in Slack, automatic retries, 240 apps connected.", `theme-color #000000`, `viewport-fit=cover`.
- Three faces: **Inter** (400 / 500 / 600) is the page's sans (`--font-inter`), **Instrument Serif italic 400** sets the one serif word in every display title (`--font-instrument-serif`), **Fragment Mono 400** sets every chip, label, readout and typed line (`--font-fragment-mono`).
- The first `<script>` in `<head>`, before any stylesheet, gates the preloader: `if(!matchMedia("(prefers-reduced-motion: reduce)").matches&&!/lighthouse|googlebot|pagespeed|headlesschrome|gtmetrix|pingdom|bingbot|yandexbot/i.test(navigator.userAgent))document.documentElement.dataset.preload=""`. A crawler, a lab run or reduced motion opens the page at once with no preloader and no WebGL.
### Tokens (the whole palette and scale — copy verbatim into `:root`)
```css
:root {
--raw-color-white: #ffffff;
--raw-color-black: #000000;
--raw-color-neutral-300: #bababa; /* bottom stop of the display gradient */
--raw-color-neutral-200: #d6dadd; /* serif accent word in titles */
--raw-color-neutral-400: #9ba0a6; /* text-muted */
--raw-color-neutral-500: #747980; /* text-dim */
--raw-color-neutral-850: #131416; /* surface-2 */
--raw-color-neutral-875: #121315; /* badge fill */
--raw-color-ink-950: #0a0b09; /* text on the lime accent */
--raw-color-lime-400: #c8f751; /* accent */
--raw-color-lime-300: #d6f84c; /* badge dot */
--raw-color-lime-100: #eaff9e; /* accent-soft */
--raw-color-lime-900: #3d5410; /* particle stream, far end */
--raw-color-sky-400: #52c8ff; /* particle stream, second hue */
--raw-color-ink-glass: rgba(8, 9, 10, 0.22); /* glass slab fill */
--raw-blur-slab: 0.75rem;
--raw-color-ink-card-glass: rgba(14, 15, 17, 0.42); /* glass cards */
--raw-blur-card: 1rem;
--raw-color-white-a04: rgba(255, 255, 255, 0.04);
--raw-color-white-a10: rgba(255, 255, 255, 0.1);
--raw-color-white-a16: rgba(255, 255, 255, 0.16);
--raw-color-white-a22: rgba(255, 255, 255, 0.22);
--raw-color-black-a50: rgba(0, 0, 0, 0.5);
--raw-color-black-a55: rgba(7, 7, 7, 0.55);
--raw-color-white-a05: rgba(255, 255, 255, 0.05);
--raw-color-white-a06: rgba(255, 255, 255, 0.06);
--raw-color-lime-400-a35: rgba(200, 247, 81, 0.35);
--raw-color-white-a70: rgba(255, 255, 255, 0.7);
--raw-color-black-a70: rgba(0, 0, 0, 0.7);
--raw-blur-nav: 0.75rem;
--raw-radius-nav-menu: 1.5rem;
--raw-radius-nav-menu-row: 0.875rem;
--raw-radius-nav-menu-tile: 0.6875rem;
--raw-blur-2: 0.125rem;
--raw-font-size-14: 0.875rem; --raw-font-size-16: 1rem; --raw-font-size-18: 1.125rem;
--raw-font-size-20: 1.25rem; --raw-font-size-24: 1.5rem; --raw-font-size-26: 1.625rem;
--raw-font-size-30: 1.875rem; --raw-font-size-32: 2rem; --raw-font-size-36: 2.25rem;
--raw-font-size-40: 2.5rem; --raw-font-size-44: 2.75rem; --raw-font-size-50: 3.125rem;
--raw-line-height-20: 1.25rem; --raw-line-height-24: 1.5rem; --raw-line-height-28: 1.75rem;
--raw-line-height-32: 2rem; --raw-line-height-42: 2.625rem; --raw-line-height-50: 3.125rem;
--raw-line-height-62: 3.875rem;
--raw-letter-spacing-minus-2: -0.125rem; --raw-letter-spacing-minus-1: -0.0625rem;
--raw-letter-spacing-0-4: 0.025rem;
--raw-size-page: 90rem; --raw-size-page-inset: 5rem;
--raw-size-page-1024: 64rem; --raw-size-page-inset-1024: 3rem;
--raw-size-page-768: 48rem; --raw-size-page-inset-768: 2.5rem;
--raw-size-page-390: 24.375rem; --raw-size-page-inset-390: 1.25rem;
--raw-size-section-gap: 4.5rem; --raw-size-section-gap-1024: 4rem;
--raw-size-section-gap-768: 3.5rem; --raw-size-section-gap-390: 3rem;
--raw-shadow-float: 0 0.5rem 1.375rem -0.25rem rgba(0, 0, 0, 0.5);
--raw-drop-shadow-cta: 0 0.5rem 0.6875rem rgba(0, 0, 0, 0.5);
--raw-duration-fast: 150ms; --raw-duration-normal: 250ms;
--background: var(--raw-color-black);
--foreground: var(--raw-color-white);
--foreground-muted: var(--raw-color-neutral-400);
--foreground-dim: var(--raw-color-neutral-500);
--title-gradient-from: var(--raw-color-white);
--title-gradient-to: var(--raw-color-neutral-300);
--title-accent: var(--raw-color-neutral-200);
--surface-raised: var(--raw-color-neutral-850);
--surface-badge: var(--raw-color-neutral-875);
--surface-glass: var(--raw-color-white-a04);
--surface-status: var(--raw-color-black-a55);
--border-subtle: var(--raw-color-white-a10);
--border-glass: var(--raw-color-white-a22);
--glass-from: var(--raw-color-white-a16);
--glass-to: var(--raw-color-white-a04);
--accent: var(--raw-color-lime-400);
--accent-soft: var(--raw-color-lime-100);
--accent-marker: var(--raw-color-lime-300);
--on-accent: var(--raw-color-ink-950);
--backdrop-light: var(--raw-color-white-a05);
--backdrop-base-light: var(--raw-color-white-a06);
--backdrop-base-light-peak: var(--raw-color-white-a05);
--backdrop-base-line: var(--raw-color-lime-400-a35);
--section-gap: var(--raw-size-section-gap);
--stream-main: var(--accent);
--stream-alt: var(--raw-color-sky-400);
--stream-core: var(--accent-soft);
--stream-deep: var(--raw-color-lime-900);
--stream-spark: var(--raw-color-white);
--glass-slab: var(--raw-color-ink-glass);
--glass-slab-edge: var(--raw-color-white-a06);
--glass-slab-frost: var(--raw-blur-slab);
--card-glass: var(--raw-color-ink-card-glass);
--card-frost: var(--raw-blur-card);
--type-label: var(--raw-font-size-14); --type-body: var(--raw-font-size-16);
--type-lead: var(--raw-font-size-18); --type-wordmark: var(--raw-font-size-20);
--type-section-title: var(--raw-font-size-44); --type-section-title-accent: var(--raw-font-size-50);
--line-label: var(--raw-line-height-20); --line-body: var(--raw-line-height-24);
--line-lead: var(--raw-line-height-28); --line-section-title: var(--raw-line-height-62);
--letter-spacing-title: var(--raw-letter-spacing-minus-2);
--letter-spacing-wordmark: var(--raw-letter-spacing-minus-1);
--letter-spacing-badge: var(--raw-letter-spacing-0-4);
--page-width: var(--raw-size-page); --page-inset: var(--raw-size-page-inset);
--elevation-float: var(--raw-shadow-float); --elevation-cta: var(--raw-drop-shadow-cta);
--duration-fast: var(--raw-duration-fast); --duration-normal: var(--raw-duration-normal);
--glass-stroke-active: var(--raw-color-white-a70);
--nav-surface-scrolled: var(--raw-color-black-a70);
--nav-blur: var(--raw-blur-nav);
--nav-menu-corner: var(--raw-radius-nav-menu);
--nav-menu-corner-row: var(--raw-radius-nav-menu-row);
--nav-menu-corner-tile: var(--raw-radius-nav-menu-tile);
--glass-blur: var(--raw-blur-2);
--font-inter: "Inter"; --font-instrument-serif: "Instrument Serif"; --font-fragment-mono: "Fragment Mono";
--ease-entrance: cubic-bezier(0.2, 0, 0, 1);
}
@media (width <= 1200px) { :root { --page-width: 64rem; --page-inset: 3rem; --section-gap: 4rem; --type-section-title: 2.25rem; --type-section-title-accent: 2.5rem; --line-section-title: 3.125rem; } }
@media (width <= 900px) { :root { --page-width: 48rem; --page-inset: 2.5rem; --section-gap: 3.5rem; --type-section-title: 1.875rem; --type-section-title-accent: 2rem; --line-section-title: 2.625rem; } }
@media (width <= 580px) { :root { --page-width: 24.375rem; --page-inset: 1.25rem; --section-gap: 3rem; --type-section-title: 1.5rem; --type-section-title-accent: 1.625rem; --line-section-title: 2rem; } }
body { background: var(--background); color: var(--foreground); font-family: var(--font-inter), sans-serif; -webkit-font-smoothing: antialiased; overflow-x: clip; }
```
### The four drawn frames and the rem grid
The page is drawn as four Figma frames — **1440** (base), **1024** (≤ 1200px), **768** (≤ 900px) and **390** (≤ 580px). Every length in the page is in `rem` (design px ÷ 16) and the root font-size tracks the viewport so each frame renders 1:1 at its reference width and scales below it; above its reference a frame never grows (the rem is capped at 16px) and the page column centres. Only the 1440 frame keeps growing, up to 1920 (a 21.333px rem), then centres too:
```css
html { font-size: min(1.111111vw, 21.333333px); } /* 16 · 100 / 1440 */
@media (width <= 1200px) { html { font-size: min(1.5625vw, 16px); } } /* / 1024 */
@media (width <= 900px) { html { font-size: min(2.083333vw, 16px); } } /* / 768 */
@media (width <= 580px) { html { font-size: min(4.102564vw, 16px); } } /* / 390 */
```
Content insets per frame: 80 / 48 / 40 / 20 px (`--page-inset`). The page column is `<main>`: `position: relative; margin: 0 auto; width: var(--page-width); overflow-y: clip`, inside a wrapper with `overflow-x: clip` so decorations bleed into the margins and clip at the viewport. Between blocks sits a spacer of `var(--section-gap)` (72 / 64 / 56 / 48 — 1.5× the frames' own gaps).
Composition, top to bottom: preloader overlay · backdrop layers · the fixed particle-stream canvas · fixed nav + a nav slot (88 / 88 / 64 / 56 tall) · `#hero` · `#integrations` (ScrollBlur leave+still) · gap · **the frosted slab** wrapping `#product` (ScrollBlur enter), gap, `#statement` (LogoOpen), gap, `#scale` (ScrollBlur leave), gap, `#signals` (ScrollBlur enter+leave), gap, `#proof` (ScrollBlur enter+leave) · gap · `#pricing` · gap · `#footer` · the cursor ring. The slab is an absolutely positioned pseudo-element behind Product → Proof, bled to the viewport edges (`left/right: calc(50% − 50vw)`), `background: var(--glass-slab)`, 1px top and bottom borders `var(--glass-slab-edge)`, `backdrop-filter: blur(12px)` — the stream reads through it frosted.
### Motion engine (replaces @react-spring/web — build a small inline version)
Every motion is a **spring**; there are no CSS keyframes. Two physics presets and one timing constant drive the page:
| preset | tension | friction | used for |
|---|---|---|---|
| `SPRING` | 280 | 32 | rises, hover sweeps, scrubs — short, dense, no overshoot (~450 ms) |
| `SPRING_SOFT` | 120 | 26 | blur-ins, sweeps, strokes, fades |
| `STEP` | 70 ms | | the stagger between siblings (`delay = i × STEP`) |
Integrate the spring exactly like react-spring: 1 ms sub-steps, `springForce = −tension × 0.000001 × (pos − to)`, `dampingForce = −friction × 0.001 × vel`, `vel += (springForce + dampingForce) / mass × 1`, `pos += vel × 1`; at rest when `|vel| ≤ restVelocity` and `|to − pos| ≤ precision` (precision = `min(1, |to − from| × 0.001)`, restVelocity = precision / 10). Retargeting keeps the velocity. Timed springs (`{ duration, easing }`) interpolate linearly through the easing. Under `prefers-reduced-motion: reduce` every spring jumps straight to its end.
A single shared `requestAnimationFrame` ticker drives everything (springs, scroll scrubs, the WebGL stream); each subscriber has its own minimum gap in ms.
**When an entrance plays — `seen`.** An element counts as *seen* the moment it is clearly on screen: one shared IntersectionObserver with `rootMargin: "0px 0px 5% 0px"`; anything on screen while `scrollY < 4` (the first screen) is seen at once; reaching the bottom of the page marks everything whose top is above the fold. Entrances start only once seen **and** intersecting (a plain IO with 0 margin), play once and stay.
The shared primitives, all gated on seen and delayed by `delay` ms after it:
- **Reveal `rise`** — `opacity 0 → 1`, `translateY 24px → 0`, `SPRING`. **Reveal `fade`** — opacity only, `SPRING_SOFT`.
- **DisplayTitle / FocusText ("Фокусировка")** — the whole element comes out of a blur while a soft mask sweeps left → right so letters resolve in reading order: `opacity 0 → 1`, `filter: blur(10px) → blur(0)` (8px for body copy), and a custom property `--reveal` from `0%` to `140%` feeding `mask-image: linear-gradient(90deg, black calc(var(--reveal) − 40%), transparent var(--reveal))`; `SPRING_SOFT`. Display titles are Inter 600 with a vertical white → `#bababa` gradient clipped to the text (`background-clip: text; color: transparent`), letter-spacing −2px (`--letter-spacing-title`), and one Instrument Serif italic word in `#d6dadd` at `line-height: 1` on the same baseline (its larger size must not grow the line box).
- **DrawLine** — a hairline drawing from the left: `scaleX 0 → 1`, `transform-origin: left`, `SPRING_SOFT`.
- **PulseDot** — the badge's 6px `#d6f84c` dot gets a one-off halo: a 1px ring of the same colour over it springs `scale 1 → 2.6`, `opacity 0.8 → 0`, `SPRING_SOFT`, 250 ms after seen.
- **TypeText** — a Fragment Mono label types itself in, whole characters at a time: a custom property `--typed` runs linearly from 0 to `length + 0.5` over `length × 30 ms` and the text is clipped with `clip-path: inset(0 calc(100% − round(down, calc(var(--typed) × 1ch), 1ch) − 0.1ch) 0 0)`; the copy stays one text node.
- **Badge** — an inline-flex pill: `gap 8px`, 1px `rgba(255,255,255,.1)` border, fill `#121315`, padding 5px 13px 5px 11px (7px 15px 7px 11px at ≤1200), `box-shadow: 0 8px 22px −4px rgba(0,0,0,.5)`, label Inter 500 14/20 `letter-spacing .4px` in `#9ba0a6`, the pulse dot before it.
- **PillButton** — `md` 40px tall, 14/20 type, `lg` 48px tall, 16/24; `padding 0 24px`, fully rounded, Inter 500, no wrap.
- **LimeCta** (primary): fill `#c8f751`, ink `#0a0b09`, `filter: drop-shadow(0 8px 11px rgba(0,0,0,.5))`, `overflow: hidden`. Hover ("handoff"): a spring `p 0 → 1` (`SPRING`) drives — the lime fill layer's opacity `1 − p²`; an ink layer (`#0a0b09`, text `#c8f751`) revealed by `clip-path: inset(0 (1 − p)·100% 0 0 round 999px)` sweeping in from the left; the ink label shifts `x = −5p px`; the Relay chevrons (an inline 28×28 SVG, two 3px round-capped `M8 8l6 6-6 6` / `M14 8l6 6-6 6` strokes, the first at 50% opacity, sized `0.8em`, `margin-left .4em` after the label) slide `x = −8 + 8p`, `opacity p`; a 1px lime stroke ring — a `conic-gradient(from 0deg, #c8f751 var(--fill), transparent var(--fill))` masked to a 1px ring (`padding: 1px; mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude`) — fills clockwise, `--fill 0 → 100%` over **1100 ms easeInOutCubic**, back in 450 ms; scale `1.02` hovered, `0.97` pressed (`SPRING`). Hover only on `(hover: hover) and (pointer: fine)`.
- **GlassButton** (secondary): 1px `rgba(255,255,255,.22)` border, `backdrop-filter: blur(2px)`, vertical gradient `rgba(255,255,255,.16) → rgba(255,255,255,.04)`, white text, `gap 8px`, `box-shadow: 0 8px 22px −4px rgba(0,0,0,.5)`. Hover: a brighter stroke (`rgba(255,255,255,.7)`) runs once round the pill on the same masked conic ring at `inset: −1px`, 1100 ms easeInOutCubic (450 back); press scale `0.97` (`SPRING_SOFT`). The pill must not clip its overflow.
- **SectionHead** — badge + title in a 760px column (flex column, gap 16), the aside copy 440px wide, Inter 16/24 `#9ba0a6`, bottom-aligned 48px to the right. ≤1200: stacked and centred, gap 16, aside 560 wide centred; ≤580: aside full width, stack gap 12. Title delay `STEP`, aside delay `2 × STEP`.
**Scroll.** Lenis (`new Lenis({ smoothWheel: true })`) on its own rAF; every per-frame scroll read uses Lenis's `scroll` number, never `window.scrollY`; scroll-driven wrappers measure their document offset once (and on ResizeObserver / resize), never per frame. Lenis is re-measured (`lenis.resize()`) whenever the preloader flags change and once fonts are ready.
**Cursor.** See "The transitions" — a lime ring glides after the native cursor on fine pointers.
**Reduced motion** stills every spring, freezes the stream on one drawn frame, and skips the preloader. A crawler UA gets neither the preloader nor WebGL.
## The loader / reveal
A black screen with the lime Relay tile in the middle; a lime line runs round the edge of the screen and how far it has gone **is** the loading progress. When the ring closes, the lime floods inward from it until the screen is all lime, then the lime folds back along the Statement fold (screen → square → tile) into the logo, uncovering the hero, and the mask keeps folding past the tile, cutting it down to nothing where it is. The hero is first seen — and plays its entrance — as the lime folds away.
**Gate.** The inline head script sets `html[data-preload]` before first paint (never for reduced motion or a crawler UA). While that attribute is set: `html { overflow: hidden }`, `main { transform: translateY(200vh) }` (the page waits off-screen so no entrance plays early) and the overlay `[data-preloader]` is `display: block`. Without the attribute the overlay never shows and the page is live at once.
**Markup.** A `position: fixed; inset: 0; z-index: 60; pointer-events: none` overlay holding: a full-bleed black stage (`#000000`); an SVG the size of the viewport (`viewBox 0 0 w h`) with one `<rect>` — inset `8 × k` px from every edge, corner radius `24 × k`, `stroke #c8f751`, `stroke-width 2 × k`, `stroke-linecap round`, `pathLength="100"`, `stroke-dasharray "100 100"`; a flood `<div>` over everything; a lime fold plate (`#c8f752`, the Statement plate colour); and the logo tile — `76 × k` px square centred (`top/left: calc(50% − 2.375rem)`), holding `nav/nav-logo-mark.svg` (the lime Relay mark, 28 × 28 scaled to fill) and, over it, the dark Statement mark (`.logo-open-fly`: `border-radius 31.58%`, 1px `rgba(10,11,9,.25)` border, `linear-gradient(135deg, #252721, #0a0b09 71.429%)`) with `statement/statement-mark-glyph.svg` (34 × 37) at `top 24.3% / left 28.25% / width 44.1% / height 48.76%`. `k` = root font-size / 16 (the frame scale), re-measured with `w`, `h` on resize.
**Progress.** `progress = 0.04 + 0.76 × (done + fonts) / (eagerImages + 1)` where `done` counts every non-lazy `<img>` that has loaded (or errored) and `fonts` is 1 once `document.fonts.ready` resolves; on the window `load` event (or after `MAX_WAIT = 6000` ms regardless) progress becomes 1. Progress is only ever raised.
**Three springs, four phases** (`draw → flood → fold → done`):
1. **draw** — `d` springs to `progress` with `{ tension: 60, friction: 22 }`; the rect's `stroke-dashoffset = 100 × (1 − d)`. Retargeting the spring keeps its velocity, so the line never stops and restarts. When `d ≥ 0.96` (`CLOSED` — the last sliver is the spring's tail) → flood.
2. **flood** — `k` springs `0 → 1` with `{ tension: 140, friction: 26 }`. The flood div shows (`opacity 1` while `k > 0`) with `box-shadow: inset 0 0 0 (inset + k × (reach − inset)) px #c8f752`, where `inset = 8 × k` and `reach = ceil(max(w, h) / 2) + 2` — the ring thickening inward until the screen is lime. The dark Statement mark fades in over the lime logo with opacity `k`, so the tile keeps its contrast. When `k ≥ 0.97` (`FULL`) → fold.
3. **fold** — ONE timed motion `c: 0 → 1`, `{ duration: 1000, easing: easeInOutQuad }`. `html[data-preload]` is removed (scrolling comes back, `<main>` drops into place) and `html[data-preload-closing]` is set for the duration so heavy work (the particle stream) waits it out. The stage, ring and flood are gone; the lime plate now covers the screen with a `clip-path` from `mask(c)`, and the tile is cut by `cut(c)`:
```js
const half = v => (1 - v) * (Math.max(w, h) / 2) // the mask's larger half-extent, shrinking
const mask = v => {
const H = half(v)
const hw = Math.min(H, w / 2), hh = Math.min(H, h / 2)
const side = Math.min(hw, hh) * 2
// while the mask is wider than the screen is tall only its width closes (screen → square);
// its corner grows into the tile's as it squares up
const squared = Math.max(w, h) === Math.min(w, h) ? 1
: Math.min(1, Math.max(0, (Math.max(w, h) / 2 - H) / ((Math.max(w, h) - Math.min(w, h)) / 2)))
const round = side * (24 / 76) * squared
return `inset(${h / 2 - hh}px ${w / 2 - hw}px round ${round}px)`
}
const cut = v => { // once smaller than the tile, the tile is cut down with it
const side = half(v) * 2, tile = 76 * k
if (side >= tile) return "none"
return `inset(${(tile - side) / 2}px round ${side * (24 / 76)}px)`
}
```
The mark stays the dark Statement mark to the end (no switch back to lime). When `c` rests at 1 → done.
4. **done** — both flags are removed, the overlay is removed. Everything listening for the release (the hero's load sequence, the stream's entrance, the logo-open measurement) starts now.
### The backdrop
`ASSET_BASE_URL = https://storage.getlayers.ai/assets/relay-4ee0729d77`. A `pointer-events: none; position: absolute; inset: 0` layer at the top of `<main>`, behind everything, drawn in the frame's own coordinates (the parent is the page column, so `left: 0` is the column's left edge). `--hero-extra` is the hero's extra viewport height (`max(0px, 100svh − nav − hero frame)`; see the hero) — the top layers follow it down by half, the field texture by all of it.
**Top backdrop** (1440 × 1154 behind nav, hero and integrations) — a wrapper `position: absolute; left: 0; right: 0; top: 0; height: 72.125rem` (1154) holding, in order:
1. **Top light** — `position: absolute; top: 0; right: calc(50% − 50vw); left: calc(50% − 50vw)` (bled to the viewport), `height: 35rem` (560; 34.9375rem = 559 at ≤900), `background: linear-gradient(to bottom, rgba(255,255,255,.05), transparent)`.
2. **Dot field** — `<img src="{ASSET_BASE_URL}/backdrop/backdrop-dot-field.svg" width=1440 height=1060>` at `top: 0; left: 0; height: 66.25rem (1060); width: var(--page-width); max-width: none; transform: translateY(calc(var(--hero-extra) / 2))`, `fetchpriority="high"`. At ≤1200 it keeps its 1440 width (`width: 90rem`) and runs past the frame. Hidden at ≤900, replaced by
3. **Dot field 768** — `<img src="{ASSET_BASE_URL}/backdrop/backdrop-dot-field-768.svg" width=768 height=1154>` (re-pitched 25.6 × 44.3 grid, round 4px dots), shown only at ≤900: `top: 0; left: 0; height: 72.125rem; width: 48rem; max-width: none`, the same `translateY`. At 390 it simply runs past the frame.
4. **Specks and marks** — `<img src="{ASSET_BASE_URL}/backdrop/backdrop-specks-and-marks.svg" width=1440 height=1180>` at `top: 0; left: 0; height: 73.75rem (1180); width: var(--page-width); max-width: none`, the same `translateY`; `width: 90rem` at ≤1200; `height: 68.075rem` (1089.2 — squeezed to 12/13) at ≤900.
**Field texture** — `<img src="{ASSET_BASE_URL}/backdrop/field-texture.svg" width=1440 height=2400>` (a 48-dot grid with four marks, behind 06 Scale onwards): `position: absolute; top: 236.3125rem (3781); left: 0; height: 150rem (2400); width: var(--page-width); max-width: none; transform: translateY(var(--hero-extra)); opacity: 0.35`, lazy-loaded. Every frame squeezes it to its own width at the same y.
The base light and the lime base line the frames draw under Integrations (`rgba(255,255,255,.06)` peaking at 55 % and fading out on 768/390, the `rgba(200,247,81,.35)` line at y = 1153 / 1267 / 1220) are drawn by the Integrations section, not here.
## The WebGL scene — the page-long particle stream
A lime ribbon of glowing filaments sweeping in S-curves down the **whole page**, wrapped in sparks and out-of-focus bokeh: additive soft billboards whose size and softness grow away from the focal plane, then an UnrealBloom pass. It is one scene for the whole page: a **viewport-sized `position: fixed` canvas** (`top: 0; left: 0; width: 100%; height: 100lvh; pointer-events: none; mix-blend-mode: screen`) placed right after the backdrop and before every section in `<main>`. The renderer clears to **opaque black** (`alpha: false`) — under `screen` black adds nothing, so the bloom sits on the backdrop without an alpha pass, and glass surfaces (the hero window, the Product → Proof slab, the cards) frost it. The stream itself lives in **document coordinates**: its spine is baked into a float texture whenever the layout changes and the shaders map it through the current scroll every frame, so it scrolls with the page like any other layer and only the visible stretch costs fragments.
Three.js 0.186.1 by importmap: `three`, `three/addons/postprocessing/EffectComposer.js`, `RenderPass.js`, `UnrealBloomPass.js`, `OutputPass.js`.
### CONFIG (bake in exactly)
```js
const CONFIG = {
particles: 36000, // full tier budget; the low tier draws lowShare of it
lowShare: 0.45,
strands: 44,
flowPx: 90, // flow speed along the curve, design px / s (±35 % per particle)
scrollFlow: 1.25, // px along the curve per px scrolled, either way
spread: 8, // stream radius where it pinches, design px
spreadEnds: 190, // … and elsewhere
turnHold: 0.5, // radius also held under turnHold × the turn's radius
pinchReach: 300, // reach of a waypoint's pinch along the curve, design px
breath: 0.35, breathWavelength: 900, breathSpeed: 0.6, // a swell travelling down the stream
spraySpread: 2.4, bokehSpread: 3.2, dustSpread: 5.5, // × the local spread
wobble: 4, // per-particle wander, design px
size: 2.1, focal: 0.15, dof: 0.55, dofSize: 2.2, twinkle: 2.4,
brightness: 0.79, heat: 1.2,
altShare: 0.45, altStrands: 0.32, // share in the second (blue) hue
lineBase: 0.16, lineSpread: 0.9,
lineWavelength: 1560, lineWaveAmp: 0.22, lineWaveSpeed: 0.25,
pulseSpacing: 1130, pulseSpeed: 205, pulseGain: 1.4, pulseSharp: 9,
parallax: 0.22,
revealPx: 2600, revealMs: 2200, // the entrance
bloomStr: 0.68, bloomRadius: 0.57, bloomThresh: 0.07,
}
const SAMPLES = 2048 // spine samples (~6 CSS px apart on a 1440 page)
const SEGMENTS = 1400 // filament vertices per strand (~9 px segments)
const CAM_Z = 6, FOV = 35, DAMP = 3.2
const TURN_BASE = 12, TURN_HOLD = 28, TURN_SMOOTH = 48, TURN_OPEN = 1e4
const REVEALED = 1e7
const DPR = { full: 1.35, low: 1.2 }
const BLOOM_SCALE = { full: 1, low: 0.5 }
const HALF_H = CAM_Z * Math.tan(((FOV / 2) * Math.PI) / 180)
```
**Colour roles** — read from CSS at construction, never hard-coded in the shader: `uColMain = --stream-main (#c8f751)`, `uColAlt = --stream-alt (#52c8ff)`, `uColCore = --stream-core (#eaff9e)`, `uColDeep = --stream-deep (#3d5410)`, `uColSpark = --stream-spark (#ffffff)`.
### The path (document space)
Waypoints are anchored to the page's blocks by id: `x` is a share of the page column's width (0 = its left edge; may overshoot), `y` a share of the anchored block's height (the section itself, or for a section inside a sticky layer — Statement — the layer's flow wrapper, so a stuck section does not drag it). Every waypoint sits lower than the one before. `z`: + toward the camera (bigger, softer), − away. `pinch` 0…1: how tightly the strands pinch and how hot they burn there.
```js
const HERO = { // per frame: in from above top right, down-left behind the window, the hot pinch left of it
1440: [ { at:"hero", x:1.14, y:-0.46, z:-2.6 }, { at:"hero", x:0.84, y:0.02, z:-1.8 }, { at:"hero", x:0.5, y:0.3, z:-0.9 }, { at:"hero", x:0.22, y:0.66, z:0, pinch:1 } ],
1024: [ { at:"hero", x:1.08, y:-0.4, z:-2.6 }, { at:"hero", x:0.86, y:0.03, z:-1.8 }, { at:"hero", x:0.5, y:0.35, z:-0.9 }, { at:"hero", x:0.22, y:0.7, z:0, pinch:1 } ],
768: [ { at:"hero", x:1.08, y:0.12, z:-2.4 }, { at:"hero", x:0.8, y:0.4, z:-1.6 }, { at:"hero", x:0.3, y:0.6, z:-0.8 }, { at:"hero", x:-0.02, y:0.8, z:0, pinch:1 } ],
390: [ { at:"hero", x:1.1, y:0.2, z:-2.4 }, { at:"hero", x:0.75, y:0.45, z:-1.6 }, { at:"hero", x:0.25, y:0.65, z:-0.8 }, { at:"hero", x:-0.02, y:0.84, z:0, pinch:1 } ],
}
const TAIL = [ // shared by every frame, values before SHIFT
{ at:"integrations", x:0.48, y:-0.25, z:0 },
{ at:"integrations", x:0.605, y:0.54, z:-0.2 },
{ at:"product", x:0.5, y:0.12, z:0.2 },
{ at:"product", x:-0.06, y:0.5, z:0.2, pinch:1 },
{ at:"statement", x:0.5, y:0.3, z:-0.5 },
{ at:"scale", x:0.92, y:0.9, z:0.2, pinch:1 },
{ at:"signals", x:0.4, y:0.75, z:-0.4 },
{ at:"proof", x:-0.06, y:0.6, z:0.2, pinch:1 },
{ at:"pricing", x:0.46, y:-0.1, z:-0.2 },
{ at:"pricing", x:0.96, y:0.35, z:0.1, pinch:0.8 },
{ at:"pricing", x:0.48, y:0.85, z:0 },
{ at:"pricing", x:-0.49, y:1.04, z:0 },
]
const PATH = { 1440: [...HERO[1440], ...TAIL], 1024: [...HERO[1024], ...TAIL], 768: [...HERO[768], ...TAIL], 390: [...HERO[390], ...TAIL] }
const SHIFT = 200 / 1440 // the whole form sits this share of the column right of its waypoints
```
Resolve to document CSS px against the live layout: `x = column.left + (x + SHIFT) × column.width` (column = `main.getBoundingClientRect()`), `y = docTop(block) + y × block.offsetHeight` where `docTop` sums `offsetTop` up the `offsetParent` chain (immune to scroll-driven transforms) and `block` is the section or, if an ancestor below `<main>` is `position: sticky`, that ancestor's parent. Points whose section is missing are skipped. Re-resolve on every `ResizeObserver` change of `<main>` and on a frame change.
### Baking the spine
`CatmullRomCurve3(points, false, "centripetal")`, `length = max(1, curve.getLength())`. Sample `SAMPLES` points and tangents; `angle = atan2(tangent.y, tangent.x)`. Turn radius per sample (design px): `turn = |angle[i+12] − angle[i−12]|` (wrapped past π), `r = min(1e4, (2 × 12 × step) / max(1e-4, turn) / unit)` with `step = length / (SAMPLES − 1)`; then `held = slideMean(slideMean(slideMin(r, 28), 48), 48)` (a sliding min over ±28 samples, then two sliding means over ±48). Texture: a `DataTexture` `SAMPLES × 2`, RGBA float, nearest filtering — row 1 = `[−sin(angle), cos(angle), held, 0]`; row 0 = `[x, y, z, pinch]` where each pinched waypoint finds its nearest sample and `pinch(at) = max over peaks of peak.pinch × exp(−d²)`, `d = (at − peak.at) × length / (pinchReach × unit)`. Set `uLen = length`, `uFadeOut = min(length / 4, 400 × unit)`, `uFlow = flowPx × unit / length`. `unit` = root font-size / 16.
### Geometry
**Points** (36 000, seeded `mulberry32(0x5eed1e)`): kinds shuffled through the buffer — stream 79 % (kind 0), spray 14 % (1), bokeh 3 % (2), dust 4 % (3). Attributes: `aU = rand()`, `aSeed = rand()`, `aOff = (gauss × .5, gauss × .5)` with `gauss = rand+rand+rand+rand − 2`, `aSize`: stream `0.35 + rand⁴ × 2.4`, spray `0.3 + rand × 0.6`, bokeh `1.2 + rand × 1.6` with `position.z = 1.0 + rand × 1.4` (toward the camera), dust `0.25 + rand × 0.4` with `position.z = −1.2 + rand × 1.0`. `Points`, `frustumCulled = false`.
**Lines** (44 strands × 1400 segments, seeded `0xf11a`): per strand `nx = (rand − .5) × 2`, `nz = (rand − .5) × 1.2`, `phase = rand × 2π`, `end = 0.93 + rand × 0.07`; per vertex `aU = i / SEGMENTS`, `aStrand = [nx, nz, phase, end]`, a `position` attribute of zeros (required, unused), an index of consecutive pairs → `LineSegments`, `frustumCulled = false`.
Both materials: `ShaderMaterial`, `AdditiveBlending`, `transparent`, `depthTest: false`, `depthWrite: false`. Shared uniforms: `uCurve, uHalfH, uCamZ, uPx (1), uLen (1), uView (vec3 1,1,0), uTime, uTimeS, uTurnHold, uBreath, uBreathWave, uBreathSpeed, uRevealPx (0), uFadeIn (120), uFadeOut (400), uSpread, uSpreadEnds, uHeat, uBright, uColMain, uColAlt, uAltShare, uColCore, uColDeep, uColSpark`. Points add `uFlow 0, uScrollU 0, uSpray, uBokeh, uDust, uWobble, uSize, uScale 1, uFocal, uDof, uDofSize, uTwinkle`. Lines add `uPulseShift 0, uPulseSharp, uAltStrands, uLineSpread, uLineWavelength, uLineWaveAmp, uLineWaveSpeed, uPulseSpacing, uPulseGain, uLineBase`.
### Shaders (verbatim — `${SAMPLES}` = 2048, `${SAMPLES - 1}` = 2047)
Shared prelude, prepended to both vertex shaders:
```glsl
uniform sampler2D uCurve;
uniform float uHalfH, uCamZ, uPx, uLen;
uniform vec3 uView; // half viewport width, half height (CSS px), scrollY
vec4 curveRow(float u, float row) {
float x = clamp(u, 0.0, 1.0) * 2047.0;
float i = floor(x);
float v = (row + 0.5) / 2.0;
vec4 a = texture2D(uCurve, vec2((i + 0.5) / 2048.0, v));
vec4 b = texture2D(uCurve, vec2((min(i + 1.0, 2047.0) + 0.5) / 2048.0, v));
return mix(a, b, x - i);
}
// a point offset from the spine: o.x across it (design px), o.y in depth
vec3 onSpine(float u, vec2 o) {
vec4 p = curveRow(u, 0.0);
vec2 n = curveRow(u, 1.0).xy;
vec2 page = p.xy + n * o.x * uPx * uView.y;
return vec3(
(page.x - uView.x) / uView.y,
(uView.y - (page.y - uView.z)) / uView.y,
p.z + o.y * uPx
);
}
uniform float uTurnHold, uBreath, uBreathWave, uBreathSpeed, uTimeS;
// stream radius at u (design px): pinched, breathing, held under the turn
float widthAt(float u, float pinch, float narrow, float wide) {
float a = u * uLen;
float swell = 1.0 + uBreath * sin(a / uBreathWave * 6.2831 - uTimeS * uBreathSpeed);
float w = mix(wide, narrow, pinch) * swell;
// a soft min: a hard one kinks the outer strands where it takes over
float r = max(curveRow(u, 1.0).z * uTurnHold, 1.0);
return w * r / pow(pow(w, 4.0) + pow(r, 4.0), 0.25);
}
vec3 toWorld(vec3 s) {
float k = (uCamZ - s.z) / uCamZ;
return vec3(s.x * uHalfH * k, s.y * uHalfH * k, s.z);
}
```
Points vertex shader (after the prelude):
```glsl
attribute float aU;
attribute float aSeed;
attribute float aSize;
attribute float aKind;
attribute vec2 aOff;
uniform float uTime, uFlow, uScrollU, uRevealPx, uFadeIn, uFadeOut, uSpread,
uSpreadEnds, uSpray, uBokeh, uDust, uWobble, uSize, uScale, uFocal, uDof,
uDofSize, uTwinkle, uHeat, uAltShare;
uniform vec3 uColMain, uColAlt, uColCore, uColDeep, uColSpark;
varying vec3 vColor;
varying float vBlur;
void main() {
float ph = aSeed * 6.2831;
// stream (0) and spray (1) ride at speed; bokeh (2) and dust (3) drift
float speed = (aKind < 1.5 ? 1.0 : 0.3) * (0.65 + 0.7 * fract(aSeed * 7.13));
float u = fract(aU + uTime * uFlow * speed + uScrollU);
float pinch = curveRow(u, 0.0).w;
float spread = widthAt(u, pinch, uSpread, uSpreadEnds);
if (aKind > 2.5) spread *= uDust;
else if (aKind > 1.5) spread *= uBokeh;
else if (aKind > 0.5) spread *= uSpray;
vec2 o = aOff * spread
+ vec2(sin(uTime * 0.7 + ph), cos(uTime * 0.6 + ph * 1.3)) * uWobble;
vec3 s = onSpine(u, o);
s.z += position.z; // bokeh sits toward the camera, dust away from it
float a = u * uLen;
float fade = smoothstep(0.0, uFadeIn, a) * smoothstep(uLen, uLen - uFadeOut, a)
* smoothstep(uRevealPx, uRevealPx - 220.0, a);
float heat = pinch * (aKind < 1.5 ? 1.0 : 0.3);
// a share of the motes carry the second hue
vec3 hue = fract(aSeed * 3.77) < uAltShare ? uColAlt : uColMain;
vec3 base = mix(uColDeep, hue, 0.55 + 0.45 * fract(aSeed * 5.31));
vec4 mv = modelViewMatrix * vec4(toWorld(s), 1.0);
float depth = -mv.z;
float blur = clamp(abs(depth - (uCamZ - uFocal)) * uDof, 0.0, 1.0);
if (aKind > 1.5 && aKind < 2.5) blur = max(blur, 0.8);
vBlur = blur;
float tw = 0.55 + 0.45 * sin(uTime * uTwinkle + ph);
vec3 c = mix(base, uColCore, clamp(heat * 0.85, 0.0, 1.0)) * (1.0 + heat * uHeat);
if (fract(aSeed * 13.7) < 0.05) c = uColSpark * 1.3;
// a blurred mote spreads the same light over a bigger disc
float spreadOut = 1.0 + blur * uDofSize;
vColor = c * fade * tw / (spreadOut * spreadOut);
gl_PointSize = uSize * aSize * uScale * (uCamZ / depth) * spreadOut;
gl_Position = projectionMatrix * mv;
}
```
Points fragment shader:
```glsl
uniform float uBright;
varying vec3 vColor;
varying float vBlur;
void main() {
float r = length(gl_PointCoord - 0.5) * 2.0;
if (r > 1.0) discard;
// sharp glint in focus, soft rimmed disc out of it
float glint = exp(-r * r * 6.0);
float disc = smoothstep(1.0, 0.6, r) * 0.5
+ smoothstep(0.5, 0.92, r) * (1.0 - smoothstep(0.9, 1.0, r)) * 0.55;
gl_FragColor = vec4(vColor * mix(glint, disc, vBlur) * uBright, 1.0);
}
```
Lines vertex shader (after the prelude):
```glsl
attribute float aU;
attribute vec4 aStrand; // x normal offset, y depth offset, z phase, w end
uniform float uTime, uRevealPx, uFadeIn, uFadeOut, uSpread, uSpreadEnds,
uLineSpread, uLineWavelength, uLineWaveAmp, uLineWaveSpeed, uPulseGain,
uLineBase, uHeat, uAltStrands;
uniform vec3 uColMain, uColAlt, uColCore, uColDeep;
varying vec3 vColor;
varying vec3 vPulse;
varying float vA;
varying float vPhase;
void main() {
float u = aU;
float a = u * uLen;
float pinch = curveRow(u, 0.0).w;
float spread = widthAt(u, pinch, uSpread, uSpreadEnds) * uLineSpread;
float wave = sin(a / uLineWavelength * 6.2831 + aStrand.z + uTime * uLineWaveSpeed) * uLineWaveAmp;
vec3 s = onSpine(u, vec2(aStrand.x + wave, aStrand.y) * spread);
float fade = smoothstep(0.0, uFadeIn, a) * smoothstep(uLen * aStrand.w, uLen * aStrand.w - uFadeOut, a)
* smoothstep(uRevealPx, uRevealPx - 260.0, a);
// a share of the strands run in the second hue
vec3 hue = fract(aStrand.z * 1.618) < uAltStrands ? uColAlt : uColMain;
vec3 c = mix(mix(uColDeep, hue, 0.75), uColCore, pinch * 0.6);
vColor = c * fade * uLineBase * (1.0 + pinch * uHeat);
vPulse = c * fade * uPulseGain;
// the pulse is placed per fragment from the arc length — per vertex it
// hopped from vertex to vertex
vA = a;
vPhase = aStrand.z;
gl_Position = projectionMatrix * modelViewMatrix * vec4(toWorld(s), 1.0);
}
```
Lines fragment shader:
```glsl
uniform float uBright, uPulseSpacing, uPulseShift, uPulseSharp;
varying vec3 vColor;
varying vec3 vPulse;
varying float vA;
varying float vPhase;
void main() {
float p = fract((vA - uPulseShift) / uPulseSpacing + vPhase) - 0.5;
float pulse = exp(-p * p * uPulseSharp * uPulseSharp);
gl_FragColor = vec4((vColor + vPulse * pulse) * uBright, 1.0);
}
```
### Renderer, composer, tiering
`WebGLRenderer({ canvas, antialias: false, alpha: false, powerPreference: "high-performance" })`, clear `0x000000` opaque; `PerspectiveCamera(35, 1, 0.1, 60)` at `(0, 0, 6)`. `EffectComposer`: `RenderPass` → `UnrealBloomPass(new Vector2(1,1), 0.68, 0.57, 0.07)` → `OutputPass`.
Tier: `low` when `(hover: none) and (pointer: coarse)` matches or `innerWidth ≤ 900`, else `full`. `retune(tier)` touches only draw ranges and DPR: points `setDrawRange(0, floor(36000 × (low ? 0.45 : 1)))`, lines `setDrawRange(0, ceil(44 × (low ? 0.6 : 1)) × 1400 × 2)`. `resize(w, h)`: `dpr = min(devicePixelRatio, DPR[tier])` on renderer and composer, `setSize(w, h, false)`; on the low tier the bloom pass is re-sized to `ceil(w × dpr × 0.5) × ceil(h × dpr × 0.5)`; camera aspect; `unit = rootFontSize / 16`; `uScale = unit × dpr`; `uPx = unit / (h / 2)`; `uFadeIn = 120 × unit`; `uLineWavelength = 1560 × unit`, `uPulseSpacing = 1130 × unit`, `uBreathWave = 900 × unit`; then re-bake the curve. The canvas is `lvh`-tall and a resize is ignored unless its size or the tier actually changed (a phone's URL bar never re-allocates the framebuffer); a coarse-pointer media-query flip also re-checks the tier.
Build in steps with the main thread handed back between them (`requestIdleCallback` / `setTimeout 0`), then **prewarm**: `renderer.compileAsync(scene, camera)`, then compile every pass material (`bloom.materialHighPassFilter`, each of `bloom.separableBlurMaterials`, `bloom.compositeMaterial`, `bloom.blendMaterial`, `output.material`, `composer.copyPass.material`) on a throwaway scene of 2×2 plane meshes via `compileAsync`, then render one frame — so nothing compiles or allocates once the page is live.
### Per frame
```js
render(now) {
const dt = lastTime < 0 ? 0 : min(0.1, max(0, (now − lastTime) / 1000)); lastTime = now
look.lerp(pointer, 1 − exp(−3.2 × dt)) // pointer parallax, damped
camera.position.set(−look.x × 0.22, look.y × 0.22, 6); camera.lookAt(0, 0, 0)
const t = revealStart < 0 ? 0 : min(1, (now − revealStart) / 2200)
uRevealPx = t ≥ 1 ? 1e7 : easeOutCubic(t) × 2600 × unit // easeOut = 1 − (1 − t)³
uTime = now / 1000; uTimeS = (now / 1000) % 1000
uView = (width / 2, height / 2, scrollY)
uScrollU = (scrollPx / uLen) % 1
const spacing = 1130 × unit
uPulseShift = ((now / 1000) × 205 × unit + scrollPx) % spacing
composer.render(dt)
}
setScroll(y) { scrollPx += |y − lastScroll| × 1.25; lastScroll = y; scrollY = y } // read from Lenis's scroll number
setPointer(x, y) // −1…1 from the viewport centre, mouse pointers only, fine pointers only
```
The loop runs through the shared ticker only while the tab is visible, reduced motion is off and the preloader has fully closed (nothing renders — and the entrance waits — while `html[data-preload]` or `html[data-preload-closing]` is set); on a coarse pointer it draws every other frame (`FRAME_GAP.phone = 20` ms, otherwise 0). **Entrance**: once released and the canvas is seen, 416 ms later (`LOAD.stream`, with the hero's first node) `reveal(now)` sets `revealStart = now` and the head draws itself 2600 design px down the curve over 2200 ms. **Reduced motion**: no loop — one still frame at `STILL_MS = 24000` with `reveal(24000, instant)` (`revealStart = now − 2200`), redrawn on scroll and resize. A crawler UA never loads three.js. If WebGL is unavailable the page stands without the stream.
## The transitions between blocks
Sections stay unaware of their neighbours; the hand-offs are wrappers placed in the composition (motion brief t1–t8):
| wrapper | around | behaviour |
|---|---|---|
| ScrollBlur `leave` + `still` | Integrations | fades once its bottom passes the viewport centre; no drift (its edge lines up with the backdrop) |
| ScrollBlur `enter` | Product | fades/drifts in from below |
| LogoOpen | Statement | the lime plate opens out of the Relay logo and folds back into it, scrubbed by scroll |
| ScrollBlur `leave` | Scale | |
| ScrollBlur `enter` + `leave` | Signals | |
| ScrollBlur `enter` + `leave` | Proof | |
| — | Pricing → Footer | plain scroll (the cards simply sit there, only their contents animate in) |
### ScrollBlur — "мягкая передача"
A wrapper `<div>` whose inner `<div>` carries one `SPRING` (280 / 32) spring set `{ opacity: 1, y: 0 }` written as `opacity` + `translate3d(0, y, 0)`. Constants: `DRIFT = 48` px (0 with `still`), `ENTER_FROM = 0.6`, `LEAVE_TO = 0.25`. No blur any more (client call). Its document offset and height are measured once (`getBoundingClientRect().top + scrollY`, `height`, `innerHeight`) and again only on a ResizeObserver change of the wrapper or `<main>`, or on resize — never per frame. While the wrapper intersects the viewport a ticker callback runs each frame:
```js
const top = docTop − scrollY, bottom = top + height // scrollY = Lenis's number
let opacity = 1, y = 0
if (enter) { const p = clamp((vh − top) / (vh / 2)); opacity *= mix(0.6, 1, p); y += mix(drift, 0, p) } // 0 with the top at the viewport bottom, 1 at the centre
if (leave) { const p = clamp((vh / 2 − bottom) / (vh / 2)); opacity *= mix(1, 0.25, p); y += mix(0, −drift, p) } // 0 with the bottom at the centre, 1 at the top
if (opacity × 1e4 + y changed) spring.start({ opacity, y })
```
When the wrapper leaves the viewport the loop stops after one last settle. Reduced motion: no wrapper motion at all.
### LogoOpen — the iris through 05 Statement, "из лого в лого"
Constants (design px of scroll, × `k` = root font-size / 16): `PIN = 240` (the plate stays pinned for a short reading hold — 15rem in the wrapper's height), `OPEN = 300`, `CLOSE = 320`, `CLOSE_LEAD = 0`, `POP = 60`, `FLIGHT = 300`, `TILE_ROUND = 24 / 76`, `smooth(t) = t·t·(3 − 2t)`.
Markup: a wrapper `position: relative; height: calc(34.5rem + 15rem)` (the plate's 552 + the pin; `calc(33rem + 15rem)` at ≤1200, `calc(23.1875rem + 15rem)` at ≤580) with `data-open="false"`; inside it the sticky plate `div.logo-open` — `position: sticky; top: calc(50lvh − 16.875rem)` — containing the Statement section and, after it, `span.logo-open-mark` (the dark Statement mark tile with the chevron glyph — `border-radius 31.58%`, 1px `rgba(10,11,9,.25)`, `linear-gradient(135deg, #252721, #0a0b09 71.429%)`, the glyph `statement/statement-mark-glyph.svg` at `top 24.3% left 28.25% w 44.1% h 48.76%`). A fixed flight element (`position: fixed; top: 0; left: 0; z-index: 30; pointer-events: none`, hidden) holds the same tile plus a cover layer with `background: url(scale/scale-runtime.svg) cover`.
The plate's CSS reads five custom properties: `--pop`, `--grow`, `--flying` (0…1), `--shift` (0px), and the plate's own Mark `--mx`, `--my`, `--msize`; it starts with `clip-path: inset(50%)` (invisible until written). The mark tile: `left: calc(var(--mx) − var(--msize)/2); top: calc(var(--my) − var(--msize)/2); width/height: var(--msize); opacity: calc(min(var(--pop), clamp(0, calc(1 − (var(--grow) − 0.06) / 0.12), 1)) × (1 − var(--flying))); transform: translateY(var(--shift)) scale(calc(0.6 + var(--pop) × 0.4))`.
Measured on resize (and once released / fonts ready): `k`; `plateRound` = the section root's `border-top-left-radius`; the plate's own Mark (`[data-statement-mark]`, 76 × 76 at 1440): `markSize = offsetWidth`, `mx = offsetLeft + markSize/2`, `my = offsetTop + offsetHeight/2` → written as `--mx/--my/--msize`.
Updated **in the scroll event** (layout is clean there), reads first, writes after:
```js
const vh = innerHeight, plateH = plate.offsetHeight, plateW = plate.offsetWidth
const wrapTop = wrap.getBoundingClientRect().top
const to = target.getBoundingClientRect() // target = the Scale section's [data-logo-target] runtime tile (96 × 96)
const from = mark.getBoundingClientRect()
const pinTop = (vh − plateH) / 2
const s = pinTop − wrapTop // scroll since the plate pinned (negative while it scrolls in)
const seenAt = pinTop + my − vh // the logo starts as soon as the mark comes up over the bottom edge
const openEnd = seenAt + POP·k + OPEN·k
const closeStart = (PIN − CLOSE_LEAD)·k, closeEnd = closeStart + CLOSE·k
const pop = smooth(clamp((s − seenAt) / (POP·k)))
const opening = clamp((s − seenAt − POP·k) / (OPEN·k))
const closing = clamp((closeEnd − s) / (CLOSE·k))
--pop = pop; --shift = 0px
const grow = smooth(min(opening, closing)); --grow = grow
plate.style.clipPath = fold(grow, plateW, plateH, mx, my, markSize, plateRound, linear)
if (s ≥ openEnd) data-open = "true"; if (s < seenAt) data-open = "false"
// flight: from the logo where the plate folded to the runtime tile, only once the fold is complete
const t = clamp((s − closeEnd) / (FLIGHT·k)), flying = t > 0 && t < 1
if (s > closeEnd) data-open = "false"
--flying = t > 0 ? 1 : 0; target.style.opacity = flying ? 0 : ""; flight.hidden = !flying
if (flying) { const e = smooth(t)
size = from.width + (to.width − from.width)·e; x = from.left + (to.left − from.left)·e; y = from.top + (to.top − from.top)·e
flight.style.transform = `translate(${x}px, ${y}px)`; flight width = height = size; runtimeCover.opacity = clamp((e − 0.35) / 0.5) }
```
`data-open="true"` on the wrapper is what the Statement plate reads to play its own timeline (see the section). Reduced motion: `data-reduced="true"`, `data-open="true"`, `--grow: 1`, `clip-path: none`, no flight.
**The fold** (`fold.ts`, verbatim) — the plate's clip at `g` (0 = folded, 1 = open), in two stages so it always folds INTO the logo tile, square-first: `g 1 → 0.5` the plate narrows into the largest square centred on the logo that fits inside it, its corners rounding up like the tile's; `g 0.5 → 0` that square shrinks onto the tile. Opening plays the same path backwards; at 0 nothing shows. LogoOpen passes a **linear** `curve` (the ease is on `grow`; a per-stage ease stalled the motion at the square):
```js
const foldBox = (g, w, h, cx, cy, tile, plateRound, curve = smooth) => {
const side0 = Math.max(tile, 2 * Math.min(cx, w - cx, cy, h - cy)) // the square stays whole inside the plate
if (g < 0.5) {
const t = curve(Math.max(g, 0) / 0.5)
const half = (tile + (side0 - tile) * t) / 2
return { top: cy - half, left: cx - half, bottom: h - (cy + half), right: w - (cx + half), round: half * 2 * TILE_ROUND }
}
const t = curve(Math.min(g, 1) * 2 - 1)
const half = side0 / 2
return { top: (cy - half) * (1 - t), right: (w - (cx + half)) * (1 - t), bottom: (h - (cy + half)) * (1 - t), left: (cx - half) * (1 - t),
round: side0 * TILE_ROUND * (1 - t) + plateRound * t }
}
const fold = (g, w, h, cx, cy, tile, plateRound, curve) => {
if (g <= 0) return "inset(50%)"
const { top, right, bottom, left, round } = foldBox(g, w, h, cx, cy, tile, plateRound, curve)
return `inset(${top}px ${right}px ${bottom}px ${left}px round ${round}px)`
}
```
### The frosted slab
Product, Statement, Scale, Signals and Proof sit on one glass slab (not in Figma) so the stream underneath reads through it frosted: an absolutely positioned layer behind that run of blocks, `top: 0; bottom: 0; left: calc(50% − 50vw); right: calc(50% − 50vw)` (bled to the viewport), `background: rgba(8, 9, 10, 0.22)`, 1px top and bottom borders `rgba(255,255,255,.06)`, `backdrop-filter: blur(12px)`, `pointer-events: none`. Cards on the slab carry no backdrop-filter of their own (the slab already frosts the stream); the hero window and chips, the Integrations tiles, the pricing cards and the nav keep theirs.
### The cursor ring
The native cursor stays. On `(hover: hover) and (pointer: fine)` only, and never under reduced motion, a `position: fixed; z-index: 50; pointer-events: none` ring — 1px `#c8f751` border, fully rounded, with an inner lime fill layer — glides after the pointer on a soft spring `FOLLOW = { tension: 210, friction: 24, mass: 1 }` (`x`, `y` from the pointer event straight into the spring; the first move places it immediately; the pointer leaving the document parks it at `(−200, −200)` immediately). Shape springs on `{ tension: 400, friction: 30 }`: size `28` → `44` over anything interactive (`a, button, input, textarea, select, label, [role='button'], [role='radio'], [role='slider']`), the fill's opacity `0` → `0.12` over the same, scale `1` → `0.82` while pressed, and `0` over anything inside `[data-cursor="none"]` (the lime CTAs have their own hover story). Transform: `translate3d(x, y, 0) translate(−50%, −50%) scale(press)`.
### 01 Navigation — `src/views/home/sections/nav/` (Figma 3888:4858)
A fixed bar at the top of the viewport (`position: fixed; inset-inline: 0; top: 0; z-index: 40`), always visible. Behind it a **plate** — `position: absolute; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(12px)` — whose opacity is a spring (tension 280, friction 32) that goes 0 → 1 the moment `scrollY` passes the nav's own height (88 px at 1440/1024, 64 at 768, 56 at 390) and back to 0 above it. At the very top the nav is transparent over the backdrop. The page keeps a spacer of the same height in the flow after the nav (88 / 64 / 56) so the hero starts below it.
The header is one row, `display: flex; align-items: center; justify-content: space-between`, 88 px tall, `width: var(--page-width)` (1440 / 1024 / 768 / 390 frame widths in rem), centred, `padding: 0 80px` (48 at 1024, 32 at 768, 20 at 390). It fades in as one block (opacity 0 → 1, soft spring tension 120 / friction 26) once the preloader releases the page.
**Brand** (left) — `<a href="/">` flex, gap 12: the logo mark `nav/nav-logo-mark.svg` at 28 × 28 (a 28 px rounded-9 tile with a lime linear gradient and a shine) and the wordmark **Relay** — Inter 20/28 weight 500, letter-spacing −1px, white. 768: 18 px. 390: 16/24.
**Links** (centre, the `space-between` puts them at x 413 on 1440, 217 on 1024, 205.5 on 768) — `<nav aria-label="Primary"><ul>` flex, gap 40 (32 at 1024, 24 at 768), Inter 14/20 in `#9ba0a6`, `white-space: nowrap`; hover → `#c8f751` with a 150 ms `cubic-bezier(0.2,0,0,1)` colour transition. Copy and hrefs, verbatim: **Product** `#product` · **How it works** `#how-it-works` · **Customers** `#customers` · **Pricing** `#pricing`. Hidden at 390.
**Actions** (right) — flex, gap 12 (8 at 390):
- the **status pill**: `<p>` flex, height 44, padding 0 16, gap 8, `border-radius: 9999px`, `border: 1px solid rgba(255,255,255,0.22)`, `background: rgba(7,7,7,0.55)`, `box-shadow: 0 8px 22px -4px rgba(0,0,0,0.5)`, `overflow: clip`; inside it the **breathing dot** — a 6 × 6 wrapper with two absolutely-filled lime (`#c8f751`) circles: the *dot* scales 1 → 1.35 → 1 → 1 in three linear 600 ms legs, while the *halo* scales 1 → 3 and fades 0.8 → 0 over 1800 ms then snaps back; both loop forever (every 1.8 s), never under reduced motion. Then the text **all systems normal** in Fragment Mono 14/20, white, nowrap. Hidden at ≤ 768.
- the **Start free** lime CTA — the shared lime pill, size md (40 tall, padding 0 24, Inter 14/20 weight 500, `#0a0b09` on `#c8f751`, `drop-shadow(0 8px 11px rgba(0,0,0,0.5))`), href `#start`, with the "handoff" hover described in the shared components.
- the **menu button**, 390 only: a 40 × 40 round glass button (`border: 1px solid rgba(255,255,255,0.1)`, `background: rgba(255,255,255,0.04)`) with three 2 px bars in `rgba(255,255,255,0.9)`, rounded: bar 1 at (11, 13) 16 wide, bar 2 at (11, 19) 16 wide, bar 3 at (14, 24) 10 wide. `aria-expanded`, label "Open menu" / "Close menu".
**The 390 menu card** (not in Figma; user-picked variant 4): an `<nav aria-label="Primary">` absolutely placed at `left: 8px; right: 8px; top: 8px; z-index: -1` inside the header (so it grows out from behind the brand and the button), `transform-origin: top right`, `border-radius: 24px`, `border: 1px solid rgba(255,255,255,0.1)`, `background: #131416`, `padding: 48px 8px 8px`. One row per link (`<ul>` column, gap 2): an `<a>` flex row, gap 12, `border-radius: 14px`, padding 10, hover/focus `background: rgba(255,255,255,0.04)`; a 36 × 36 **tile** (`border-radius: 11px`, `border: 1px solid rgba(255,255,255,0.1)`, Fragment Mono 14/20 `#9ba0a6`; on row hover: border `rgba(200,247,81,0.5)`, background `rgba(200,247,81,0.1)`, text `#c8f751`, all 150 ms) holding the code, then a column with the label (Inter 16/24 weight 500 white) and a note (14/20 `#9ba0a6`). Rows, verbatim: **PR** Product — *Canvas, approvals, run history* · **HW** How it works — *From trigger to answer* · **CU** Customers — *Numbers we did not explain away* · **$** Pricing — *Pay for runs, not seats*.
Menu motion, one spring `t` (tension 280 / friction 32) 0 ↔ 1: bar 1 `translateY(t·6px) rotate(t·45deg)`, bar 2 `opacity: 1 − t`, bar 3 `translateY(−t·5px) rotate(−t·45deg) scaleX(1 + 0.6t)`; the card `opacity: t`, `scale(0.94 + 0.06t)`, `visibility: hidden` below t 0.01. The rows are a **trail**: row 0 springs to `t` 60 ms after the card opens (no delay on close) and each next row springs toward the previous row's live value; a row renders `opacity: t` and `translateY((1 − t)·8px)`. The menu closes on Escape, on a pointerdown outside the card and button, on any link click, and whenever the layout leaves the 390 frame.
### 02 Hero — `src/views/home/sections/hero/` (Figma 3488:21055)
`<section id="hero">` directly under the nav slot. Its height is `calc(var(--hero-frame) + var(--hero-extra))` where the **frame** is 712 px (1440 and 1024), 976 (768), 949 (390) and `--hero-extra: max(0px, 100svh − nav − frame)` (nav 88 / 64 / 56), so the first screen fills the viewport and the frame's own layout is centred vertically inside it: an inner `position: absolute; inset-inline: 0; top: calc(var(--hero-extra)/2); height: var(--hero-frame)` box holds everything below. Coordinates are frame-relative, in px at 1440 (convert to rem ÷ 16 so the adaptive grid scales them).
Every number in this section is Figma's; the section is drawn four times (1440 3488:21055, 1024 3603:2145, 768 3832:6548, 390 3893:2141). All copy verbatim from `src/data/mocks/home/hero.ts`.
#### Tokens (hero.css, Tier 1 → Tier 2)
`--hero-ink #f4f5f6` · `--hero-chip-border rgba(200,247,81,.45)` · `--hero-chip-fill rgba(255,255,255,.04)` · `--hero-tick rgba(255,255,255,.14)` · `--hero-window-surface rgba(12,13,14,.5)` with `--hero-window-frost 24px` · the window's stroke ring `--hero-window-ring: linear-gradient(to bottom right, rgba(200,247,81,.5) 0%, rgba(207,248,103,.4462) 7.5%, rgba(214,249,124,.3925) 15%, rgba(221,250,146,.3387) 22.5%, rgba(228,251,168,.285) 30%, rgba(234,252,190,.2313) 37.5%, rgba(241,253,212,.1775) 45%, rgba(248,254,233,.1238) 52.5%, rgba(255,255,255,.07) 60%, rgba(255,255,255,.07) 100%)` painted `border-box` and masked to a 1 px ring (`mask: linear-gradient(#000 0 0) padding-box exclude, linear-gradient(#000 0 0)`) · `--hero-rule rgba(255,255,255,.07)` · `--hero-bar-divider rgba(255,255,255,.16)` · `--hero-corner-dot rgba(255,255,255,.2)` · tools: fill `rgba(255,255,255,.05)` border `.1`, active fill `.1` border `.18` · zoom: fill `rgba(255,255,255,.06)` border `.12` · cards (`--hero-node`, `--hero-wait-chip`, `--hero-label`): `rgba(22,23,26,.5)` with `--hero-card-frost 12px`, border `rgba(255,255,255,.1)` · run dots live `#c8f751` / idle `#9ba0a6` · wait dot `#747980` · connectors lime `#c8f751`, grey `rgba(135,135,135,.6)` (`.45` at ≤ 768) · packet `#c8f751` halo `rgba(200,247,81,.28)`, grey packet `#9ba0a6` halo `rgba(155,160,166,.22)` · node glow `0 0 16px 0 rgba(214,248,76,.18), 0 10px 22px -4px rgba(0,0,0,.55)`; the other cards `0 8px 22px -4px rgba(0,0,0,.5)` · radii: wait chip / branch label 6, release chip / zoom 8, tool 10, node 16, window 28 · title: 72/84 with a 78 serif accent (1024: 44/52, accent 48; 768: 36/46, accent 40; 390: 28/38, accent 32) · the 390 note 12 / 1.3.
#### Left column (all `position: absolute`)
| element | 1440 | 1024 | 768 | 390 |
|---|---|---|---|---|
| release chip wrapper | (80, 122) | (48, 162) | (231.5, 40) | (42, 32) |
| h1 wrapper | (80, 194) w 580, h 168 | (48, 226) w 380, h 104 | (176.5, 100) w 416, h 46, centred | (20, 92) w 350, h 38 |
| subhead | (80, 402) w 471, 18/28 | (48, 354) w 380 | (144, 158) w 481, centred | (31, 142) w 328, 16/24 |
| actions row | (80, 498) gap 12 | (48, 442) | (188, 238) | (20, 230) w 350, column, full-width pills |
| note | (80, 570) | (48, 510) | (181, 302) | (86.5, 352) w 217, centred |
| tick 1 × 8 `--hero-tick` | (1180, 620) | (975, 546) | (727, 546) | (369, 546) |
| field marks `hero/hero-field-mark.svg` 12 × 12 | (880, 672) and (1290, 668) | hidden | hidden | hidden |
- **Release chip**: `<p>` flex, height 36, gap 12, `border-radius: 8px`, 1 px `--hero-chip-border`, `--hero-chip-fill`, `backdrop-filter: blur(4px)`, padding 7 13 7 11 (right 15 at ≤ 1024), `overflow: clip`; a 6 × 6 **square** lime dot, then **open beta / 240 apps connected** — Fragment Mono 14/20 in `#f4f5f6`, typed in (see motion).
- **h1** (DisplayTitle): **The handoffs run** *themselves*. — Inter 600, white → `#bababa` vertical gradient clipped to the text, letter-spacing −2px; *themselves* in Instrument Serif italic 400, `#d6dadd`, `line-height: 1` on the same baseline.
- **Subhead**: *Relay runs the steps between your apps and only pings a person when it matters.* in `#9ba0a6`.
- **Actions**: lime pill **Start free** (`#start`, size lg: 48 tall, padding 0 24, 16/24) and the glass pill **Watch a build, 2 minutes** (`#watch`, same size) with `hero/hero-play-icon.svg` 16 × 16 before the label (gap 8).
- **Note** (Fragment Mono 14/20 `#9ba0a6`, nowrap): **free for 14 days / no card / no call with sales**. At 1024 it is two lines, *free for 14 days / no card / no call with* + *sales*; at 390 two centred 12 px lines, *free for 14 days / no card /* + *no call with sales*.
#### The product window group (3542:24364)
A `position: absolute; inset: 0; pointer-events: none` **tilt group** with `transform-origin: 76.3889% 50%` (768: `50% 66.5%`, 390: `50% 71.6%`). Inside, the window and its layers:
- **Bloom** `hero/hero-window-bloom.webp` (1400 × 1020, a lime ellipse with 200 px padding) at (440, −160) 1400 × 1020, unclipped — it reaches up into the nav. 1024: (236, −95) 965 × 903; 768: (−246, −205) 1260 × 1250; 390: (−78, −245) w 546. It is its own parallax layer (depth 60).
- **Window** at (760, 44) 680 × 624, `overflow: clip`, `border-radius: 28px 0 0 28px` (all four corners at ≤ 768), 1 px transparent border, `background: rgba(12,13,14,.5)`, `backdrop-filter: blur(24px)`. 1024: (508, 27) 516 × 659; 768: (126, 354) 516 × 590; 390: (20, 422) 350 × 515. Over it, same box, the **gradient stroke ring**. Positions below are window padding-box px (Figma's minus the 1 px border).
- **Canvas grid** `hero/hero-canvas-grid.svg` (680 × 624: 812 dots 2 × 1.75 on a 24 × 21.03 pitch, `#A6A6A6` @ .14 in a .35 group) at (−1, −1) 680 × 624 (1024: 516 × 659; 768: y −36, h 590; 390: y −73, w 350). Parallax depth 10.
- **Chrome** (fades in together): corner dots 4 × 4 `--hero-corner-dot` at (15, 15) and (15, 603) (1024 instead: one at (499, −1); 390 at (333, −1)); the title **Refunds over 5000** Inter 16/24 `#9ba0a6` at (39, 39) (1024: (23, 23); 390: x 19); a 1 × 14 divider `--hero-bar-divider` at (203, 44) (1024: (187, 28); hidden 390); **3 steps / 1 branch** Fragment Mono 14/20 `#f4f5f6` at (223, 41) (1024: (203, 25); hidden 390); a full-width 1 px rule at y 87, x −1 (1024: y 63); the **tool column** at (39, 111) — four 36 × 36 tiles in a column, gap 8 (1024: a row at (23, 76); 390 x 19), each `border-radius: 10px`, 1 px border, icon 20 × 20: `hero/hero-tool-select.svg` (active: border `.18`, fill `.1`), `hero-tool-add.svg`, `hero-tool-redirect.svg`, `hero-tool-extra.svg`; the **zoom** chip **100%** at (581, 111) (1024: (432, 76); 390: x 270): Fragment Mono 14/20 `#9ba0a6`, `border-radius: 8px`, padding 5 11 (7 at ≤ 1024), 1 px `--hero-zoom-border`, `--hero-zoom-fill`.
- **running** — Fragment Mono 14/20 `#c8f751`, right-aligned in a 160-wide box at (479, 41) (1024: (331, 25); 390: x 169). Arrives at 896 ms.
- **Flow** layer (parallax depth 22). At 390 the whole flow is the 768 flow scaled 310/380 = 0.8158 about (67, 130) and shifted 48 px left (`translate: −48px 0; scale: .8158`).
- Two **connector** groups (drawn inline SVG, 2 px round-capped strokes). 1440: 80 × 180 boxes at (89, 189) (mirrored, the fan **out of** Refund created) and (379, 189) (**into** Ask finance); paths `M0 90C40 90 40 10 80 10` (lime, gradient .25 → .95 toward the shared port: x 80 → 0 on the mirrored one) + `M0 90C40 90 40 170 80 170` (grey) on the left group, `M0 10C40 10 40 90 80 90` (lime, .25 at x 0 → .95 at x 80) + `M0 170C40 170 40 90 80 90` (grey) on the right. ≤ 1024 the fan is turned 90° into a vertical fan `w × (stem + edge)` — 1024: w 228, stem 16, edge 52, control drops 29 / 23; 768/390: w 208, stem 14, edge 50, drops 30 / 20 — placed at (143, 189) 228 × 68 (mirrored, top) and (143, 317) (bottom) on 1024; (153, 190) 208 × 64 and (153, 314) on 768. Mirrored group: a grey stem `M m 0 V stem` then two edges from the stem foot `M m stem C m stem+c1, x stem+c2, x h` for x = 0 (lime, gradient .3 at the stem → .95 at the node) and x = w (grey). The other group: edges up `M x 0 C x c1, m c2, m edge` for x = 0 (lime) and x = w (grey), then the stem `M m edge V h`.
- **Nodes** (WindowNode: 76-tall cards, `border-radius: 16px`, 1 px `rgba(255,255,255,.1)`, `rgba(22,23,26,.5)` + `blur(12px)`, padding 13 19, column gap 4, nowrap; title Inter 16/24 weight 500 white, source Fragment Mono 14/20 `#747980`; ≤ 1024: centred, gap 0, padding 11 15, title 14/20; ≤ 768 padding-block 9): **Amount over 5000** / *condition* at (169, 162) w 210 with the lime glow shadow; **Log to warehouse** / *postgres* at (169, 322) w 210; **Ask finance** / *waiting on a yes* at (459, 242) w 180. 1024: (49, 257) w 188 · (277, 257) w 188 · (163, 385) w 188. 768: (67, 254) w 172 · (275, 254) w 172 · (171, 378) w 172.
- The **wait chip** **26 min** at (459, 328) (1024: (212, 461); 768: y 454): flex gap 8, `border-radius: 6px`, 1 px `--hero-zoom-border`, `rgba(22,23,26,.5)` + blur 12, padding 3 11; a 6 px `#747980` dot, Fragment Mono 14/20 `#9ba0a6`.
- **Branch labels** **true** at (104, 216) and **else** at (104, 314) (1024: (174, 208) and (288, 208); 768: (170, 207) and (291, 207)): `border-radius: 6px`, 1 px `rgba(255,255,255,.1)`, card glass, padding 3 7, Fragment Mono 14/20 `#9ba0a6`.
- **Ports** `hero/hero-port.svg` 8 × 8 (a 6 px `#F4F5F6` disc with a 2 px `#070707` stroke) at (165, 195), (165, 355), (375, 195), (375, 355), (455, 275); hidden at 1024; at 768 at (149, 310), (357, 310), (149, 250), (357, 250), (253, 374). **Port flashes** — 8 px lime discs, opacity 0 — sit on the branch in-ports and Ask finance's port: (375, 195) *condition*, (375, 355) *log*, (455, 275) *approval*; 1024: (139, 253), (367, 253), (253, 381); 768: (149, 250), (357, 250), (253, 374).
- The **packets** SVG covers the window padding box (`viewBox 0 0 678 622` at 1440; `0 0 514 657` 1024; `0 0 514 588` 768; `0 0 348 513` 390, `preserveAspectRatio: none`, overflow visible): a lime packet (`r 6` halo `rgba(200,247,81,.28)` + `r 2.5` `#c8f751`) and a grey one (`r 5` `rgba(155,160,166,.22)` + `r 2.25` `#9ba0a6`).
- **Last runs**: a 1 px rule at (39, 441) w 600 (1024: (23, 505) w 468; 768 y 500; 390: (19, 435) w 310); the label **last runs** Fragment Mono 14/20 `#747980` at (39, 461) (1024: (23, 522); 768 y 513; 390: (19, 448)); the list at (39, 494) 600 × 89 (1024: (23, 558) 468 × 76; 768: y 545, h 20 — only the newest row shows; 390: (19, 480) w 310), Fragment Mono 14/20, rows 21 tall (20 at ≤ 1024) at y 0 / 34 / 68 (0 / 28 / 56 at ≤ 1024): a 6 px dot at (0, 8) (live `#c8f751`, idle `#9ba0a6`; y 7 at ≤ 1024), the time at x 20 in `#747980` (x 18 at ≤ 1024), the name at x 110 in `#9ba0a6` (x 100), the duration right-aligned in `#747980`. Rows, verbatim: **09:41:02 · refunds over 5000 · 129 ms** (live) · **09:38:57 · weekly export · 2.1 s** · **09:31:14 · invoice reminder · 410 ms**.
- **Refund created** / *stripe* — the trigger node, outside the window on the same depth-22 layer, at (650, 288) w 200 (1024: (672, 155) w 188; 768: (298, 485) w 172, h 60, padding-top 11; 390: (125, 553), scaled .8158 from its top-left). Its out-port flash + port at (846, 320) (1024: (762, 213); 768: (380, 541); 390: (192, 599), 7 × 7) and the overhang port `hero/hero-port-overhang.svg` at (646, 321) (1440 only).
#### Motion
**Cursor tilt** ("3D-наклон", fine pointers only, off under reduced motion): the pointer over `#hero` becomes a vector from the window centre — anchor (1100/1440, 0.5) at 1440 and 1024, (0.5, 649/976) at 768, (0.5, 679.5/949) at 390 — divided by half the section's width / height and clamped to length 1, rounded to 3 decimals, fed to a spring `{x, y}` (tension 120, friction 26) that returns to 0 when the pointer leaves the section, the document or the window loses focus. The tilt group renders `perspective(1400px) rotateX(−y·12deg) rotateY(x·12deg)` (`none` at rest); each parallax layer renders `translate3d(x·d px, y·d px, 0)` with d = 10 (grid), 22 (flow, and the Refund created layer), 60 (bloom).
**Load sequence** ("Загрузка системы", ~1.45 s, ms after the hero is on screen once the preloader has released it; STEP = 70):
| what | at | spring |
|---|---|---|
| chip rises (opacity 0→1, y 24→0) | 0 | tension 280 / friction 32 |
| chip text types in, 30 ms/char | 120 | linear |
| h1 rises, and comes out of `blur(10px)` behind a left→right mask sweep (`mask-image: linear-gradient(90deg, black calc(var(--reveal) − 40%), transparent var(--reveal))`, `--reveal` 0% → 140%) | 70 | rise 280/32; focus 120/26 |
| subhead fades in | 140 | 120/26 |
| actions rise | 210 | 280/32 |
| note rises, then types in at 30 ms/char | 280, type at 400 (the second 1024 line at 1660, the second 390 line at 1270) | 280/32; linear |
| canvas grid: a radial mask `radial-gradient(circle at 50% 50%, black r·480px, transparent r·480+120px)` with r −0.25 → 1 (mask removed at 1) | 192 | 120/26 |
| window chrome, the runs rule + label fade in | 288 | 120/26 |
| nodes: opacity 0→1, y 8→0, in flow order | 416 (Refund created), 472 (Amount), 528 (Log), 584 (Ask finance) | 280/32 |
| wait chip fades in | 654 | 120/26 |
| the overhang port pops in (opacity 0→1, scale 0.4→1) | 416 | 120/26 |
| Refund created's out-port pops in; the left connector fan draws (stroke-dashoffset 1→0 with `pathLength 1`, invisible until it starts) | 608 | 120/26 |
| the right fan draws; branch labels fade; the four in/out ports pop in | 688 | 120/26 |
| Ask finance's port pops in; **running** fades in | 896 | 120/26 |
| the three run rows fade in | 800, 870, 940 | 120/26 |
| the particle stream starts drawing itself in | 416 | — |
| first packet leaves Refund created | 944 | — |
**Live run** ("Живой прогон", while the window is in view, never under reduced motion): from t = 944 ms a packet makes a 720 ms trip every 1920 ms; every third trip (k mod 3 = 2) takes the grey branch through Log to warehouse, the others the lime branch through Amount over 5000. Route, in window padding-box px at 1440, as cubic segments: `(89,279) → c(129,279) c(129,y) (169,y)`, a straight run to `(379,y)`, then `c(419,y) c(419,279) (459,279)` with y = 199 (lime) or 359 (grey). ≤ 1024 the same trip turned vertical: down a stem from `(cx, y0)`, an edge into column x, straight through the card (60 px), the second edge back, and a stem out — 1024: cx 257, y0 189, stem 16, edge 52, drops 29/23, x 143 (lime) / 371 (grey); 768 and 390: cx 257, y0 190, stem 14, edge 50, drops 30/20, x 153 / 361. Each route is sampled 48 points per segment; the packet's arc position is `ease(u)·total` with `ease(u) = (1 − cos(πu))/2`, u = time into the trip / 720; its opacity is `min(1, u/0.08, (1−u)/0.04)`. A port **flashes** when the packet reaches it: at trip fraction 0 (Refund created), at the end of segment 1 for the branch node it passes (`acos(1 − 2·hit/total)/π`), and at 1 (Ask finance); a flash runs 416 ms as `f = (1 − since/416)²`, rendered `opacity: f·0.85`, `scale(1 + (1 − f)·1.4)`.
**Last runs feed** (while the list is in view): every 4000 ms a new row slides into the top — its name/duration cycles through *refunds over 5000 · 134 ms*, *invoice reminder · 388 ms*, *refunds over 5000 · 121 ms*, *weekly export · 1.9 s*, *refunds over 5000 · 142 ms*, *invoice reminder · 402 ms*, its timestamp is the previous top row's plus 47, 73, 38, 91, 56 s cycled. Four rows are laid out absolutely and a spring p 0 → 1 (280/32) moves row i to `translateY((i − 1 + p)·pitch)` with pitch 34 px (2.125rem; 28 px at ≤ 1024); row 0 fades in with p, row 3 fades out with 1 − p, and the previous top row's lime dot fades to idle. After the first shift the list is clipped `inset(0 −16px 0 −16px)` so only its height shows (one row at 768/390).
#### Assets
`hero/hero-window-bloom.webp`, `hero/hero-canvas-grid.svg`, `hero/hero-play-icon.svg`, `hero/hero-field-mark.svg`, `hero/hero-tool-select.svg`, `hero/hero-tool-add.svg`, `hero/hero-tool-redirect.svg`, `hero/hero-tool-extra.svg`, `hero/hero-port.svg`, `hero/hero-port-overhang.svg` — all under `https://storage.getlayers.ai/assets/relay-4ee0729d77/`. The connectors and packets are inline SVG.
### 03 · Integrations — `<section id="integrations">`
A strip under the hero: one mono caption, a dashed rail running behind a row of app tiles, and a lime packet that glides along the rail lighting each tile as it passes. Wrapped by the page in the "leave, still" scroll fade (opacity only, no drift — its edge lines up with the backdrop).
**Section box.** `position: relative; overflow: clip;` height **354px** at 1440 · 212 at 1024 · 228 at 768 · 216 at 390. `content-visibility: auto`. No fills — the particle stream reads through it. (The Figma lime blooms under this section are deliberately NOT drawn.)
**Caption** (`<p>`, Fragment Mono 14/20, `#9ba0a6`, nowrap): "connects to 240 apps, and to whatever you built yourself". Absolute at (80, 96) at 1440; (48, 32) at 1024; (40, 40) at 768. At 390 it is replaced by a centred two-line stack at (65, 32), 261 wide: line 1 "connects to 240 apps, and to", line 2 "whatever you built yourself". The caption **types itself in** once seen: the text is clipped to whole characters (`clip-path: inset(0 calc(100% - round(down, var(--typed) * 1ch, 1ch) - 0.1ch) 0 0)`), `--typed` runs 0 → length+0.5 linearly at **30 ms per character**; the 390 second line starts after the first finishes (28 chars × 30 = 840 ms).
**Tiles** — `<ul>` absolute at (80, 160) at 1440, flex, gap 114 (tile pitch 174). Eight `<li>`, each 60×60 (56×56 at 390):
| app | initials | 1440 | 1024 / 768 | 390 |
|---|---|---|---|---|
| slack | SL | ✓ | ✓ | ✓ |
| stripe | ST (rest-state active tile) | ✓ | ✓ | ✓ |
| postgres | PG | ✓ | ✓ | ✓ |
| hubspot | HS | ✓ | ✓ | – |
| notion | NO | ✓ | – | ✓ |
| gmail | GM | ✓ | – | – |
| webhooks | WH | ✓ | ✓ | – |
| 233 more | + | ✓ | ✓ | – |
Row position / gap per frame: 1024 → (48, 84), gap 113.6 (pitch 173.6); 768 → (40, 92), gap 65.6 (pitch 125.6); 390 → (20, 96), gap 42 (pitch 98).
Each tile is a `border-radius: 16px; overflow: clip; backdrop-filter: blur(8px)` box with **two stacked looks cross-faded by one value `a`** (so either end state paints exactly one Figma style):
- idle (opacity 1−a): 1px border `rgba(255,255,255,.12)`, background `linear-gradient(to bottom, rgba(255,255,255,.06), rgba(255,255,255,.02))` (background-origin border-box), initials in Fragment Mono 14/20 (12px at 390), letter-spacing 1px, colour `#9ba0a6`.
- active (opacity a): border `rgba(200,247,81,.55)`, fill the Figma unpremultiplied lerp `linear-gradient(to bottom, rgba(200,247,81,.18) 0%, rgba(176.75,218,72.5,.2825) 12.5%, rgba(153.5,189,64,.385) 25%, rgba(130.25,160,55.5,.4875) 37.5%, rgba(107,131,47,.59) 50%, rgba(83.75,102,38.5,.6925) 62.5%, rgba(60.5,73,30,.795) 75%, rgba(37.25,44,21.5,.8975) 87.5%, rgb(14,15,13) 100%)`, initials `#c8f751`.
- The box scales `s × (1 + 0.06·a)` — the active tile grows 6 %.
Label under each tile (`<span>`, Fragment Mono 14/20, `#9ba0a6`, 80 wide, top 78 (76 at 1024/768, 68 at 390)): the first tile's label is left-aligned at the tile's left edge, the last is right-aligned ending 2px past the tile's right edge (flush from 1024 down), the rest are centred; at 390 every label is centred.
**Rail** — a div at (90, 190) 1260×2 at 1440 (1024: (58, 114) 908 wide; 768: (50, 121) 668; 390: (28, 123) 334) holding an SVG `<path d="M0 1H{railW}">` stroked with `stroke-dasharray="2 8"` (dash offset 1 at 1440, 0 elsewhere) in a **moving 3-stop horizontal gradient** (userSpaceOnUse): grey `#c2c2c2` at 35 % → a **lime peak right under the packet** (`color-mix(in srgb, #c8f751 calc(var(--lit)*100%), #c2c2c2)`, 35 %) → grey 35 %. Stop offsets follow the packet: `peak = (x − railX)/railW`, `from = peak − 0.1001`, `to = peak + 0.8999` (10 % of the rail lit behind the packet, the rest ahead); `--lit` is the packet's opacity, so the peak fades to grey with it. The whole rail + packet layer is masked with a horizontal `mask-image` that is transparent under every tile box (so nothing shows through a tile while it is still fading in).
**Packet** — a 6×6 lime dot (`integrations-packet.svg`: `<circle r="3" fill="#C8F751">`) at rest at rail position **x = 217** (between slack and stripe) — element at (214, 188) at 1440; (162, 112) at 1024; (130, 119) at 768; (94, 121) at 390. Moved with `translateX((x − rest)/16 rem)` so it scales with the page.
**Motion.**
1. *Reveal* — when the section is properly in view (IntersectionObserver rootMargin `0px 0px -12% 0px`, or the whole tile row is visible at threshold 1 on first layout): the rail draws left → right over **1760 ms** with ease-in-out-quad (`clip-path: inset(0 (1−p)·100% 0 0)`), and each tile **pops** as the drawn edge reaches its left edge: `scale 0.8 → 1, opacity 0 → 1`, spring `{tension 120, friction 26}` (all tiles are forced shown 160 ms after the draw ends). Tile idle/active cross-fade uses the same soft spring. Reduced motion: every tile is simply shown.
2. *Relay lap* — starts at once when the reveal starts and loops while any pixel of the section is on screen. Steps (x in section px, tile centres = tileX + i·pitch + tile/2):
- step 0: jump to under slack (`x = centre(0)`, opacity 0), slack lit, wait 30 ms;
- step 1: glide to the last tile's centre over **5600 ms** (same pace in every frame) with `glide(t)`: quadratic ease-in over the first 18 % joined to a straight line — `k = 1/(1 − 0.09)`; `t < 0.18 ? k·t²/(0.36) : k·(t − 0.09)`; opacity fades in over 360 ms; the lit tile follows the packet (`floor((x − tileX + 0.4·tile)/pitch)`, clamped); wait 5800 ms;
- step 2: opacity → 0 over 360 ms (linear), last tile lit, wait 380 ms; then back to step 0.
With nothing running (before the reveal / reduced motion) the packet sits at rest (x 217) with opacity 1 once the rail has reached stripe, and stripe is the active tile.
3. Leaving the viewport completely resets everything (undrawn, packet hidden, step −1), so the next visit replays the draw, the pops and a first lap.
**Assets.** `https://storage.getlayers.ai/assets/relay-4ee0729d77/integrations/integrations-packet.svg` (6×6 lime circle).
### 04 · Product — `<section id="product">`
Three feature rows on the frosted slab, each pairing a copy column with a 760×328 glass card that plays its own looping sequence: a run trace "waterfall", a Slack approval message arriving, and an isometric bar chart growing. Wrapped by the page in the "enter" scroll fade (opacity .6→1, 48px drift up). Cards on the slab carry **no backdrop-filter of their own** (the slab already frosts the stream).
**Section box.** `display: flex; flex-direction: column;` height **1593px**, padding `96px 80px 0` (80 = page inset) at 1440 · 1509 tall, `80px 48px 0` at 1024 · 2000 tall, `44px 40px 0`, row-gap 16 at 768 · 1680 tall, `48px 20px 0`, row-gap 32 at 390. `content-visibility: auto`.
**Section head** (shared pattern): badge + title in a 760 column, aside 440 wide bottom-aligned 48 to the right; padding-bottom 64 (32 at 1024, 16 at 768, 0 at 390). ≤1200 it stacks centred (badge, title 24 below (12 at 390), aside 16 below), aside 480 wide at 1024, 520 at 768, full at 390.
- Badge: pill `#121315`, 1px `rgba(255,255,255,.1)`, shadow `0 8px 22px -4px rgba(0,0,0,.5)`, padding 5px 13px 5px 11px (7/15/7/11 ≤1200), a 6px `#d6f84c` dot + "content service" in Inter Medium 14/20, tracking .4px, `#9ba0a6`. The dot flashes a halo once when seen: a ring `scale 1→2.6, opacity .8→0`, spring {120, 26}, 250 ms after.
- Title (h2, 44/62 Inter Semi Bold, tracking −2px, vertical gradient text `#ffffff → #bababa`; 36/50 at 1024, 30/42 at 768, 24/32 at 390): "Most of a process is " + *glue* (Instrument Serif italic 400, `#d6dadd`, 50px / 40 / 32 / 26, `line-height: 1`) + ".". Enters as a **focus-in**: `opacity 0→1, filter blur(10px)→0`, with a soft mask sweeping left→right (`mask-image: linear-gradient(90deg, black calc(var(--reveal) − 40%), transparent var(--reveal))`, `--reveal` 0% → 140%), spring {120, 26}, 70 ms after the stack rises.
- Aside (Inter 16/24 `#9ba0a6`): "Copy this into that. Check it went through. Ask Dana if it is fine. Relay takes the glue and leaves you the decisions." — same focus-in with blur(8px), 140 ms.
- The badge + title stack **rises** on being seen: `opacity 0→1, y 24→0`, spring {tension 280, friction 32}.
**Hairlines** between the head and rows: 1280×1 `rgba(255,255,255,.09)`, each **draws from the left** when seen (`scaleX 0→1`, transform-origin left, spring {120, 26}). The two inner hairlines are absent at 390.
**Feature row** (`<article>`, `display:flex; align-items:center; justify-content:space-between; padding: 48px 0` (32 at 1024)): rows are 424 / 424 / 376 tall at 1440 (the chart row has `padding-bottom: 0` so its card sits flush on the row bottom); 388 / 384 / 352 at 1024. Row 02 is `flex-direction: row-reverse` (card left, copy right). At 768 rows stack (`flex-direction: column; height: auto; gap: 32px`, copy 520 wide centred and centred text, card centred); at 390 gap 24, padding 0, copy full width.
- Copy column: 440 wide (360 at 1024), flex column, gap 16. Index (Fragment Mono 14/20, tracking 1px, `rgba(200,247,81,.4)`; at 390 `margin-bottom: −8px`), title (Inter Medium 26/32, tracking −1px, white; 24/32 at 1024, 20/28 at 768, 18/24 at 390), body (Inter 16/24 `#9ba0a6`). Body widths at 1440: row 01 352, row 03 351; row 02 full column (406 at 768).
- Copy rises when seen (spring {280,32}, opacity 0→1, y 24→0); inside it the index **rolls like a counter** 150 ms later: each digit is a vertical strip `0 1 2 … 9 0 … d` clipped to one line-height and slid up by `(10 + d) × 1lh` with spring {120, 26}, digits 70 ms apart (played once when the copy crosses the −12 % line). Title focus-in (blur 8) after 70 ms, body after 140 ms.
- The card rises 70 ms after the copy (same rise).
**Card chrome** (`<figure>`): 760 wide, `border-radius: 26px`, 1px `rgba(255,255,255,.07)`, background `rgba(14,15,17,.42)`, `overflow: clip`, `position: relative`. 528 wide, r 18.063 at 1024; 528, r 24 at 768; 350, r 24 at 390. Card text is Fragment Mono 14/20 (12/18 at 390) unless noted. All positions below are inside the card at 1440 (then 1024 / 768 / 390).
**Row 01 — trace card** (328 tall; 324 / 321 / 249). "run 41982" white at (31, 33) — (23, 23) / same / (19, 19); "finished in 129 ms" `#747980` right-aligned in a 280-wide box at top 33, right 33 — (23, right 23, 156) / (19, 19, 134). Head rule 696×1 `rgba(255,255,255,.07)` at (31, 63) — (23, 55) 480 / (19, 49) 310. Three step rules 696×1 `rgba(255,255,255,.05)` at y 121, 163, 205 — 111/155/199 / 108/153/198 / 90/125/160 (310 wide at 390).
Four steps, each: a 16×16 icon at x 31 (11×11 at 1024/768, 7×7 at 390, x 23 / 19); label at x 59 (45 / 33), white (muted for the pending step); a value right-aligned in a 120-wide box at right 33 (64 wide / right 23; 52 / right 19), `#747980`; a span 12 tall (8 / 5), `border-radius: 4px` (2.779 / 2 at 390), transform-origin left:
| # | icon y | label y | span (x, y, w) | value | span fill |
|---|---|---|---|---|---|
| read refund ✓ | 92 (83/80/70) | 91 (79/76/64) | (223, 95, 228) — 1024 (189, 85, 158) — 390 (146, 70.5, 102) | 120 ms | `linear-gradient(to right, #96c435, #c8f751)` |
| check amount ✓ | 134 (127/125/105) | 133 (123/121/99) | (451, 137, 15) — (189, 129, 10) — (149, 105.5, 7) | 8 ms | `rgba(255,255,255,.8)` |
| branch, over 5000 ✓ | 176 (171/170/140) | 175 (167/166/134) | (466, 179, 6) — (189, 173, 4) — hidden at 390 | 1 ms | `rgba(255,255,255,.8)` |
| ask finance ⏱ | 218 (215/215/175) | 217 (211/211/169) | (468, 221, 60) — (189, 217, 42) — (146, 175.5, 27) | on Dana | pending: SVG `viewBox 0 0 60 12`, rect .5/.5 59×11 rx 3.5, fill `rgba(200,247,81,.18)`, stroke `rgba(200,247,81,.55)` dasharray `3 4` |
Axis: hairline 380×1 `rgba(255,255,255,.09)` at (223, 263) — (189, 271) 220 / (149, 200) 180; five 1×5 ticks `rgba(255,255,255,.16)` at x 223, 318, 413, 508, 603, y 264 (1024: 189, 244, 299, 353, 408 at y 272, 3 tall; 390: 149, 194, 239, 284, 328 at y 201, 2 tall); labels "0 50 100 150 200 ms" centred on the ticks (60 wide, y 273; 279 / 276 / 210) — 390 keeps only 0 / 100 / 200 ms at x 154, 240.5, 308.5.
*Motion (loops while on screen — see the loop driver below; CARD_START = 240 ms after the card is armed):* spans grow `scaleX 0→1` (spring {120, 26}) at **360, 760, 920, 1040 ms**, their ms values counting up with the same spring; each check draws 240 ms after its span starts (`stroke-dasharray 1`, `pathLength 1`, dashoffset 1→0, spring {280, 32}); the clock icon and "on Dana" fade with the pending span's growth; "run 41982" types in from 240 ms at 24 ms/char, the summary from 1400 ms at 24 ms/char (typing = the first N characters drawn, the rest `color: transparent`); the pending span's dashes crawl continuously (`stroke-dashoffset = −t × 7`, one period per 1120 ms) while the card is visible. Play length 2072 ms. Reset runs the waterfall back last-span-first, 48 ms apart; the typed strings clear over 200 ms.
**Row 02 — approval card** (328; 320 / 321 / 289). "#finance" Inter Medium 14/20 white at (31, 31) — (23,23) / (19,19); "today" mono `#747980` right-aligned (120 wide) at top 31 right 31; rule 696×1 at (31, 63) — (23,55) / (19, 47) 310. Typing dots: three 6px `#747980` circles, gap 4, at (71, 124) — (63, 114) / (71, 125) / (19, 104). Message group: avatar `product-avatar.svg` 28×28 at (31, 76) — (23, 75) / (23, 76) / (19, 58); "Relay" Inter Medium 14/20 white at (71, 80) — (63, 79) / (71, 80) / (55, 63); "9:41" mono `#747980` right 31 top 80; message text Inter 16/24 `#9ba0a6`, 520 wide, at (71, 115) — (63, 105) 440 / (71, 116) / (19, 98) 310 at 14/18: "Refund of 6,400 for Northwind is over your limit. Send it?". Accent bar: `product-accent.png` 3×80, `border-radius: 9999px`, at (71, 155), transform-origin top — (63, 143) 3×92 / (71, 152) / (19, 150) 2×70. Fields (`<dl>`): keys mono `#747980` at x 88 (81 / 74 / 29), values Inter Medium white at x 192 (173 / 166 / 117):
| key | value | key y | value y (size) |
|---|---|---|---|
| amount | 6,400 USD | 155 (147/152/150) | 155 (145/152/150), 14/20 (14/18 at 390) |
| customer | Northwind | 183 (177/180/176) | 183 (175/180/176), 14/20 |
| rule | over 5,000 needs a human | 213 (207/210/202) | 211 (205/208/202), 16/24 (14/18 at 390) |
Mini buttons at y 251 (252 at 768, 240 at 390), Inter Medium 14/20 (13/16 at 390), `border-radius: 8px` (4 at 390), float shadow, flex gap 8 (6): **Approve** lime `#c8f751` on ink `#0a0b09`, padding 12px 16px (6/12 at 390) with `product-icon-approve.svg` 14×14, at x 71 (63 / 19); **Hold** `#131416` with 1px `rgba(255,255,255,.16)`, padding 11px 15px (5/11), white, `product-icon-hold.svg`, at x 194 (186 / 124). "waiting 26 min" mono `#747980` tabular-nums, right-aligned 160 wide at top 275 right 31 — (263, 23) / 276 / (245, 19).
*Motion:* typing dots fade in at 240 ms (spring {280,32}) and each dot hops twice (3px, `−3·|sin(π·p)|`, p = clamp(t·2.5 − i·0.25, 0, 2)) on one linear 640 ms clock from 360 ms, the dots a quarter-hop apart; dots fade out at 1120 ms; the message group rises (`opacity 0→1, y 8→0`, spring {280,32}) at 1160; the three fields at 1360 + i·112; the accent bar grows down (`scaleY 0→1`, spring {120,26}) at 1584; Approve, Hold and the waiting time rise at 1800 + i·56. From 2312 ms the waiting minute count ticks +1 every 720 ms, up to +4. Play length 4472 ms. Reset: later elements leave first (rise reversed with delays rank×32: message 192, fields 160/128/96, bar 64, buttons 64/32/0); the minutes return to 26 after 280 ms.
**Row 03 — chart card** (328; 320 / 321 / 263). Six isometric cubes (`product-cube-1…6.svg`, 76 wide, heights 108 / 128 / 150 / 174 / 198 / 222) standing on a baseline at y 254, 123 apart from x 33 — boxes at (33,145) (156,125) (279,103) (402,79) (525,55) (649,31); each with a diamond cap (`product-cap-1…6.svg`, 60×32) 8px in from the cube's left at the cube's top; Saturday's cap has a lime glow (`product-cap-glow.svg` 100×72, placed −20/−20 from its cap). At 1024 cubes are 57 wide on an 85 pitch from x 23 (heights 92/109/128/149/169/190, tops 161/144/125/104/84/64; caps 45×24 at +6); at 768 60 wide, 84 pitch from 23 (heights 84/99/116/135/153/172; caps 47×25); at 390 43 wide, 53 pitch from 19 (heights 60/71/84/97/110/124; caps 34×18). Baseline 696×1 `rgba(255,255,255,.08)` at (31, 259) — (23, 262) 480 / (19, 213) 310. Day labels mono `#747980` centred (80 wide) at y 275 (276 at 768, 226 at 390 with 16px line-height), x 71 / 194 / 317 / 440 / 563 / 687 (1024: 51/136/221/305/389/474; 768: 53/137/221/305/389/473; 390: 40.5/93.5/146.5/199.5/252.5/305.5): mon tue wed thu fri **sat** (sat in `#c8f751`). Caption "runs per weekday" `#747980` at (31, 31) and "62,000 average" `#9ba0a6` at (213, 31) — 1024/768 (23,23) & (381,23); 390 (19,19) & (19,39).
*Motion:* cubes grow from the baseline (`scaleY 0→1`, transform-origin 50% 100%) one after another at **240 + i·120 ms** with an overshooting spring `{tension 390, friction 20.8}`; each cap drops onto its cube 208 ms later (`opacity 0→1, y −26→0` px — −23 / −20 / −14.5 per frame — spring {280,32}); 304 ms after the last cap lands Saturday's glow swells (`opacity 0→1→0, scale 0.8→1.25→0.8`, spring {120,26}). Play length 2072 ms. Reset shrinks the cubes back last-first, 32 ms apart.
**Loop driver (all three cards).** A card *arms* once any pixel is on screen AND it has crossed the −12 % line (IntersectionObserver rootMargin `0px 0px -12% 0px`); while armed it plays for `playMs + 2800 ms` hold, reverses for 800 ms, then plays again. Leaving the screen fully disarms it and snaps every value to its start (so a return replays from the first frame). Reduced motion: the resting (played) state is shown, nothing loops.
**Assets** (`https://storage.getlayers.ai/assets/relay-4ee0729d77/product/`): `product-avatar.svg` 28×28, `product-accent.png` 3×96, `product-icon-approve.svg`, `product-icon-hold.svg`, `product-icon-clock.svg` (16×16), `product-cube-1.svg … product-cube-6.svg` (76 × 108/128/150/174/198/222), `product-cap-1.svg … product-cap-6.svg` (60×32), `product-cap-glow.svg` (100×72).
### 05 — Statement (the lime plate, `#statement`)
A full-width **lime contrast plate** — the one bright block on the page — sitting inside the LogoOpen iris wrapper (see *Transitions*): the wrapper is `34.5rem + 15rem` tall (`33rem + 15rem` at ≤1200, `23.1875rem + 15rem` at ≤580) and the plate is `position: sticky; top: calc(50lvh - 16.875rem)` inside it, so the plate pins mid-screen for 240 design px of scroll while its own timeline plays. The plate itself:
- `<section id="statement" aria-labelledby="statement-title">` — `position: relative; overflow: clip; border-radius: 32px; background: #c8f752` (`--statement-plate`); height **552px** at 1440, **528** at 1024 and 768, **371** at 390. Everything inside is absolutely positioned at the frame's own coordinates (all values below are design px at 1440 → 1024 → 768 → 390; 1 rem = 16 design px in every frame).
- **Tokens** (statement.css): plate `#c8f752`; dot `rgba(10,11,9,.175111)`; ink-muted `rgba(10,11,9,.55)` (kicker, hour labels, the "now" flash ring); ink-caption `rgba(10,11,9,.5)`; mark border `rgba(10,11,9,.25)`; mark gradient from `#252721`; stem from `rgba(10,11,9,.85)` to `rgba(10,11,9,.2)`; plate radius 32, mark radius 24. Kicker 14/20 (12/16 at 390); title line-height = the section-title leading (62 / 50 / **40 at 768** / 32 at 390).
**Dot field** — an SVG `1440 × 548` at top-left (`width: 90rem; height: 34.25rem; color: var(--statement-dot)`), a `<pattern>` 48 × 48 with one `<circle cx=25.5 cy=25.5 r=1.5 fill=currentColor>`, `<rect>` filled with it. Over it a `<canvas>` (`inset: 0; pointer-events: none`, same colour) — see motion.
**Kicker** `<p>` — `[ what we are actually selling ]` — Fragment Mono 14/20, colour ink-muted, centred, box `top 64 left 80 w 1280 h 20` → `48/48/928` → `40/40/688` → `20/20/350 h 16` (12/16 type at 390).
**Title** `<h2 id="statement-title">` — Inter 600, `--type-section-title` (44 / 36 / 30 / 24), tracking −2px, colour `#0a0b09` (`--on-accent`), centred, two `display:block` lines: line 1 `The best workflow is the one`, line 2 `<em>nobody mentions</em> in standup.` where the em is Instrument Serif italic 400 at `--type-section-title-accent` (50 / 40 / 32 / 26), `transform-origin: bottom left`. Every word is an `inline-block` span separated by real spaces. Box `top 108 left 80 w 1280` → `84/48/928` → `76/40/688` → `48/20/350`.
**Mark** (`data-statement-mark`, the tile the iris opens from) — 76 × 76 at `top 292 left 682` → `232/474` → `204/346` → `176/157`. Inner: `border-radius: 24px; border: 1px solid rgba(10,11,9,.25); overflow: clip; background: linear-gradient(135deg in srgb, #252721, #0a0b09 71.429%)`. Inside, a chevron wrapper (`inset: 0`) holding `statement-mark-glyph.svg` (34 × 37 chevrons in `#C8F751`, the left one at 50 % opacity) at `top 18.47 left 21.47 w 33.53 h 37.06` (24.3 % / 28.25 % / 44.1 % / 48.76 % of the tile).
**Stem** — a 1px line `top 368 left 720 h 102` → `308/512/138` → `288/384/156` → `252/195/48`, `transform-origin: top`, `background: linear-gradient(to bottom in srgb, rgba(10,11,9,.85), rgba(10,11,9,.2))`.
**Ruler** —
- caption `<p>` `one tick, one run` — mono 14/20 ink-caption, `top 448 left 80` → `406/48` → `398/40` → `245/20 w 79 h 40 white-space normal` (two lines at 390).
- ticks `<svg width=W+2 height=18 viewBox="0 0 W+2 18" overflow=visible color: #0a0b09>` at `top 484 left 79 w 1282 h 18` → `448/47/930` → `442/39/690` → `301/19/352`, where **W** = ruler width 1280 / 928 / 688 / 350. 121 slots at `x = 1 + i·W/120`; slot 60 (under "now") is skipped; a slot divisible by 12 is a 2px-wide tick from y 0, divisible by 6 from y 7, otherwise 1px from y 12 — all to y 18, `stroke: currentColor`, rest opacity **.35**. Each path has `transform-box: fill-box; transform-origin: bottom`.
- `now` `<p>` — mono 14/20 `#0a0b09`, centred, `top 470 left 690 w 60 h 20` → `476/482` → `468/371 w 26` → `331/182`.
- now-dot — 10 × 10 (11 at 390) at `top 493 left 715` → `441/507` → `439/380` → `298/190`; holds `statement-now-node.svg` (a 10px `#0A0B09` disc with a 2px 30 % ring) plus a flash ring `<span>` (`inset: 0; border-radius: 50%; border: 1px solid rgba(10,11,9,.55)`).
- hour labels `<ul>` 09:00 11:00 13:00 15:00 17:00 19:00 — mono 14/20 ink-muted, `top 508` → `476` → `468` → `331`; boxes 80 wide (44 at 768/390): lefts `80, 296, 552, 808, 1064, 1280` (first left-aligned, last right-aligned, rest centred) → `48, 189, 331, 613, 755, 896` → `40, 147, 255, 469, 577, 684` → `20, 84, —, 262, 326 (right-aligned), —` (13:00 and 19:00 hidden at 390).
#### Motion — one timeline, keyed off the iris
The plate exposes two flags to every leaf: **`seen`** and **`inView`**. `inView` is plain IntersectionObserver visibility of the section. `seen` is read from the LogoOpen wrapper's `data-open` attribute (a MutationObserver on the closest `[data-open]`): `true` once the iris has fully opened, `false` again once the plate has folded away or the reader scrolls back above it — so **every visit replays the timeline**, and the entrance never plays on an empty screen. (Without an iris host, `seen` would be an IO with `rootMargin: 0px 0px -72% 0px` — plate top at 28 % of the viewport — cleared when fully off screen.) When `seen` turns false everything jumps back to its hidden state instantly. `prefers-reduced-motion` → everything jumps straight to its resting state.
**TIMELINE** (ms after `seen`): tile 0 · title 380 (+60 per word) · kicker 460 · stem 1150 · nowPop 1600 · wave 1750 · caption 1800 · chevrons every 4000. Springs: SPRING `{tension 280, friction 32}`, SPRING_SOFT `{120, 26}`, DROP `{260, 15}`, POP `{420, 14}`.
| element | from → to | spring | delay |
|---|---|---|---|
| Mark tile | `y −40px, opacity 0` → `y 0, opacity 1` | y: DROP (a small overshoot landing), opacity: SPRING | 0 |
| Title words (order: The, best, workflow, is, the, one, in, standup., then the serif accent last) | `opacity 0, y 22` → `opacity 1, y 0` | SPRING | 380 + i·60 |
| Title accent `nobody mentions` | `opacity 0, y 28, skewX −16°` → `0 / 0°` (origin bottom-left) | SPRING_SOFT | 380 + 8·60 = 860 |
| Kicker | types in letter by letter (mono clip: `clip-path: inset(0 calc(100% − round(down, calc(var(--typed)·1ch + .5px), 1ch) − .1ch) 0 0)`, `--typed` 0 → length + .5 over length × 30 ms, linear; invisible until `seen`) | timed | 460 |
| Stem | `scaleY 0 → 1` from the top | SPRING_SOFT | 1150 |
| "now" dot | `scale 0 → 1`; the `now` label's opacity = clamp(scale) and y = (1 − min(1,scale))·6px | POP | 1600 |
| Ruler wave — entrance pass | `wave` −0.12 → 1.12, **1600 ms linear** | timed | 1750 |
| Caption | types in (same clip) | timed | 1800 |
| Chevrons in the tile | `x 0 → 5px` then back after 200 ms, every 4000 ms, only while `seen && inView` | SPRING | — |
**The ruler wave** (`wave` = the crest's position as a fraction of the ruler, `NOW_P = 60/120 = .5`, `CREST = .06`, `EDGE = .03`, `crest(w,p) = d = |w−p|/CREST; d ≥ 1 ? 0 : .5 + .5·cos(πd)`, `reveal(w,p) = shown ? 1 : clamp01((w − p + EDGE)/EDGE)`):
- every tick: `opacity = reveal · (.35 + .40·crest)`, `scaleY = (.2 + .8·reveal) · (1 + .5·crest)` — so on the first pass ticks appear left → right behind the crest, and on later passes they rise and darken as it passes;
- each hour label: `opacity = reveal(w, i·24/120)`;
- the flash ring on the dot: `t = (w − .5)/.22`, `scale = 1 + 2.2·clamp01(t)`, `opacity = 0 < t < 1 ? .6·(1 − t) : 0` — a ring leaves "now" as the crest crosses it; the dot itself bumps `scale = 1 + .6·crest(w, .5)`.
- After the entrance pass finishes, `shown = true` (reveal = 1 everywhere) and, **while `inView`**, the same pass replays every **3600 ms hold + 1600 ms pass with easeInOutSine**; between passes the ruler rests exactly on the Figma frame (wave parked at 1.12). Leaving the viewport parks it; scrolling fully away resets `shown` so the next visit draws the ruler in again.
**Dot field pointer swell** (fine pointers only, not under reduced motion): the grid is 30 × 11 dots, pitch 48 from (25.5, 25.5), radius 1.5, in design px. On `pointermove` over the plate each dot within `RADIUS = 120` design px of the pointer gets a target scale `1 + (2.4 − 1)·f²` where `f = 1 − (d/120)²`; each dot's scale follows its target with an under-damped per-dot spring `v += (170·(target − s) − 11·v)·dt; s += v·dt` (dt capped at 32 ms). While any dot is displaced (> .002) the canvas is drawn (device-pixel sized, scaled by `rem/16 · dpr`, fill = the dot colour, one path of arcs at `r = 1.5·scale`) and the SVG pattern is hidden; when every dot is back at rest the canvas is cleared and the SVG shows again, so the idle plate is the original pattern. `pointerleave` sets the target back to 1; leaving the viewport resets all dots instantly.
Assets: `https://storage.getlayers.ai/assets/relay-4ee0729d77/statement/statement-mark-glyph.svg`, `…/statement/statement-now-node.svg`. (`statement-ruler-ticks.svg` is the Figma export of the resting ruler at 1440 and is not loaded — the ticks are generated per frame so they can animate.)
### 06 — Scale (`#scale`, "reliability")
`<section id="scale" aria-label="Reliability">` — `position: relative; content-visibility: auto`, height **758px** at 1440 → **858** at 1024 → **1286** at 768 → **1138** at 390 (design px, 1 rem = 16 in every frame). It sits on the frosted slab with Product → Proof and wraps in a ScrollBlur `leave` (fades to .25 and drifts up 48px once its bottom passes the viewport centre).
**Tokens** (scale.css): card gradient `rgba(23,24,26,.55) → rgba(14,15,17,.4)` (top → bottom, srgb); lane track `rgba(255,255,255,.07)`; share fill `rgba(255,255,255,.32)`; hairline `rgba(255,255,255,.06)`; card radius 24. Type: card title 26/32 tracking −1px (24/32 at 1024, 20/28 at 768, 18/20 tracking 0 at 390), card copy 16/24 (14/20 at 390), art mono 14/20 (12/16 at 390), region names 14/20 (13/18 at 390).
**Section head** (the shared SectionHead): badge `reliability` + title `<em>Boring</em> in the ways that matter.` (Inter 600 gradient title with the serif italic accent first) in a 760 column, aside `Credentials, retries, rate limits, residency. The parts nobody demos, and the parts that decide the year.` 440 wide bottom-aligned 48 to the right; at `top 96 left 80 w 1280` → `80/48/928` (stacked centred, aside **516** wide) → `80/40/688` (aside 514) → `48/20/350` (aside 340).
**Cards row** — `display: flex; gap: 20` at `top 254 left 80` → `286/48 gap 12` → `274/40 column` → `248/20 column`. Two `<article>` cards, **630 × 480** (Figma 504 tall incl. the border; padding 31 = 32 − 1 for the 1px border), `flex column; align-items: flex-start; gap 12; overflow: clip; border-radius 24; border 1px rgba(255,255,255,.1)` (`--border-subtle`), the gradient fill above, **no backdrop-filter** (the slab already frosts the stream). At 1024: 458 × 492, padding 23, gap 32; 768: 688 × 480, gap 16, art `margin-top 8`; 390: 350 wide, height auto. Inside each: `<h3>` (title, 524 wide → 410 → 640 → 310), `<p>` (copy, 500 → 410 → 520 → 310), then the **art box** `position: relative; width: 100%; height 336` → `276` → `316` → runtime 251 / regions 243 at 390. The regions art clips its overflow; the runtime art does not (its glow bleeds, the card clips it).
#### Card 1 — "One runtime, every app"
Copy: `Relay holds the credentials, retries and rate limits, so a step behaves the same on Stripe or on the API you wrote last week.`
Two mono-dim lines (Fragment Mono 14/20, `#747980`) sit outside the hub in every frame: **label** `one runtime, every connection` at `top 24 left 0 w 518` → `0/0/358` → `24/0/640` → `0/0/310`; **caption** `credentials, retries, rate limits, replay` at `top 292` → `256 w 358` → `296 w 640` → `236 w 310`.
**The hub group** — a `566 × 336` box at the art origin, `transform-origin: top left`. At 1024 Figma draws the same group scaled: `transform: translate(−47.31px, −6.91px) scale(0.888031)`; at 768 it is `translate(125px, 0) scale(1)` (its own layout below); at 390 `translate(0,0)`. Children, in paint order (positions design px at 1440 / 768 / 390; 1024 = 1440 scaled):
1. `scale-wires-768.svg` (304 × 86 — a flat grey rail + two dashed spokes) at `top 94 left 43 w 304 h 86`, only at 768.
2. `scale-wires-390.svg` (228 × 57) at `top 70 left 41 w 228 h 57`, only at 390.
3. **core glow** `scale-core-glow.png` (a 280 lime circle exported with 120 of blur padding, 1040² png) at `top −88 left 23 size 520` / `top −61 left −45 size 480` / `top −74 left −45 size 400`.
4. **orbit inner** `scale-orbit-inner.png` (176²) at `top 81 left 195 size 176` / `109/125/140` / `68/97/116`.
5. **orbit outer** `scale-orbit-outer.png` (220²) at `top 59 left 173 size 220` / `89/105/180` / `51/80/150`.
6. `scale-wires.svg` (518 × 240: two horizontal lime rails `M64 140H259` / `M259 140H454` fading .12 → .6 toward the centre, and two dashed diagonals `M139 66L259 140` / `M259 140L379 66`, dash `2 7`) at `top 29 left 24 w 518 h 240` — hidden at ≤900.
7. four **packets** `scale-packet.svg` (a 6px `#C8F751` disc), 6 × 6, resting at `(top,left)`: SL `166,160` / `176,124` / `122,75`; ST `125,214` / `132,146` / `89,104`; PG `125,346` / `130,240` / `89,199`; HS `166,400` / `176,260` / `122,228`.
8. four **app nodes** `<ul aria-label="one runtime, every connection">`: a 40 × 40 (34 at 390) circle `border 1px rgba(255,255,255,.1); background #121315 (--surface-badge); mono 14/20 #9ba0a6; box-shadow 0 8px 22px −4px rgba(0,0,0,.5)` with the code, a same-size **flash ring** (`border 1px #c8f751; opacity 0`) on top of it, and a centred mono-dim label 72 wide (88 at 768) under it. Nodes at: SL `top 149 left 68` / `159,24` / `109.25,9`; ST `75,143` / `75,88` / `43,33`; PG `75,383` / `75,262` / `39,251`; HS `149,458` / `159,326` / `109.25,267`. Labels (slack, stripe, postgres, hubspot): `195,52` / `205,0` / `149,9 w38`; `121,127` / `121,64` / `77,39 w45`; `121,367` / `121,238` / `77,219 w60`; `195,442` / `205,302` / `149,249 w52`.
9. the **runtime tile** `scale-runtime.svg` (96 × 96: a 24-radius rounded square filled `#E2FF8F → #C8F751` diagonal, a white 22 % shine ellipse at the top, the two chevrons in `#0A0B09` at 10 stroke, a .25 white 1px inset stroke) at `top 121 left 235 size 96` / `135,151,88` / `94.25,123,64` — **`data-logo-target`**: the LogoOpen transition flies the Statement logo into this exact box.
#### Card 2 — "Runs where your data lives"
Copy: `Pick Frankfurt, Virginia or Sydney. Payloads and logs stay in the region you chose, and the audit trail says so in writing.`
- label (mono-dim) `where runs executed, last 24 hours` at `top 24 left 0 w 518` → `0/0/390` → `24/0/640` → `0/0/310`.
- `<ul>` of three rows at `top 76 w 566` → `44/390` → `76/640` → `28/310`; each `<li>` `position: relative; height 72` (69 at 1024, 72 at 768, 61 at 390):
- region name (mono 14/20 white): `eu-central-1`, `us-east-1`, `ap-southeast-2` at top-left;
- city (14/20 `#747980`, Inter): `Frankfurt`, `Virginia`, `Sydney` at `top 24` (20 at 390);
- **lane**: track `262 × 10` at `top 6 left 262` → `150 × 10 at 5/198` → `220 × 10 at 6/378` → `110 × 5 at 15.5/158`, `border-radius 9999; overflow: clip; background rgba(255,255,255,.07)`; inside it the **fill** `width: calc(var(--lane-grow) · var(--lane-fill))` where `--lane-fill` = **121 / 100 / 42** px (61 / 50 / 21 at 390) and the colour is `#c8f751` for the first row, `rgba(255,255,255,.32)` for the other two; the **head** `scale-bar-head.svg` (a 10px `#F4F5F6` disc; 5px at 390) rides outside the clipped lane at `left: calc(lane-x + grow · fill − 5px)` (−2.5 at 390) with a halo ring (`border 1px #fff`) over it; the **share** `46%` / `38%` / `16%` right-aligned mono 14/20 white tabular-nums at top-right (`top 10` at 390);
- a hairline `top 56 h 1 w 100% rgba(255,255,255,.06)` (46 at 390).
- **lock** `scale-lock.svg` (14 × 14 grey padlock) at `top 295 left 0` → `259/0` → `291/0` → `213/0 size 12`, and the **footnote** `a payload never leaves the lane it started in` (mono-dim) at `top 292 left 26 w 492` → `256/22/390` → `288/26/596` → `211/20/174`.
#### Motion
Entrance (once, when *seen*, stagger STEP = 70 ms; index 0 = runtime card, 1 = regions card):
| element | effect | spring | delay |
|---|---|---|---|
| section head | badge + title Reveal rise (`opacity 0→1, y 24→0`, SPRING {280,32}); title focus (`blur 10→0` + the left→right mask sweep, SPRING_SOFT {120,26}) at +70; aside focus (blur 8) at +140; badge dot pulse halo at +250 | | |
| card `<article>` | Reveal rise | SPRING | (index+2)·70 → 140 / 210 |
| card title | focus (blur 8 + mask sweep) | SPRING_SOFT | (index+2)·70 |
| card copy | focus | SPRING_SOFT | (index+2)·70 + 70 |
| runtime label | types in (mono clip, 30 ms/char) | timed | 140 |
| runtime caption | types in — **started by the label being seen**, not by itself (it sits below the seen line) | timed | 350 |
| regions label | types in | timed | 210 |
| region names | type in | timed | (1+3+i)·70 → 280 / 350 / 420 |
| cities | focus (blur 8) | SPRING_SOFT | 350 / 420 / 490 |
| footnote | types in, triggered by the `<ul>` being seen | timed | 700 |
**Runtime hub loop** — starts once the *card* is properly seen (IO `rootMargin 0px 0px -12% 0px`), runs on its own rAF from `t = 0`, elapsed capped at 100 ms per frame; fully off screen it stops and **rewinds to t = 0** (the first frame is parked before it starts, so the start is never a jump); reduced motion never starts it:
- `outer` ring `rotate(turn)`, `inner` ring `rotate(−turn)`, `turn = (t mod 32000)/32000 · 360°`;
- glow **breath**: `b = (1 − cos(2πt/3840))/2`, `opacity = 1 − .3b`, `scale = 1 + .06b`;
- **packets**: each rides its wire from the satellite centre to the runtime centre with a per-wire phase offset `[0, .25, .5, .75]` of a **1920 ms** cycle: `p = (rest + t/1920 + offset) mod 1`, `translate((p − rest)·dx, (p − rest)·dy)` in design px, `opacity = smoothstep(0,.15,p) · (1 − smoothstep(.85,1,p))`; the sender's flash ring `opacity = max(0, 1 − p/.3)²`. Wire vectors (`dx, dy, rest`): 1440/1024 — SL `195, 0, 75/195`; ST `120, 74, 54/120`; PG `−120, 74, 54/120`; HS `−195, 0, 75/195`. 768 — `151,0,83/151` · `87,84,41/87` · `−87,84,39/87` · `−151,0,83/151`. 390 — `129,0,52/129` · `105,48,57/105` · `−113,51.5,66/113` · `−129,0,53/129`. Crossing a frame line swaps the wire set and redraws the current time (no restart).
**Regions cycle** — phases `off → play → reset → play …`: the first `play` starts once the card is properly seen (same −12 % margin); `play` lasts **1920 + 2800 ms** (grow + hold on the Figma frame), `reset` **1280 ms**, then `play` again; fully off screen → `off` (lanes empty, instantly) and the next visit waits to be seen again. Reduced motion parks it on `play` with everything immediate.
- each lane's `--lane-grow` springs 0 → 1 on `play` (and back to 0 on `reset`) with **GROW = {tension 180, friction 31.72, clamp}** (SPRING_SOFT ×1.5 tension, ×1.22 friction, clamped so the fill never overshoots) after a stagger of `.8·(1+3+i)·70` → **224 / 280 / 336 ms**; the share text counts on the same value: `round(grow · 46) + '%'`; when a bar lands at 1 its head sends one **halo**: `pulse` 0 → 1 with {180, 31.72}, ring `scale 1 + 1.6p`, `opacity .8(1 − p)`;
- the lock: `scale .82 → 1` with **{420, 14}** after **1040 ms** into `play` (a snap shut), back to .82 on `reset`, immediate on `off`.
Assets (all under `https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/`): `scale-wires.svg`, `scale-wires-768.svg`, `scale-wires-390.svg`, `scale-core-glow.png`, `scale-orbit-inner.png`, `scale-orbit-outer.png`, `scale-packet.svg`, `scale-runtime.svg`, `scale-bar-head.svg`, `scale-lock.svg`.
### 07 · Signals — "A Tuesday, seen from Relay." (`<section id="signals">`, Figma 3888:6522)
The dashboard section: the shared section head and five glass widgets on the frosted slab (the slab is the page's, not the section's — the widgets carry **no backdrop-filter of their own**). The section is wrapped by the page's scroll hand-off (`enter leave`: fades from 0.6 → 1 while its top rises to the viewport centre, drifting up 48px; fades 1 → 0.25 once its bottom passes the centre).
**Section root.** `position: relative`, `content-visibility: auto`; height **774px** (1024: 784 · 768: 1138 · 390: 1401). Everything inside is absolutely positioned in the frame's own coordinates.
**Head** (the shared SectionHead): at (80, 96), 1280 wide (1024: (48, 0) 928 wide · 768: (40, 40) 688 wide · 390: (20, 0) 350 wide). Badge `daily view` (lime 6px dot with a one-off halo 250ms after seen, 14/20 Inter Medium +0.4px in #9ba0a6 on #121315 with a 1px rgba(255,255,255,.1) ring, pill), then the title `A <em>Tuesday</em>, seen from Relay.` — Inter Semi Bold 44/62 −2px, white → #bababa vertical gradient clipped to the text, the word *Tuesday* in Instrument Serif italic 50px #d6dadd (1024: 36/50 + 40 · 768: 30/42 + 32 · 390: 24/32 + 26) — and the aside `The screen people actually keep open: what ran, what is queued, and who still owes an answer.` 16/24 #9ba0a6, 440 wide bottom-aligned 48 right of the 760 badge+title column (1024/768/390: stacked and centred; aside 494 / 502 / 328 wide). Motion: the badge+title stack rises 24px + fades (SPRING 280/32) on seen; the title comes out of `blur(10px)` with a left→right mask sweep (`mask-image: linear-gradient(90deg, black calc(var(--reveal) − 40%), transparent var(--reveal))`, `--reveal` 0% → 140%, SPRING_SOFT 120/26) 70ms later; the aside the same from `blur(8px)` 140ms later.
**Widget grid.** Column at (80, 254), 1280 wide, 20px gaps; Row A = week (876 × 256) + hands off (384 × 256); Row B = waiting (413 × 244) + failures (413 × 244) + where it ran (414 × 244).
1024: at (48, 206), 928 wide, 12px gaps; Row A two 458 × 258 halves, Row B three equal thirds 268 tall (flex-grow 1, basis 0).
768: at (40, 234), 688 wide, 12px gaps, the two rows dissolve into one wrapping flex row (`display: contents`) with `order`: where it ran (1, 338 × 294) | hands off (2, 338 × 294), waiting (3, 338 × 248) | failures (4, 338 × 248), then the full-width week widget (5, 688 × 298).
390: at (20, 200), 350 wide, one column: where it ran (350 × 290), hands off (350 × 290), waiting (350 × 290), week (350 × 295). **Failures is not in the 390 frame** (`display: none`).
**Widget shell** (all five): `<article>`, `overflow: clip`, radius 24, 1px rgba(255,255,255,.07) border, fill `rgba(14,15,17,.42)` (the card glass token). Each widget rises 24px + fades in (SPRING 280/32) at `(order + 2) × 70ms` after seen: week 140 · hands off 210 · waiting 280 · failures 350 · where it ran 420. Every widget title is Fragment Mono 14/20 in #747980 (color/text-dim), absolutely placed, and **types itself in** (the TypeText clip: `--typed` runs 0 → length + 0.5 over `length × 30ms`, `clip-path: inset(0 calc(100% − round(down, calc(var(--typed) × 1ch + 0.5px), 1ch) − 0.1ch) 0 0)` — note the 0.5px nudge before rounding so the last glyph is never shaved) starting 150ms after its widget starts to rise (title delays: 290 · 360 · 430 · 500 · 570). Body copy in a widget is 16/24 #9ba0a6 and focuses in (blur 8 + mask sweep) 220ms after the widget starts (430 · — · 500 · — · 640).
Tokens (section-scoped): `--signals-widget-border rgba(255,255,255,.07)`, today gradient `#c8f751 → rgba(200,247,81,.72)`, today rule `rgba(200,247,81,.75)`, chip fill `rgba(200,247,81,.06)` over a `rgba(0,0,0,.08) → rgba(0,0,0,.25) at 65%` vertical shade, chip border `rgba(200,247,81,.4)`, label plate `rgba(11,11,11,.9)`, ring track `#383b3e`, recovered curve `rgba(200,247,81,.9)`, failed curve `#606468`, marker fill `#0f1112`, map dot `rgba(166,166,166,.38)`, peak glow `0 0 14px 2px rgba(200,247,81,.55)`, stat type 32/40 Inter Medium −1px (1024 82% only: 28/36 · 390 82%: 22/28), chip copy 12px at 390, region labels 15/21 at 390. `shadow-float` = `0 8px 22px −4px rgba(0,0,0,.5)`.
#### Widget 1 — `this week, run by run` (876 × 256 · 458 × 258 · 688 × 298 · 350 × 295)
- Title at (23, 29) (1024: y 23 · 768: y 31 · 390: (19, 19)).
- **Next-run chip** `<p>` at (653, 23), 202 × 32, radius 8, 1px `rgba(200,247,81,.4)` border, the chip fill above, padding 5 × 11, shadow-float, flex/center gap 8: a 14 × 14 lime clock icon (inline SVG: circle r 5.4 stroke 1.4 + hand `M7 4.2V7L9 8.4` round cap) and `next run in 12 min` Fragment Mono 14/20 white. 1024 (and below): (264, 17), 160 wide, padding-x 7, copy `next in 12 min`. 768: (491, 23), 172 × 36, padding 11 → 15. 390: under the title at (19, 51), 154 wide, 2px border, padding-left 10, 12px copy. The copy types in 400ms after the widget starts (540ms).
- **Clock hand loop** (from 380ms): a full turn on spring 105/24.4, then on a 640ms beat (8 beats per cycle) beats 4–6 tick the hand +6° each on 420/39 (SPRING ×1.5 tension, ×1.22 friction), beat 7 rests, and the next cycle's turn carries it to the next whole turn — `angle = 360 × turns + ticks × 6`, transform-origin 7px 7px; `none` when the angle is a whole turn. Runs only while in view; a widget that leaves the screen resets to angle 0.
- **Hour labels** `09 11 13 15` at x 23 (1024: 31 · 768: 23 · 390: 19), y 68 / 102 / 136 / 170 (1024: 66 / 100 / 134 / 168 · 768: 77 / 117 / 157 / 197 · 390: 100 / 134 / 168 / 202), mono 14/20 #747980 — **`13` in #9ba0a6** (the row holding today's peak). They type in at 290 + i × 70ms.
- **Heatmap** `<ul>` at (63, 65) (1024: (71, 63) · 768: (86, 79) · 390: (47, 103)), 7 columns 98 wide with 16 gaps (1024: 45 wide, 8 gaps · 768: 79 wide, 8 gaps · 390: 36 wide, 5 gaps). Each column: 8 bars 14 tall (768: 16) radius 4 with 3px gaps (768: 4), then the day label (`mon … sun`, mono 14/20, #9ba0a6 for `tue`, #747980 otherwise; centred at ≤1200) 13px below (768: 18 · 390: 16). Every bar is a left→right gradient `rgb(r g b / a₀) → rgb(r g b / a₁)`:
| day | bars top → bottom `[r, g, b, a₀, a₁]` |
|---|---|
| mon | [62,65,69,.52,.22] [73,77,82,.6,.25] [82,86,91,.66,.28] [89,94,99,.71,.3] [86,91,96,.69,.29] [77,81,86,.63,.26] [67,71,75,.56,.24] [58,61,65,.5,.21] |
| tue | [66,69,73,.55,.23] [77,81,86,.63,.26] [88,93,98,.7,.29] [97,102,107,.76,.32] **PEAK** [89,94,99,.71,.3] [73,77,82,.6,.25] [62,65,69,.52,.22] |
| wed | [60,63,67,.51,.21] [72,75,80,.59,.25] [80,85,90,.65,.27] [88,93,98,.7,.29] [85,89,94,.68,.29] [74,79,83,.61,.26] [64,68,72,.54,.23] [57,60,64,.49,.21] |
| thu | [64,68,72,.54,.23] [74,79,83,.61,.26] [85,89,94,.68,.29] [94,99,104,.74,.31] [88,93,98,.7,.29] [79,83,88,.64,.27] [69,72,77,.57,.24] [58,61,65,.5,.21] |
| fri | [58,61,65,.5,.21] [69,73,77,.57,.24] [79,83,88,.64,.27] [86,91,96,.69,.29] [80,85,90,.65,.27] [72,75,80,.59,.25] [62,65,69,.52,.22] [55,58,61,.47,.2] |
| sat | [45,47,51,.41,.17] [48,50,54,.43,.18] [51,54,57,.45,.19] [52,55,59,.46,.19] [51,54,57,.45,.19] [48,50,54,.43,.18] [45,47,51,.41,.17] [43,46,49,.4,.17] |
| sun | [43,46,49,.4,.17] [46,48,52,.41,.17] [49,51,55,.44,.18] [51,54,57,.45,.19] [49,52,56,.44,.18] [47,50,53,.43,.18] [44,47,50,.41,.17] [42,44,47,.39,.16] |
The PEAK (tue, 5th bar) is the lime gradient `#c8f751 → rgba(200,247,81,.72)` with a glow layer (`box-shadow: 0 0 14px 2px rgba(200,247,81,.55)`, opacity 0 at rest). The **today rule** under tue's column: 2px tall, column-wide, radius 1, `rgba(200,247,81,.75)`, at y 139 inside the column (768: 164), origin left.
- **Heatmap motion** (the illustration's `seen` = its widget card 12% up from the bottom of the viewport; it rewinds when the art leaves the screen entirely). Every cell springs `t` 0 → 1 (opacity t, `scale(0.92 + 0.08t)`, `none` at rest) on a long spring **105/26.9**, delay `208 + (col + row) × 36ms` — a diagonal wave from mon/top to sun/bottom; the peak cell waits for `208 + (13 + 2) × 36 = 748ms`, then its glow rises and falls on one spring 33/15.9 (opacity `sin(π·g)`) from 844ms, and the today rule draws (`scaleX` 0 → 1, 180/31.72) from 1084ms. Day labels fade in (105/26.9) when the wave's bottom cell reaches their column: `208 + (col + 7) × 36`; on every cycle a white copy over each label breathes up (135/29.4) at that same moment, holds 96ms, and eases back (60/24.4). **Loop**: play 2240ms → hold 2800ms → every cell fades out along the same diagonal, quicker (`(col + row) × 20ms`) over 1200ms → again, while the widget is in view. Reduced motion: the resting frame.
#### Widget 2 — `hands off` (384 × 256 · 458 × 258 · 338 × 294 · 350 × 290)
- Title at (27, 23) (768: x 23 · 390: (19, 19)).
- **Ring gauge**, a 130 × 130 SVG at (126, 62) (1024: (163, 61) · 768: (103, 67) · 390: (99, 65)): track circle `M65 119C94.8234 119 119 94.8234 119 65C119 35.1766 94.8234 11 65 11C35.1766 11 11 35.1766 11 65C11 94.8234 35.1766 119 65 119Z` stroke #383b3e width 10; the lime arc `M65 11C76.6879 11.0309 88.0503 14.8532 97.3805 21.8927C106.711 28.9322 113.505 38.8089 116.743 50.0393C119.981 61.2698 119.488 73.2477 115.338 84.174C111.188 95.1003 103.605 104.385 93.7277 110.634C83.8504 116.883 72.2124 119.758 60.5615 118.829C48.9106 117.899 37.8758 113.214 29.1146 105.478C20.3534 97.7417 14.3387 87.3717 11.9739 75.9255C9.60906 64.4793 11.0218 52.5748 16 42` stroke #c8f751 width 10 round caps, `pathLength=1`.
- **Figure** `82%` centred over the ring: a 130-wide `<p>` at (126, 107) (1024: (163, 108) 28/36 · 768: (103, 114) · 390: (99, 116) 22/28), 32/40 Inter Medium −1px white.
- **Caption** `of steps finish without a person in the loop` centred, 16/24 #9ba0a6, at (23, 209) 336 wide (1024: 410 wide · 768: (81, 221) 174 wide · 390: (54, 209) 240 wide), focus-in at 430ms.
- **Motion**: one spring `p` (180/31.72) drives both the arc (`stroke-dasharray: 1 1; stroke-dashoffset: 1 − p`, visible once p > 0, dasharray `none` at rest) and the figure (`round(p × 82)%`, tabular-nums while counting). Loop: first play starts 410ms after seen (lead), draw 1440ms → hold 2800 → ease back to 0 over 1120ms → again, while in view.
#### Widget 3 — `waiting on people` (413 × 244 · 301 × 268 · 338 × 248 · 350 × 290)
- Title at (23, 23) (390: (19, 19)).
- **Face stack** `<ul>` at (23, 72) (768: y 67 · 390: (19, 74)): five 52 × 52 PNG avatars (768: 44 · 390: 60) overlapping by 8px (768: 12 · 390: 16), in order `signals-face-1.png … signals-face-5.png`.
- **Count** `<p>` at (23, 142), 320 × 40 (1024: 276 wide · 768: y 135 · 390: (19, 169)): the number `3` in 32/40 Inter Medium −1px **#c8f751** top-left, the label `approvals` 16/24 #9ba0a6 at (30, 8) inside it (768: x 29 · 390: y 12), focus-in at 750ms.
- **Note** `Longest wait: 26 minutes, in #finance` 16/24 #9ba0a6 at (23, 195) 365 wide (1024: 253 wide · 768: y 199, 290 wide · 390: (19, 245)), focus-in at 500ms.
- **Motion** (lead 440ms on the first cycle only): face *i* starts at `translateX(36 + i × 14px)` opacity 0 and slides in to 0 / 1 on 180/31.72, `lead + i × 48ms` apart; the count then springs 0 → 3 (same spring) at `lead + 5 × 48`. Loop: play 1600 → hold 2800 → the faces leave last-first (`(4 − i) × 32ms`) and the count runs back to 0 over 960ms → again, while in view.
#### Widget 4 — `failures and recoveries` (413 × 244 · 302 × 268 · 338 × 248 · not at 390)
- Title at (23, 23).
- **Curves**, a 365 × 110 SVG at (23, 67), 365 × 110 shown (1024: (23, 83) 253 wide, height auto · 768: (23, 67) 290 wide), `overflow: clip`, `role="img"` "Recovered and failed-hard runs crossing over the week":
- recovered `M0 78C60.8333 78 76.0417 26 128.185 30C182.5 34 193.363 84 252.024 82C310.685 80 325.893 40 365 36` stroke rgba(200,247,81,.9) width 3 round caps;
- failed `M0 34C60.8333 34 76.0417 86 128.185 82C182.5 78 193.363 28 252.024 30C310.685 32 325.893 72 365 76` stroke #606468 width 3 round caps;
- three crossing markers, circles r 5 at (62, 56), (185, 56), (322, 56), fill #0f1112, stroke rgba(200,247,81,.9) width 2.
- **Legend** `<ul>` at (23, 197) (1024: y 199, stacked with 4px gaps · 768: y 178): an 8px lime dot (`signals-legend-recovered.svg`) + `recovered`, a 20px gap, an 8px #848484 dot (`signals-legend-failed.svg`) + `failed hard`, mono 14/20 #747980, typing in at 850 / 1150ms.
- **Motion** (art delay 550ms after seen): both lines draw in (`pathLength=1`, dasharray 1 1, dashoffset 1 → 0) on 180/31.72 — recovered at 550, failed at 662 — and the markers pop (`scale(0.4 + 0.6m)` + opacity) at 1270. From 550 + 1360ms, while in view: a 4.8s drift loop — every control/end point except the fixed ends shifts by `dx = 10 × sin(2π·t / 4800)` for the recovered curve and `−dx` for the failed one (the paths are rewritten each frame), and each marker glides to the curves' actual intersection near its resting x (scan ±45px, interpolate the sign change of the y-difference; markers move by `translate(cx − base, cy − base)`). Off screen everything rewinds to the undrawn start.
#### Widget 5 — `where it ran` (414 × 244 · 301 × 268 · 338 × 294 · 350 × 290)
- Title at (23, 23) (390: (19, 19)).
- **Dot map**, a 366 × 132 SVG at (23, 53), 366 × 132 (1024: (23, 71) 253 wide, height auto · 768: (23, 94) 290 wide · 390: (19, 74) 310 wide), `role="img"` "Dot map of run regions: iad 46%, fra 38%, syd 16%": 249 ellipses rx 1.525 ry 1.4 fill rgba(166,166,166,.38) at these centres (the columns are 6.5357 apart, rows 6 apart):
x=35.9464: y 33 39 45 51 · x=42.4821: 27 33 39 45 51 57 · x=49.0179: 27 33 39 45 51 57 63 · x=55.5536: 27 33 39 45 51 57 63 · x=62.0893: 27 33 39 45 51 57 63 · x=68.625: 27 33 39 45 51 57 63 · x=75.1607: 27 33 39 45 51 57 63 69 · x=81.6964: 27 33 39 45 51 57 63 69 75 · x=88.2321: 33 39 45 51 57 87 93 99 105 · x=94.7679: 39 45 81 87 93 99 105 111 · x=101.304: 81 87 93 99 105 111 · x=107.839: 87 93 99 105 · x=147.054: 39 · x=153.589: 33 39 45 75 81 87 · x=160.125: 33 39 45 51 63 69 75 81 87 93 · x=166.661: 33 39 45 51 57 63 69 75 81 87 93 99 · x=173.196: 33 39 45 57 63 69 75 81 87 93 99 · x=179.732: 39 57 63 69 75 81 87 93 99 · x=186.268: 63 69 75 81 87 93 99 · x=192.804: 69 75 81 87 93 · x=199.339: 39 45 51 · x=205.875: 33 39 45 51 57 · x=212.411: 27 33 39 45 51 57 · x=218.946: 27 33 39 45 51 57 63 69 · x=225.482: 27 33 39 45 51 57 63 69 75 · x=232.018: 27 33 39 45 51 57 63 69 75 · x=238.554: 27 33 39 45 51 57 63 69 · x=245.089: 27 33 39 45 51 57 63 · x=251.625: 27 33 39 45 51 57 63 · x=258.161: 27 33 39 45 51 57 63 · x=264.696: 27 33 39 45 51 57 63 · x=271.232: 27 33 39 45 51 57 · x=277.768: 33 39 45 51 57 · x=284.304: 39 45 51 · x=290.839: 93 99 105 · x=297.375: 93 99 105 · x=303.911: 93 99 105 · x=310.446: 93 99 105 · x=316.982: 93 99 105 · x=323.518: 99.
- **Three region markers** (widget coordinates, inside the 1px border; the label plates and labels are drawn first, the ring/halo/pin stacks above them so a plate never covers a pin). Each marker: a 28 × 28 box (768: scaled 24/28 from its top-left · 390: 26/28) holding `signals-region-ring.svg` (circle r 13.5 stroke rgba(194,194,194,.35)), `signals-halo.svg` (34 × 34, a r-10 #c2c2c2/.22 disc blurred σ 3.5) at (−3, −3) (1024: (−2, −2)), and the 9 × 9 lime-100 pin `signals-pin.svg` (#eaff9e) at (10, 10) (1024: (9, 9)); behind the pin a 32 × 32 1px #c8f751 ring centred on it, opacity 0 (the ping). The label plate is 75 × 24 (1024: 77 · 768: 73 × 22 · 390: 78 × 23), radius 6, rgba(11,11,11,.9), shadow-float; the label Fragment Mono 14/20 white (390: 15/21).
| region | dot (1440 · 1024 · 768 · 390) | plate | label |
|---|---|---|---|
| `iad 46%` | (65, 85) · (49, 95) · (35, 123) · (32, 105) | (89, 89) · (77, 97) · (59, 124) · (58, 106) | (97, 91) · (85, 99) · (65, 125) · (64, 107) |
| `fra 38%` | (165, 78) · (125, 77) · (137, 98) · (141, 78) | (192, 81) · (153, 79) · (161, 99) · (167, 79) | (200, 83) · (161, 81) · (167, 100) · (173, 80) |
| `syd 16%` | (308, 129) · (110, 128) · (115, 171) · (117, 156) | (237, 139) · (138, 130) · (139, 172) · (143, 157) | (245, 141) · (146, 132) · (145, 173) · (149, 158) |
- **Caption** `Every run stays in the region that started it` 16/24 #9ba0a6 at (23, 195) 366 wide (1024: 168 wide · 768: y 221, 226 wide · 390: (19, 221)), focus-in at 640ms.
- **Motion** (art delay 580ms after seen): the dots fade in (180/31.72) in nine pseudo-random buckets — bucket = `(imul(i + 1, 2654435761) >>> 0) % 9` — 44ms apart, so the map fills scattered; then each pin's ring + halo + plate fade in while the pin drops 14px (420/39.04) at `580 + 416 + k × 112ms` (iad, fra, syd), and its label types in 160ms after that (1156 / 1268 / 1380). From 580 + 416 + 336 + 400ms, while in view: each pin pings — every 2400ms (offset 360ms per region) a ring grows from `scale(0.28)` to 1 over 1040ms with cubic ease-out, opacity `0.8 × (1 − e)`. Off screen the map rewinds to empty.
**Assets** (`https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/`): `signals-face-1.png` … `signals-face-5.png` (60 × 60), `signals-legend-recovered.svg`, `signals-legend-failed.svg` (8 × 8), `signals-region-ring.svg` (28), `signals-halo.svg` (34), `signals-pin.svg` (9). The clock, ring gauge, curves and map are inline SVG (their source files `signals-clock.svg`, `signals-ring.svg`, `signals-curves.svg`, `signals-map.svg` are reproduced above).
### 08 · Proof — `<section id="proof">`
The third block on the frosted slab (wrapped by the page's ScrollBlur with **enter + leave**: it fades in from 0.6 and rises 48px as its top climbs to the viewport centre, fades to 0.25 and drifts 48px up once its bottom passes the centre). Three case-study stat columns under a head; the page's field-texture backdrop shows through it. Section root: `position: relative`, `content-visibility: auto` with the frame's explicit height — **884px** at 1440, 816 at 1024, 906 at 768, 1706 at 390.
**Blooms.** A `div` covering the section (`absolute; inset:0; overflow:clip; pointer-events:none`) that fades in as a whole (Reveal *fade*: opacity 0→1 on the soft spring 120/26 once seen), holding two `<img>` PNGs (940×680 dithered Figma exports), each inside its own drift layer:
- `proof-bloom-right.png` — 1440: top 20, left 600, 940×680 displayed; 1024: top −152, left −160, 1344 wide (height stays 680 — the image is stretched, by design); 768: top −160, 1088 wide; 390: top −152, 710 wide.
- `proof-bloom-left.png` — 1440: top 60, left −40, 940×680; same narrower-frame overrides.
- `https://storage.getlayers.ai/assets/relay-4ee0729d77/proof/proof-bloom-right.png`, `…/proof/proof-bloom-left.png`.
- **Drift:** each bloom's layer is scrubbed by scroll over the whole section (progress 0 with the section's top at the viewport bottom → 1 with its bottom at the viewport top): `translateY` from −40px to +40px for the left bloom (direction +1) and +40 → −40 for the right one (direction −1), linear, no spring. Reduced motion: no drift.
**Head (1440).** A Reveal *rise* wrapper (opacity 0→1, y 24→0, spring 280/32) at top 96, left 80, width 536, column, gap 6: the badge (lime 6px dot in a pill: `#121315` fill, 1px `rgba(255,255,255,.1)` border, shadow `0 8px 22px -4px rgba(0,0,0,.5)`, padding 5px 13px 5px 11px, label "case studies" 14/20 Inter 500, tracking 0.4px, `#9ba0a6`; the dot's halo scales 1→2.6 and fades .8→0 on the soft spring 250ms after seen) then the display title `Numbers we did **not** have to explain away.` — Inter 600, 44/62 at 1440 (36/50 at 1024, 30/42 at 768, 24/32 at 390), tracking −2px, white→`#bababa` vertical gradient clipped to the text, the word *not* in Instrument Serif italic 400 at 50px (40 / 32 / 26) in `#d6dadd`, `line-height: 1`. The title focuses in 70ms after the wrapper: opacity 0→1, `blur(10px)→0`, and a mask `linear-gradient(90deg, black calc(var(--reveal) − 40%), transparent var(--reveal))` sweeping `--reveal` 0%→140%, all on the soft spring. Aside `<p>` "Counted across every account, including the ones that churned. An honest average, not a best case." — 16/24 Inter `#9ba0a6`, absolute top 140, left 920, width 440, Reveal *fade* 140ms after seen.
- 1024: head wrapper top 40, left 48, width 928, centred, gap 24; title 928 wide centred; aside top 166, left 266, width 492, centred. 768: wrapper left 40 width 688; title 688 wide; aside left 177, width 415. 390: wrapper top 48, left 20, width 350, gap 12; title 271 wide; aside top 177, left 31, width 328.
**Hairlines.** Two 1px `rgba(255,255,255,.09)` rules, 1280 wide from x80 (928 from 48 at 1024; 688 from 40 at 768; none at 390), at y300 and y626 (246 / 643 at 1024; 246 / 657 at 768). Each draws itself from the left (`scaleX` 0→1, `transform-origin: left`, soft spring) 210ms and 350ms after being seen.
**Stat columns.** A `<ul>` of three `<li>` boxes, each `absolute` at top 332, 384×456, at x **80 / 528 / 976** (1024: top 295, 278×521, x 48 / 372 / 696; 768: 218×571, x 40 / 274 / 508). At 390 the list becomes `absolute; top 281; left 20; width 350; flex column; gap 12` and each `li` is a card in flow: `overflow:clip; border-radius:24px; border:1px solid rgba(255,255,255,.07); background:#0c0d0e; padding:20px`, everything inside static/flowing.
Inside a column (offsets from the column top at 1440 / 1024 / 768; at 390 the rows flow with margins: number +8, caption +12, visual +24, pill +24, card hairline +24, quote +24):
| element | 1440 | 1024 | 768 | style |
|---|---|---|---|---|
| label | top 0 | 0 | 0 | Fragment Mono 14/20 `#9ba0a6`, types itself in |
| number | top 32 | 28 | 28 | Inter 600, **56/60** (44/48 · 36/44 · 28/34 at 390), tracking −2px, white |
| caption | top 104, w 384 | 92, w 278 | 84, w 218 | Fragment Mono 14/20 `#9ba0a6` |
| visual box | top 148, 384×56 | 156, 278×56 | 148, 218×56 | see below (390: 310 wide; the progress box is only the 14px track) |
| trend pill | top 226 | 236, **278×64** flex, centred text, wraps | 228, 218 wide | Fragment Mono 14/20, `padding: 5px 11px`, radius 8 (6 at 390), `background: rgba(255,255,255,.04)`, 1px border `rgba(200,247,81,.45)` (from 1024 down: `rgba(255,255,255,.1)`), text `#c8f751` — from 1024 down only the FIRST column's pill stays lime, the others are `#9ba0a6`; at 1024 padding 11px 15px; at 768 the middle pill's text is boxed at 140 wide so it wraps |
| column tick | top 293, 28×2, radius 999, `rgba(255,255,255,.25)` | 347 | 361 | draws from the left; at 390 only the first card keeps it (at 306, left 20) |
| quote figure | top 326, w 384 | 381, w 278 | 395, w 218 | blockquote 16/24 white; figcaption `margin-top 40` (24 at 1024; at 768 `absolute; top 112` under the quote top; 12 at 390), column, gap 2 (4 at 1024): name Inter 500 14/20 white, role Fragment Mono 14/20 `#747980`, nowrap (wrap at 768) |
| card hairline (390 only) | — | — | — | 1px `rgba(255,255,255,.09)`, full width, draws from the left |
Copy, verbatim:
1. `volume` · **62,000** · "runs handled on an average weekday" · sparkline · "up 18% since March" · “We killed four scripts nobody wanted to own and a spreadsheet that had been lying to us.” — **Marta Silvani**, head of operations, Fielder
2. `time back` · **11** · "hours a week given back per person" · hour bars · "measured across 1,400 teams" · “Finance stopped asking me for the weekly export. It just lands in the channel now.” — **Owen Brandt**, data lead, Cassette
3. `first attempt` · **99%** · "of runs finish on the first attempt" · progress track · "the other 1% retried and passed" · “What sold my team was the log. When something breaks we see the exact payload.” — **Tom Ekelund**, cto, Bright Coast
**The three mini visuals** (inline SVG / DOM, 56 tall; they LOOP while on screen — see the clock below):
*Sparkline* (column 1). An SVG `viewBox 0 0 <box> 56`, `overflow: visible`, box **384** at 1440 (line to x378, "now" dot at 378), 278 at 1024 (line to 278, dot at 277), 218 at 768 (dot 217), 310 at 390 (dot 309). The 15 sample y-values, evenly spaced from x0 to the line end: `52 46 48 40 42 34 28 30 22 24 16 18 10 12 4`. Layers, all clipped to `rect(−1,−4, box+6 × 64)`: an area path (the line closed `V56 H0 V52 Z`) filled with a vertical gradient `#c8f751` .22 → 0; the line itself, `stroke-width 2`, round joins, stroked with a `userSpaceOnUse` gradient from (0,4) to (22.961, 94.2887) [(29.6896, 89.8616) / (35.4876, 84.4794) / (27.1935, 91.6954) per frame] — `#e8ff9f` at 41.71% → `#c8f751` at 100%; a lime dot r3 at (dot, 4); a 1×52 "now" rule at x dot from y4 filled `#c8f751` .9 → .1 vertically; and, outside the clip, a halo circle at the dot, 1px lime stroke. Play (clock `t` in ms): the line draws over **720ms** (`stroke-dashoffset` 1→0 with `pathLength 1`, easeInOutCubic); the area fades in from t=240 over 480 (easeOutCubic); the dot and the now-rule pop in from t=720 over 96 (linear); the halo's radius runs 3→11 over 560 from t=720 (easeOutCubic) while its opacity is `0.8 × dot × (1 − pulse)`. PLAY = 1280.
*Hour bars* (column 2). 11 lit rounded bars (rx 6, 56 tall) plus 3 static dim ones. Lit SVG: width **300**, bars 20 wide on a 28 pitch (1024: 218 wide, 14.5332 on 20.3467; 768: 170.429, 11.8574 on 15.8571; 390: 242.615, 18 on 22.4615), filled with a diagonal gradient from (0,0) to (36.7842, 98.398) [(45.6208, 88.6796) / (51.5176, 78.2171) / (42.6534, 92.4756)] `#e8ff9f` @41.71% → `#c8f751`. Dim bars: `absolute; top 22; 20×34; radius 6; rgba(244,245,246,.06)` at left 308 / 336 / 364 (1024: 16 wide at 222 / 242 / 262; 768: 12 wide at 174 / 190 / 206; 390: 18 wide at 247.08 / 269.54 / 292). Play: bar *i* grows from the bottom (`y = 56 − h`, `h = 56 × easeOutCubic`) over **520ms**, starting **48ms** after the previous one. PLAY = 1000.
*Progress run* (column 3). A track `absolute; top 20; 384×14` (278 at 1024, top 21; 218 at 768; at 390 the whole box is the 310-wide track at top 0), `overflow: clip; radius 999; background rgba(255,255,255,.07)`, holding a full-size fill with `linear-gradient(175.82deg, #e8ff9f 27.819%, #c8f751 66.696%)` (174.24° / 172.67° / 174.83° per frame), revealed with `clip-path: inset(0 <(1−p)·100>% 0 0 round 999px)` (dropped to `none` at p ≥ 1). `p(t)`: runs to 0.99 over 880ms (easeOutCubic); pauses 176; stumbles back 0.018 over 128 (easeOutQuad); waits 96; then pushes 0.972→1 over 336 (easeInOutCubic). PLAY = 1616.
**The loop clock.** Each visual owns one linear clock `t` from 0 to `PLAY + 2720 (hold the Figma frame) + 360 (fade the drawn marks out: opacity 1 − easeInOutQuad) + 280 (empty gap)`, restarting from 0 while the visual is *seen*. "Seen" here re-arms: true once the element crosses an IntersectionObserver line with `rootMargin "0px 0px -12% 0px"` (or the page bottom is reached with it on screen), false only when a plain observer reports it fully off screen — then the clock stops and parks at 0 so the next entrance replays from the first frame. First play after seen is delayed by the column stagger `i × 112ms`. Reduced motion parks the clock at PLAY (the resting frame).
**Cascade (ms, relative to the section head being seen; STEP = 70):** hairline top 210, hairline bottom 350; column *i* starts at `210 + i×140`: its label types (30ms per character, clip-path on whole `ch` units), the number counts up 70ms later, the caption focuses in (blur 8 → 0 + mask sweep, soft spring) at +140, the visual first plays at `i × 112` after ITS OWN seen, the trend pill rises (Reveal *rise*) at `column start + 210 + visualLength` (880 sparkline / 1000 hours / 1600 progress), and the quote row starts at `i × 90` after the figure is seen: quote text, then name +150, role +220, each a blur-8 focus-in on a quicker spring **260/30**; the tick / card hairline draw at the same `i × 90`.
**Count-up.** Every time the number is seen (same re-arming rule) it counts from 0 to its value over **1400ms** easeOutCubic, digits `tabular-nums` while counting (`62,000` grouped en-US, `99%` keeps its suffix), switching back to proportional figures and the exact copy on landing; parks at 0 when off screen; reduced motion shows the value. The real value stays in a visually-hidden span.
### 09 · Pricing — `<section id="pricing">`
After the frosted slab, with no scroll wrapper: the cards are simply there, only their contents animate in (client call). Section root `position: relative`, `content-visibility: auto`, explicit height **878px** at 1440 (1120 at 1024, 1664 at 768, 2131 at 390). Three things stacked at frame coordinates: the section head, one wide **run meter** card, and a row of three **plan cards**.
**Head.** The shared section head at top 0, left 80 (1024: top 80, left 48, width 928, stacked centred; 768: top 40, left 40, width 688; 390: top 48, left 20, width 350): badge "pricing" + display title `You pay for runs, not **for seats**.` (44/62 Inter 600 gradient white→`#bababa`, tracking −2px, *for seats* in Instrument Serif italic 50px `#d6dadd`) in a Reveal *rise* wrapper, title focusing in 70ms later; aside "Invite the whole company on any plan. Nobody should ask permission to automate their own job." 16/24 `#9ba0a6`, 440 wide, bottom-aligned 48 to the right, focusing in 140ms after seen (1024 down: 560 wide centred under the title; 390: full width).
**Run meter card** — `absolute; top 158; left 80; 1280×302` (1024: top 286, left 48, 928×308; 768: top 238, left 40, 688×423; 390: top 248, left 20, 350×542). `overflow: clip; border-radius 30px` (24 at ≤900px); 1px `rgba(255,255,255,.07)` border; `background rgba(14,15,17,.42)` with `backdrop-filter: blur(16px)` (glass — the stream shows through). Children absolute inside the border (1440 / 1024 / 768 / 390):
| element | 1440 | 1024 | 768 | 390 | style |
|---|---|---|---|---|---|
| eyebrow `estimate` | top 39, left 39 | 68, 23 | 23, 23 | 23, 19 | Fragment Mono 14/20 `#747980`, types in at 0ms |
| question (a `<label for=range>`) `How many runs do you expect a month?` | top 67, left 39 | 96, 23 | 51, 23 | 51, 19, width 264, wraps | Inter 500 **20/28**, tracking −1px, white; focus-in (blur 8, mask sweep, soft spring) at 70ms |
| range input (invisible) | top 142, left 39, 860×26 | 171, 23, 616 | 98, 36, 614 | 132, 32, 284 | `appearance:none; opacity:0; cursor:pointer`, thumb 1×1 |
| track | top 149, left 39, 860×12 | 178, 23, 616 | 105, 36, **640** | 139, 19, **310** | `overflow:clip; radius 999; rgba(255,255,255,.08)`; a 2px lime outline, offset 4, when the input is focus-visible |
| fill | width `f × 860` | `f × 616` | `13 + f × 614` | `13 + f × 284` | `linear-gradient(to right, rgba(200,247,81,.35), rgba(200,247,81,.95))` |
| knob box | top 124, left 39, 860×76 | 153, 23, 616 | 80, 36, 614 | 114, 32, 284 | holds `pricing-knob.svg` 70×76 (a lime r13 disc with two drop shadows) at `left: f×100% − 35px` |
| ticks (1×10, `rgba(255,255,255,.14)`, active `.3`) | top 173; x 39 / 412 / 799 | 202; 23 / 288 / 639 | 129; 36 / 298 / 663 | 163; 32 / 152 / 316 | |
| stop labels `3,000` `30,000` `300,000` | top 189; x 39 / 392 / 779 | 218; 23 / 262 / 578 | 145; 36 / 272 / 602 | 179; 19 / 126 / 268 | Fragment Mono 14/20, `#747980`, the active one white; type in at 140 / 210 / 280ms |
| vertical rule | top 40, left 967, 1×222, `rgba(255,255,255,.09)` | 23, 671, ×260 | none | a horizontal 1px at top 223, left 19, 310 wide | |
| result eyebrow `your plan` | top 39, left 1007 | 23, 703 | 197, 23 | 248, 19 | mono `#747980`, types at 240ms |
| plan name | top 63, left 1007 | 51, 703 | 221, 23 | 276, 19 | Inter 500 20/28 tracking −1px white; focus-in at 310ms; text swaps (below) |
| price + period row | top 107, left 1007, `flex; align-items:flex-end; gap 17` | 91, 703, column, gap 2 | 197, **right 23**, items end, gap 4 | 316, left 19, column, gap 2 | price Inter 600 **44/48** (36/50 at ≤1200 and 390) tracking −2px white; `a month` mono 14/20 `#9ba0a6`, types at 240 |
| result rule | top 167, left 1007, 232×1 `rgba(255,255,255,.1)` | 175, 703, 200 | 285, 23, 640 | 400, 19, 310 | |
| included line | top 181, left 1007 | 199, 703 | 310, 23 | 425, 19 | mono `#9ba0a6`; focus-in at 450ms (its length changes, so it never types); text swaps |
| CTA `Start free` | top 213, left 1007, 232 wide | 235, 703, 200 | 350, 23, 640 | 469, 19, 310 | the shared lime pill, lg (48 tall, 16/24 Inter 500, `#c8f751` fill, `#0a0b09` text) |
*Slider mapping.* State is a **position in decades** `pos` above the first tier: 0 = 3,000, 1 = 30,000, 2 = 300,000; rest = **1**. Per frame the knob's travel and the knob centres for the three stops: 1440 `length 860, stops [0, 373, 760]` (the track runs 60px past 300,000 — max pos ≈ 2.258); 1024 `616, [0, 265, 616]`; 768 `614, [0, 262, 614]`; 390 `284, [0, 120, 284]`. `px(pos) = stops[i] + (pos−i)·(stops[i+1]−stops[i])` with `i = pos ≤ 1 ? 0 : 1`, and the inverse for the input (`min 0, max length, step 1`, value = px). `runs(pos) = round(tier[i].runs × 10^(pos−i) / 100) × 100`; the plan is the first tier whose `runs` ≥ that (Starter ≤ 3,000, Team ≤ 30,000, else Scale). The drawn knob + fill follow `px/length` on a spring **280/32** (`--pricing-fill`); the input's `aria-valuetext` reads "<runs> runs a month, <Plan> plan". On a frame change the position is clamped to the new track's max.
*Price roll.* The price is `$` + as many digit columns as the longest price (3); each column is a vertical strip of `0…9` plus a blank row moved with `top: −value × 100%` on a spring **180/31.72** (soft ×1.5 / ×1.22), column *n* delayed `n × 56ms`; each column also springs its width to the glyph it lands on (widths measured once fonts are ready; a blank column closes to 0) so "a month" glides. Empty leading columns hold a zero-width space so the baseline stays. *Swap text* (plan name, included line): the old value fades out to `blur(6px)` while the new one fades in from it, both on the soft spring 120/26, stacked in one grid cell.
**Plan cards.** Card row box `absolute; top 480; left 80; 1280×398` (1024: top 606, left 48, 928×434; 768: top 673, left 40, 688×957; 390: top 802, left 20, 350×1281), `flex; gap 20` (12 at ≤1200; column at ≤900). Columns: **413 / 413 / the rest (414)** at 1440; three equal at 1024; full width, **311** tall each at 768; 417 / 423 / 417 at 390. Three layers share exactly that flex box: a glow layer under the cards (`border-radius 30; box-shadow 0 0 24px rgba(214,248,76,.12), 0 18px 44px −10px rgba(0,0,0,.6)`), the cards, and a stroke layer above them (`.stroke-ring` 1px ring painted with `linear-gradient(to bottom right, #6a8230 0%, #607237 10%, #56613b 20%, #49513a 30%, #3c4035 40%, #2d2f2c 50%, #1d1e1f 60%, #1d1e1f 100%)`). One card at a time is **lit**: the one under a fine pointer, otherwise the slider's plan (Team at rest). Each card's glow and stroke opacity spring to 1/0 on the soft spring **120/26** — a cross-fade in place, nothing slides.
Card: `relative; 100%; overflow:clip; radius 30 (24 ≤900); 1px rgba(255,255,255,.07); background rgba(14,15,17,.42); backdrop-filter blur(16px)` (these keep their own blur). Inside (1440 / 1024 / 768 / 390; the featured card's text block sits 6px lower at 1024 and 390, 8px at 768):
| element | 1440 | 1024 | 768 | 390 |
|---|---|---|---|---|
| name `<h3>` mono 14/20 `#9ba0a6` | top 31, left 31 | 23 (29 featured), 23 | 31 (39), 31 | 31 (35), 19 |
| price 44/48 → 36/50 + period mono `#747980` | top 57, column gap 10 | 57 (63), gap 4 | 67 (75), **row**, items end, gap 12, period `margin-bottom 7` | 57 (63), column gap 4 |
| tag `most picked` (Team only) | top 31, left 260, radius 8, `#c8f751`, `#0a0b09`, padding 6 12, mono 14/20 | 23, 153, py 8 | 31, 543 | 31, 209 |
| feature list | top 161, left 31, rows **32** tall, gap 10, text 280 wide | 157, 23, rows 36, gap 14, text 229 | 133 (141), 2-col grid, col-gap 24, rows 32, text 284 | 151 (157), 19, rows 36, text 286 |
| hairline above buttons | none | top 325, left 23, 253×1 | 214, 640 | 307 (313), 19, 310 |
| glass button box | top 317, left/right 31, 48 tall | 349, 23, **60** tall | 238, 48 | 332 (338), 19, 60 |
| shadow behind glass button | top 331, l/r 35, 40 tall, `rgba(0,0,0,.55)`, `filter: blur(13px)`, radius 999 | 363, 27, 52 | 252, 40 | 346 (352), 23, 52 |
Copy: **Starter** `$29` · 3,000 runs a month (14/20) · 10 workflows (14/20) · 7 days of run history (16/24) · email support (14/20) · `Start free`. **Team** — tag `most picked` — `$89` · 30,000 runs a month · unlimited workflows · 90 days of run history · Slack approvals and code steps (all 16/24) · `Start free`. **Scale** `$290` · 300,000 runs a month · single sign on and audit log · 1 year of run history · named engineer, 4 hour response · `Talk to us` (→ `#contact`). At 768 every 14/20 line becomes 16/24; at 390 every line is 16/24 on a 36 pitch. Each feature has an inline check SVG: 16×16 `M3.5 8.5L6.5 11.5L12.5 4.5` at 1440, a narrower 10×16 `M1 8.5L2.77778 11.5L6.33333 4.5` from 1024 down; stroke 2, round caps, `#a7a7a7` (`#9ba0a6` on Team), `margin-top 2` (6 at 1024, 4 at 390).
*Two button looks per card, always both mounted, cross-fading* on the soft spring with the lit state (the hidden one is `aria-hidden` + `inert` + `pointer-events:none`): the **glass tier button** — `flex; 48 tall (60 at 1024/390); radius 999; 1px rgba(255,255,255,.16); background rgba(255,255,255,.07); Inter 500 16/24 #f4f5f6` — whose hover fills it with lime from the side the pointer entered (`clip-path: inset(0 <rest>% 0 0 round 999px)` growing from the left, or `inset(0 0 0 <rest>%)` from the right, soft spring 120/26; the fill layer is an inset −1px lime pill carrying an ink copy of the label so it stays readable), press scales to .97 (spring 280/32); and the **lime pill** (the page's shared lime CTA, full box, 16/24) sitting on a `0 0 18px rgba(214,248,76,.18)` glow that fades out (spring 280/32) while the pill is hovered.
**Motion table (delays in ms after *seen*; STEP = 70):**
- Meter: eyebrow types at 0 (30ms/char); question focus-in at 70; stop labels type at 140 / 210 / 280; result eyebrow and `a month` type at 240; plan name focus-in at 310; included line focus-in at 450.
- Card *n* (delay `d = (n+2)×70` = 140 / 210 / 280): name types at `d`; price focus-in (soft spring) at `d+70`; period types at `d+140` — off the CARD being seen; `most picked` pops in (opacity 0→1, scale .6→1, spring **320/17**, overshoots) at `d+280`; check *i* pops (opacity + scale .5→1, spring 420/39) at `d + (1+i)×40` and draws its stroke (`stroke-dashoffset` 1→0, spring 180/31.72) 48ms after; feature line *i* focus-in (blur 8, mask sweep, spring 280/32) at `d + (2+i)×40` — checks and lines run off the card being seen, not each line.
- Lit-card cross-fade, glass-button fill, lime pill hover/press: on interaction, as above. Hover lighting only on `(hover:hover) and (pointer:fine)`.
Assets: `https://storage.getlayers.ai/assets/relay-4ee0729d77/pricing/pricing-knob.svg` (the check SVGs are inlined; their sources `pricing-check.svg` / `pricing-check-light.svg` are the same 16×16 path in `#A7A7A7` / `#9BA0A6`).
### 10 · Footer — `<footer id="footer" aria-label="Site footer">`
The last block in `<main>`, after a section gap. It follows Pricing by plain
scroll. `position: relative`, `content-visibility: auto` (so its box clips
what bleeds past it — the aurora glows are drawn to overhang upward but are
cut at the footer's top edge exactly as in the project), explicit height:
**764 px** at 1440 · 646 at 1024 · 1084 at 768 · 1237 at 390. Every child is
absolutely positioned at the frame's own coordinates (design px below; the
page is a rem grid, 1 rem = 16 design px of the active frame, so divide by
16 for rem).
Every asset lives at `https://storage.getlayers.ai/assets/relay-4ee0729d77/footer/`.
Mono type everywhere below = Fragment Mono 14/20 (`--type-label` /
`--line-label`), `white-space: nowrap`.
#### Section tokens (Tier 1 → Tier 2, verbatim)
```css
--raw-footer-color-panel-from: #202326; --raw-footer-color-panel-via: #111214; --raw-footer-color-panel-to: #0a0b0c;
--raw-footer-color-white-a12: rgba(255,255,255,.12); /* brand panel border */
--raw-footer-color-white-a14: rgba(255,255,255,.14); /* panel hairline, social chip border */
--raw-footer-color-white-a09: rgba(255,255,255,.09); /* page hairlines */
--raw-footer-color-white-a80: rgba(255,255,255,.8); /* social chip label */
--raw-footer-color-white-a25: rgba(255,255,255,.25); /* logo mark border */
--raw-footer-color-mark-from: #e2ff8f; /* logo mark gradient 0% (→ #c8f751 at 71.429%) */
--raw-footer-color-ink-a45: rgba(10,11,9,.45); /* email placeholder */
--raw-footer-color-ink-a50: rgba(10,11,9,.5); /* "what now" */
--raw-footer-color-ink-a60: rgba(10,11,9,.6); /* CTA copy */
--raw-footer-color-signup: #fafbfc; /* signup pill */
--raw-footer-color-ink-a15: rgba(10,11,9,.15); /* signup focus ring */
--raw-footer-font-size-36: 2.25rem; --raw-footer-font-size-40: 2.5rem; --raw-footer-font-size-32: 2rem; --raw-footer-font-size-26: 1.625rem;
--raw-footer-line-height-52: 3.25rem; --raw-footer-line-height-42: 2.625rem; --raw-footer-line-height-34: 2.125rem; --raw-footer-line-height-26: 1.625rem;
--raw-footer-letter-spacing-1: 0.0625rem;
--raw-footer-radius-24: 1.5rem; --raw-footer-radius-8: 0.5rem; --raw-footer-radius-4: 0.25rem;
```
Semantic names: `--footer-panel-from/-via/-to`, `--footer-panel-border` (a12),
`--footer-panel-hairline` (a14), `--footer-social-fill` (= `--raw-color-white-a10`),
`--footer-social-border` (a14), `--footer-social-label` (a80), `--footer-hairline`
(a09), `--footer-mark-border` (a25), `--footer-mark-from` (#e2ff8f),
`--footer-mark-to` (= lime #c8f751), `--footer-ink-placeholder/-eyebrow/-copy`,
`--footer-signup-fill`, `--footer-signup-ring`, `--footer-type-cta-title` 36 /
`--footer-type-cta-accent` 40 / `--footer-line-cta-title` 52, wordmark 20/28,
`--footer-letter-spacing-mono` 1px, panel corner 24, mark corner 8, chip corner 8.
Frame overrides: ≤1200 px → CTA title 32 / accent 32 / line 42; ≤900 → 26 /
26 / 34 and wordmark 18/26; ≤580 → 32 / 32 / 42, wordmark back to 20/28, chip
corner 4.
#### Glows (`<div aria-hidden>`, pointer-events none, drawn first)
Three `<img>` (PNG exports carrying an 80 px blur, so each box is the ellipse
grown by 160 on every side), `position: absolute; max-width: none`:
| image | 1440 (top / left / h / w) | 1024 | 768 | 390 |
|---|---|---|---|---|
| `footer-bloom.png` 940×680 | −100 / 740 / 680 / 940 | −192 / −264 / w 1552 | −160 / −198 / w 1164 | −232 / −101 / w 591 |
| `footer-aurora-blue.png` 1420×800 | −288 / 40 / 800 / 1420 | −192 / −149 / w 1322 | −160 / −112 / w 991 | −232 / −57 / w 503 |
| `footer-aurora-violet.png` 960×700 | −68 / 780 / 700 / 960 | −192 / −256 / w 1536 | −160 / −192 / w 1152 | −232 / −98 / w 585 |
Heights never change; at the narrower frames all three sit at x 0 of the
frame and are stretched horizontally by frame-width ÷ 1440-ellipse-width.
#### Brand panel (`.footer-brand`) — 530 × 320 at (80, 96)
`overflow: clip; border-radius: 24px; border: 1px solid rgba(255,255,255,.12);
background: linear-gradient(148.88deg in srgb, #202326, #111214 55%, #0a0b0c);
background-origin: border-box`. 1024: 459 × 278 at (48, 0). 768: 688 × 278 at
(40, 40). 390: 350 × 309 at (20, 0). Children are placed from the padding box
(inside the 1px border).
- **Panel glyph** — inline SVG, `viewBox 0 0 300 300`, `overflow: visible`,
absolute, 300 × 300 at (−40, 299); two round-capped white chevrons, stroke
26: back `M64 84L124 150L64 216` at opacity .06, front `M124 84L184 150L124 216`
at opacity .12. 1024: (−48, 235). 768: 285 × 285 at (−44, 477) with
`clip-path: inset(68px 98px 67px 49px)`. 390: 228 × 228 at (−35, 179),
`clip-path: inset(55px 77px 53px 40px)`.
- **Logo link** `<a href="/">` at (31, 31), flex, gap 12: a 28 × 28 lime tile
(`border-radius: 8px; border: 1px solid rgba(255,255,255,.25); background:
linear-gradient(135deg in srgb, #e2ff8f, #c8f751 71.429%); background-origin:
border-box`) holding `footer-logo-glyph.svg` (28 × 28, offset −1/−1) + the
wordmark "Relay" — Inter 20/28, weight 500, letter-spacing −1px, white. 1024:
(23, 23). 390: left 19.
- **Tagline** "Automation for the work that lives between your tools." —
Inter 18/28 white, 360 wide at (31, 166). 1024: (23, 132), 247 wide. 768:
top 124, 292 wide. 390: (19, 155), 310 wide.
- **Panel hairline** 1 px, rgba(255,255,255,.14), 466 wide at (31, 238). 1024:
(23, 204) 411 wide. 768: top 196, 640 wide. 390: (19, 235) 310 wide.
- **Social row**: label "stay in touch" mono 14/20, letter-spacing 1px,
`--foreground-muted` (#9ba0a6) at (31, 267); `<ul aria-labelledby>` at
(345, 255), flex gap 8, four 32 × 32 chips (`border-radius: 8px; border: 1px
solid rgba(255,255,255,.14); background: rgba(255,255,255,.1)`) with mono
labels `in` / `x` / `gh` / `dc` in rgba(255,255,255,.8) (aria-labels
LinkedIn / X / GitHub / Discord, hrefs `#linkedin` `#x` `#github`
`#discord`); hover → white, 150 ms `cubic-bezier(.2,0,0,1)` colour
transition. 1024: label (23, 233), list (282, 221). 768: label top 233, list
left 511. 390: label (19, 264), list (177, 252), chips corner 4.
#### CTA panel (`.footer-cta`) — 730 × 308 at (630, 96), lime
`overflow: clip; border-radius: 24px; background: #c8f751 (--accent);
box-shadow: 0 8px 22px -4px rgba(0,0,0,.5)`. 1024: 457 × 278 at (519, 0). 768:
688 × 278 at (40, 330). 390: 350 × 340 at (20, 321).
- **Eyebrow** "what now" — mono 14/20, rgba(10,11,9,.5), at (40, 40); 1024 (24,
24); 390 left 20. Types itself in (see motion).
- **Title** `<h2>` at (76, 40), 650 wide, `overflow: clip` (it is the mask
the line rises out of): "Pick the task you *hate* most." — Inter 36/52
weight 600, letter-spacing −1px, ink #0a0b09 (`--on-accent`); "hate" is
Instrument Serif italic 40, line-height 52, weight 400. 1024: (60, 24), 409
wide, 32/42 with the serif word also 32. 768: top 68, 624 wide, 26/34. 390:
(60, 20), 254 wide, 32/42 — wraps to two lines.
- **Copy** "Bring one job nobody enjoys doing by hand. If Relay cannot run it
by the end of the trial, we will tell you so." — Inter 16/24,
rgba(10,11,9,.6), 650 wide at (136, 40). 1024: (122, 24), 409 wide. 768: top
126, 432 wide. 390: (160, 20), 310 wide.
- **Signup form** wrapper at (228, 40); 1024 (198, 24); 390 (260, 20).
`<form>`: flex, 650 × 52, gap 8, `overflow: clip; border-radius: 9999px;
background: #fafbfc; padding: 6px 6px 6px 20px`. 1024: 409 × 56, padding 8 /
20 / 8 / 20. 768: 640 wide, padding-right 8. 390: 310 wide.
- a focus ring `<span>` filling the pill: `box-shadow: inset 0 0 0 1px
rgba(10,11,9,.15)`, opacity 0 at rest;
- `<label class="sr-only" for="footer-email">Work email</label>` +
`<input id="footer-email" name="email" type="email" required
autocomplete="email" placeholder="Work email">` — 24 tall, flex 1, Inter
16/24 ink, placeholder rgba(10,11,9,.45), no outline;
- `<button type="submit">` — flex, 40 tall, `border-radius: 9999px;
background: #0a0b09; padding: 0 24px`, Inter 14/20 weight 500, lime text.
Inside: a relative label span (`aria-live="polite"`) holding the visible
"Start free" and, absolutely over it (top 0, left 0, flex, gap 6, opacity
0), a 14 × 14 check icon (`M2.5 7.5l3 3 6-7`, stroke 1.8, round) + "Sent";
then an arrow slot (flex, `justify-content: flex-end; overflow: clip`,
width 0, opacity 0) holding a 14 × 14 arrow icon (`M2 7h10M8 3l4 4-4 4`,
stroke 1.6, round).
#### Status row (`<ul>`) — y 464, 20 tall, full width
Three `<li>` (flex, gap 8, mono 14/20, #9ba0a6) at x 80 / 588 / 1040: a 6 × 6
lime dot (`footer-status-dot.svg`, a #C8F751 circle) + the label. Labels (and
their ≤900 px short form): "all systems normal" (same), "99% pass on the first
attempt" ("99% first-attempt pass"), "three regions, no cross border hops"
("three regions, no hops"). 1024: top 326, x 48 / 306 / 659. 768: top 648, x
40 / 264 / 523. 390: a column at x 20, top 693, rows at y 0 / 20 / 40 (row
60 tall). At rest each dot sits at opacity .2, scale .6.
#### Hairlines, company block, link columns, legal
- **Bottom hairline** 1 px rgba(255,255,255,.09), 1280 wide at (502, 80); 1024
(362, 48) 928 wide; 768 (684, 40) 688 wide; 390 (769, 20) 350 wide.
- **Company block** at (533, 80), 400 wide: a 26 × 26 lime tile (same gradient
as the logo mark, `footer-labs-glyph.svg`), "Relay Labs" Inter 16/24 weight
500 white at (1, 36), "Independent, profitable, and boring on purpose since
2024." Inter 16/24 #9ba0a6 at (42, 0) 400 wide, "SOC 2 Type II / audit log /
data residency" mono 14/20 `--foreground-dim` (#747980) at (110, 0). 1024:
(395, 48), 300 wide; name left 38; copy 248 wide; compliance 237 wide,
wrapping. 768: (709, 40), 688 wide; name 18 px; compliance top 86. 390: (794,
20), 350 wide; name 16 px; compliance top 110.
- **Four columns** `<nav aria-label="Footer">`: each a flex column, gap 12,
top 533, at x 630 / 803 / 973 / 1132: a mono 14/20 #747980 title
(`product` · `learn` · `company` · `legal`) then a `<ul>` (flex column, gap
6; 8 at 1024) of Inter 14/20 #9ba0a6 links, hover / focus-visible → lime
#c8f751 over 150 ms. Links: product → Canvas `#product`, Apps `#apps`, Run
history `#run-history`, Pricing `#pricing`; learn → Docs `#docs`, Templates
`#templates`, Changelog `#changelog`, Status `#status`; company → About
`#about`, Careers `#careers`, Security `#security`, Contact `#contact`;
legal → Privacy `#privacy`, Terms `#terms`, Cookies `#cookies`. 1024: top
402, x 396 / 547 / 698 / 849. 768: top 855, x 40 / 216 / 392 / 568. 390: top
984, x 20 / 111 / 202 / 293.
- **Foot hairline** at (695, 80), same width as the bottom one; 1024 top 577;
768 top 1015; 390 top 1144.
- **Legal band** at (720, 80): flex, `justify-content: space-between`, 1280 ×
26, mono 14/20 #747980: "2026, all rights reserved" left, "built in Lisbon
and Kraków" right-aligned. 1024: (602, 48), 928 × 20. 768: (1040, 40), 688
wide. 390: (1169, 20), 350 wide, stacked (column, gap 8), both left-aligned.
#### Motion — one footer timeline (`stage.tsx`)
Two invisible sentinel rows drive everything, so the footer enters as one
calm sequence rather than every block firing on its own observer: **top** =
the panel row (80, 96, 1280 × 320; 1024: 48, 0, 928 × 278; 768: 40, 40, 688 ×
568; 390: 20, 0, 350 × 661) and **bottom** = the links row (80, 533, 1280 ×
162; 1024: 48, 395, 928 × 150; 768: 40, 709, 688 × 282; 390: 20, 794, 350 ×
326).
- `top` is ready when the panel row is *seen* (IntersectionObserver with
rootMargin `0px 0px 5% 0px`, anything on screen at load counts at once, and
reaching the bottom of the page counts for whatever is on screen — the
footer sits in the last fifth of the page, so this fallback is what lets it
play).
- `bottom` is ready once its own row intersects (rootMargin `0px 0px 5% 0px`)
**and** at least 350 ms have passed since `top` became ready.
- the ambient loop (panel glyph) unlocks 1200 ms after `top`.
Every cue is a once-only spring, from → to after its delay (ms after its
stage is ready), and stays. Springs: **SPRING** `{tension 280, friction 32}`,
**SPRING_SOFT** `{tension 120, friction 26}`.
| element | cue | from → to | spring | stage · delay |
|---|---|---|---|---|
| CTA panel | rise | opacity 0→1, translateY 24px→0 | SPRING | top · 0 |
| brand panel | rise | same | SPRING | top · 50 |
| eyebrow "what now" | type | `--typed` 0 → 8.5, clip-path `inset(0 calc(100% − round(down, calc(var(--typed) * 1ch), 1ch) − 0.1ch) 0 0)` | linear, 8 × 30 = 240 ms | top · 160 |
| CTA title line | line-rise | translateY 108% → 0 inside the clipping `<h2>` | SPRING | top · 160 |
| CTA copy | focus | opacity 0→1, `filter: blur(8px)→blur(0)`, `--reveal` 0%→140% behind `mask-image: linear-gradient(90deg, black calc(var(--reveal) − 40%), transparent var(--reveal))` | SPRING_SOFT | top · 260 |
| signup form wrapper | rise | opacity 0→1, y 24→0 | SPRING | top · 350 |
| brand tagline | focus | as the copy | SPRING_SOFT | top · 430 |
| brand hairline | draw | `transform-origin: left`, scaleX 0→1 | SPRING_SOFT | top · 490 |
| social label + chips (one wrapper) | fade | opacity 0→1 | SPRING_SOFT | top · 540 |
| status dot i (0,1,2) | light | opacity .2→1 (0.2 + 0.8·on), scale .6→1 (0.6 + 0.4·on) | SPRING | top · 620 + i·130 |
| status label i | fade | opacity 0→1 | SPRING_SOFT | top · 680 + i·130 |
| bottom hairline | draw | scaleX 0→1 | SPRING_SOFT | bottom · 0 |
| company block | rise | opacity 0→1, y 24→0 | SPRING | bottom · 40 |
| column i (0–3) | rise | same | SPRING | bottom · 100 + i·50 |
| foot hairline | draw | scaleX 0→1 | SPRING_SOFT | bottom · 300 |
| legal band | fade | opacity 0→1 | SPRING_SOFT | bottom · 380 |
Nothing inside the columns types; link hover is a plain colour transition.
**Panel glyph loop** ("step forward") — runs only while the glyph is on
screen, the loop is unlocked, and reduced motion is off; one spring `p`
(config `{tension 420, friction 39.04}` — SPRING ×1.5 tension, ×1.22 friction)
drives both chevrons: front chevron `translateX(26px · lead(p))` and stroke
opacity `.12 + .08·lead(p)`, back chevron `translateX(26px · trail(p))` and
opacity `.06 + .05·trail(p)`, where `lead(p) = min(1, p / 0.7)` and
`trail(p) = max(0, (p − 0.3) / 0.7)` (the back one trails by 30 % of the
step; translate is in SVG user units = CSS px on the 300-box). Sequence: wait
320 ms → spring p to 1 → hold 720 ms → spring p to 0 → rest 2080 ms → repeat.
Leaving the viewport stops it and snaps p to 0; coming back starts the cycle
from the 320 ms lead.
**Signup form** ("focus and send") — four values on one SPRING:
`ring = focused ? 1 : 0` (focus-ring opacity), `arrow = focused && !sent ? 1 :
0` (arrow slot width `arrow × 22px`, opacity `arrow`, the icon inside
`translateX(−6px · (1 − arrow))`), `done = sent ? 1 : 0` ("Start free" opacity
`1 − done` and `translateY(−8px · done)`; "✓ Sent" opacity `done` and
`translateY(8px · (1 − done))`), `width` = the measured width of "Start free"
→ of "✓ Sent" once sent (the label span gets `overflow: clip` and that
animated width, so the button narrows to fit). Submitting prevents the
default and only flips `sent` — the project posts nowhere — and the button
stays "✓ Sent". Native `required` / `type=email` validation gates the submit.
Reduced motion: every cue jumps to its end state, the glyph never moves.
## Fixed parameters (bake these in)
Every constant is given in its section above; the ones that recur everywhere:
- Springs: `SPRING = {tension: 280, friction: 32}` · `SPRING_SOFT = {tension: 120, friction: 26}` · `STEP = 70` ms.
- Reveal: rise `opacity 0→1, y 24px→0` (SPRING); fade `opacity 0→1` (SPRING_SOFT); title focus
`blur(10px)→0` + mask sweep `--reveal 0%→140%` (SPRING_SOFT); FocusText the same at 8px.
- Seen rule: a reveal starts when its element crosses an IntersectionObserver line with
`rootMargin: "0px 0px 5% 0px"`; anything on screen at load counts as seen at once; reaching the
bottom of the page counts everything on screen as seen. Loops start on `"0px 0px -12% 0px"`.
- Breakpoints: `≤1200px` (1024 frame), `≤900px` (768 frame), `≤580px` (390 frame); root font-size
`min(1.111111vw, 21.333px)` / `min(1.5625vw, 16px)` / `min(2.083333vw, 16px)` / `min(4.102564vw, 16px)`.
- Content insets 80 / 48 / 40 / 20 px; section gaps 72 / 64 / 56 / 48 px; nav slot 88 / 88 / 64 / 56 px.
- Stream: `CONFIG` as given verbatim; DPR 1.35 (full) / 1.2 (low); low tier ≤ 900px or coarse pointer.
- Reduced motion: every spring finishes immediately, loops render a still, pointer effects never attach.
## Assets
Every runtime asset lives under `ASSET_BASE_URL = https://storage.getlayers.ai/assets/relay-4ee0729d77`
at the same path it has under the project's `public/assets/`. Load each from that URL exactly as
the sections reference them (e.g. `https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-window-bloom.webp`).
The full list:
- https://storage.getlayers.ai/assets/relay-4ee0729d77/backdrop/backdrop-dot-field-768.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/backdrop/backdrop-dot-field.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/backdrop/backdrop-specks-and-marks.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/backdrop/field-texture.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/footer/footer-aurora-blue.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/footer/footer-aurora-violet.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/footer/footer-bloom.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/footer/footer-labs-glyph.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/footer/footer-logo-glyph.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/footer/footer-panel-glyph.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/footer/footer-status-dot.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-canvas-grid.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-connectors.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-field-mark.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-play-icon.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-port-overhang.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-port.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-tool-add.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-tool-extra.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-tool-redirect.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-tool-select.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/hero/hero-window-bloom.webp
- https://storage.getlayers.ai/assets/relay-4ee0729d77/integrations/integrations-bloom-1024.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/integrations/integrations-bloom-390.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/integrations/integrations-bloom-768.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/integrations/integrations-bloom.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/integrations/integrations-packet.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/nav/nav-logo-mark.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/pricing/pricing-check-light.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/pricing/pricing-check.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/pricing/pricing-knob.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-accent.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-avatar.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-bars.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cap-1.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cap-2.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cap-3.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cap-4.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cap-5.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cap-6.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cap-glow.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cube-1.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cube-2.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cube-3.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cube-4.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cube-5.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-cube-6.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-icon-approve.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-icon-check.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-icon-clock.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/product/product-icon-hold.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/proof/proof-bloom-left.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/proof/proof-bloom-right.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/proof/proof-hours.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/proof/proof-volume.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-bar-head.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-core-glow.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-lock.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-orbit-inner.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-orbit-outer.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-packet.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-runtime.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-wires-390.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-wires-768.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/scale/scale-wires.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-clock.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-curves.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-face-1.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-face-2.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-face-3.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-face-4.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-face-5.png
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-halo.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-legend-failed.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-legend-recovered.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-map.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-pin.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-region-ring.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/signals/signals-ring.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/statement/statement-mark-glyph.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/statement/statement-now-node.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/statement/statement-ruler-ticks.svg
- https://storage.getlayers.ai/assets/relay-4ee0729d77/ui/badge-dot.svg[/hidecontent]