meridian · motion · proposed 2026-07-26
That is one field of 2,400 dots, and it is never rebuilt between acts. It deforms: the logo's six strokes scatter onto the spinning globe, the globe collapses into your balance, the digits fall onto a price line. Every act is a real exotic artifact drawn from the real geometry, and each dot keeps its place on the meridian the whole way through, so you can follow one of them from the mark to the market. Move your cursor into it.
The stage exists to make one argument. We already own two pieces of motion nobody else has: the dot that breathes a ring on the landing globe, and the six strokes travelling on one sine in the mark. Everything below descends from one of those two, not as an homage but as the same functions re-scaled. A loading state should be recognisable as ours before you read a word of it.
Builders in js/motion-kit.js (window.MOTION_KIT),
styles in css/motion-kit.css. Every specimen below is live and running the real code.
Flip the theme, the colorway, or Reduced and watch each one answer honestly.
In letter/globe.js, a tradable name announces itself by breathing a ring out of
its dot on the lattice, and neighbours are offset so a handful are ever alive at once.
In js/exotic-brand.js, the mark is six brush-strokes on one travelling sine, each
lagging the last. Its crash mode rolls a swell across the box once and lets it recede.
Six posters in the Swiss line language: a single primitive per graphic, repeated with a parametric offset, drawn as strokes and never as fill. That last constraint isn't borrowed from a moodboard, it's the meridian law we already wrote down, which is why a line poster is the one poster format this brand can actually make. Each one carries a meaning rather than a mood, and each is assigned a page. Move your cursor through them — every one answers.
The dot atom at its own scale. One dot, one ring, 2.2 seconds. Everything that needs to say this number is live or that landed is this, and nothing else needs to be invented.
MOTION_KIT.ping(host, {tone})An ink dot with one ring of brand light breathing out of it. Set beside a number, it means the number is moving. It is the globe's tick-dot at 8px, on the same radius ramp and the same alpha falloff. The dot stays neutral by default and the ring carries the ramp, which is the meridian law and also what keeps it unambiguous under Solar, where every ramp stop is warm.
up/down/warn colour both; ramp fills the dot..pending's tick-dot, the same atom minus the ring.MOTION_KIT.rippleOn(node)The same ring, fired once, from the exact point you pressed. Not a material-design flood-fill: a single hairline ring that expands and is gone in 620ms. Press these.
Delight, not information. It confirms the tap landed where you thought it did, which is the one thing a touch target can't otherwise prove.
The globe's dot grid, flattened. A shimmer sweeping across a grey bar says a machine is scanning this. A field of dots where a few quietly breathe says this place is populated and the contents are on their way, which happens to be both true and already ours.
MOTION_KIT.field(host).resolve()The lattice stands in for content that hasn't arrived, in the shape it will arrive in. When it does, the dots clear row by row, 77ms apart (the mark's own per-stroke lag) and the real thing fades up underneath. No sheen sweeps across it at any point.
This is the proposed replacement for .kit-skel's shimmer bar.
Same job, our vocabulary, and it degrades to a plain still field rather than a stuck gradient.
mask() gives it the shape it stands in for.resolve() cuts instead of cascading.MOTION_KIT.field(host, {mode:'idle'})The same field with nothing to resolve into: an empty surface that is quietly inhabited instead of quietly broken. The dots walk the meridian by row, so it carries the brand without a single filled pixel of it.
MOTION_KIT.trail(host).progress(p)Lattice dots laid along a route instead of a grid, with a packet of three walking it and the leading dot carrying the ping. The path behind stays lit, so how far is legible without a percentage. Determinate when the wait has a known end, looping when it doesn't.
MOTION_KIT.tally(host, {total}).step()When the steps are discrete and few, show the steps. Dots you can actually count, walking the meridian left to right, each landing one lag after the last. The frontier dot wears the ping: the one thing currently happening is the one thing that beats.
Six dots is the ceiling. Past that you are drawing a bar, and a bar should say a number instead.
role="progressbar".The mark atom. When exotic is thinking, the thing that moves should be exotic rather than a borrowed rotating arc. And when something completes, a wave should reach shore and recede, because that is the shape the brand already makes.
MOTION_KIT.think(host, {size})The strongest single element here, and the least invented: this is
mountMark at 20px in its ordinary wave mode. The spinner is the logo, with
no new asset and no new vocabulary, and the six strokes travel exactly as they do at hero size.
16 / 24 / 40px. Below about 14px the strokes fuse and it reads as a smudge, so use a quiet Ping there instead.
EXOTIC.mountMark({motion:'wave'}), drawn at 6× and scaled down.still. The live region does the telling.MOTION_KIT.rule(host).crest()One element, two behaviours, because they are one function upstream: the mark's
ampAt returns a steady amplitude in wave mode and a rolling gaussian swell in crash
mode. So a divider can be quietly alive while its section loads, and then the swell crosses once,
the line settles flat, and that settling is the confirmation. Press crest.
ampAt from mountMark, both modes. Crash retimed 3600ms → 1400ms.crest() settles instantly.The one element on this page that could only exist in this product. exotic clears in a batch auction, so a trade waiting for confirmation is not an unknown wait. It is a known interval, and spinning an indeterminate arc at someone who could be told the number is a small lie.
MOTION_KIT.arc(host, {seconds, total})The lattice bent into a ring: one dot per beat, lighting in reading order along the meridian, with the seconds in the middle in tabular mono. The frontier dot pings. When the clock lands, the ring swells outward once (the crest, wrapped around a circle) and the face resolves.
Two pieces that inform nobody of anything. They exist because the mark's own code left the door open for them, and because software people love has details nobody reports.
MOTION_KIT.swim(host, {word})mountMark exposes getPhase(), and its own comment says why:
"lets a companion renderer swim in sync." Somebody built that hook and nothing has used it. So
the wordmark's underline is not a rule beside the mark, it is the mark's wave at the mark's phase.
Hover the mark; the mark speeds up, and so does the underline, because they are one wave.
Nobody will report this in a survey. It is the difference between a logo placed next to a line and a logo that owns the line.
getPhase().MOTION_KIT.wash(card).once(swap)The page sweep in js/wave-sweep.js washes the whole viewport between
pages. Bounded to one card, the same sheet is how a single value should refresh: the wave covers it,
the number changes underneath, the wave leaves. No spinner and no flash of empty, because the value
was never absent, it was behind the light.
Cover 420ms (--t-ovl), reveal 700ms (--t-story). The
exit is not faster here, because unlike a dismissal this one is showing you something.
Everything above is ambient or state-indicating. These four are driven by the reader: two by
scroll, two by the finger. The scroll pair are native CSS
(animation-timeline: view()) — no library, no scroll listener, no rAF, so the
compositor runs them and they stay smooth exactly when the main thread is busiest.
MOTION_LIVE.clock(host)Drag it sideways. One screen is 24 hours. The daylight band moves, the exchanges open and shut, and on release it springs home to the real hour — you can look at another time, but you can't leave it there. Notice the stretch where nothing is open.
MOTION_LIVE.scrub(host, {readout})The price draws itself as you scroll past, and the readout counts with it. The line
is a clip-path wipe on a view() timeline, so it rides the compositor; only the number
needs JS, because a number has to be computed. Scroll this one up and down.
EXOTIC_CHARTS.walk + a clip wipe, never a dash animation.MOTION_LIVE.kinetic(el)Every word takes its own slice of one shared view() timeline, so the
stagger is tied to scroll position and not to a clock. Scroll back up and the words
un-reveal in order — a time-based stagger can't do that at any price.
The sun is always up somewhere, so a market is always somewhere in its day. We built the venue that never has to close, and priced it against the exchanges that do.
view() timeline, each word's range offset by its index.MOTION_LIVE.odo(host).set(v)Every money app rolls the whole number. A real odometer doesn't — the ones column
spins constantly and the millions column sits still for years. So only the columns that actually
changed move, cascading from the right at one LAG each, the direction a carry travels.
The moved column keeps a hair of ramp for a beat. It's the only thing that tells you which digit changed.
MOTION_KIT.LAG.Buttons, charts, scroll rails, an answer arriving. Organised against the principles in devouringdetails, because they name the reasons better than "it looks nice" does. Two rules run through all six: slow where the user is deciding, fast where the system is responding, and a value that changes should move to its new value, never cut to it.
MOTION_PARTS.hold(btn, {onConfirm})Closing every position shouldn't be one click from a fat finger, and it shouldn't be a modal either — a modal moves the decision somewhere else. Holding is the confirmation. Press and hold. Let go early and it snaps back in 200ms.
1.6s linear in, 200ms ease-out back. The press is the user deciding; the release is the system responding.
MOTION_PARTS.morph(btn).run()Most products swap a button for a spinner and then for a toast: three objects, and you have to re-find each one. This is one surface that changes width and content in place. The label crossfades through a 2px blur, which is what stops the eye reading two overlapping words instead of one word changing.
The width is measured from the next label before it's shown, so the surface can travel while the words are still blurred out. The two never fight for the same frames.
MOTION_PARTS.minimap(host)A range picker throws you between presets. A minimap keeps the whole series on screen and lets you hold a window over it. Drag the window. The main chart rescales its y-axis to what's inside — without that you're cropping, not zooming, and everyone forgets it.
MOTION_PARTS.range(host)Every chart on the internet blanks and redraws when you change the range. That throws away the only thing you asked for, which is the relationship between the two views. Interpolating point by point costs nothing. Switch between them.
MOTION_PARTS.stream(host).play(text)Text appearing character by character is a typewriter, and a typewriter is a costume. Real generated text arrives in tokens, so it should land in word-sized pieces, each blurring up. The caret blinks only once the stream stops — during output, two things would be claiming to be the live edge.
MOTION_PARTS.strip(host, {items})A rail whose content is cut off at the edge is a bug that looks like a layout. The fade isn't decoration — it's the only thing telling you there's more, and it appears only on the side that has more. Scroll it sideways.
scroll() timeline on the rail, so the fade can't desync.The five pieces from the review pass, built from Rauno Freiberg’s reference components (Devouring Details, credited per piece). The through-line is his thesis: the interface should already know — from your clipboard, your selection, your history — and a wrong guess must cost one glance.
MOTION_SENSE.timeMachine(host)Drag anywhere on the panel. The value rolls through the odometer (only changed digits move), ledger rows arrive and leave as you cross their dates. Unlike the clock it stays where you leave it — history is a place you visit — so a chip carries you back.
MOTION_SENSE.dial(host)The Arc said the wait is known. This says so is the history: drag sideways and the ring rotates under your finger — whatever sits at the notch is the clearing you’re reading. Release and it springs home to live, overdamped, no oscillation.
Drag = dx × 0.5, 1:1 while down; release springs on ~150/42 — the reference’s values. Arrow keys step clearings; Esc returns to live.
MOTION_SENSE.deposit(host)Copy the bank balance, then focus the field. What you copied is offered as a chip — offered, never auto-filled, so a wrong guess costs one glance. Accepting it counts the value in over 240ms; pasted junk (“$2,500.00”) lands formatted.
MOTION_SENSE.kInfer(stage)Select a name below, then press ⌘K (or ctrl-K). The palette opens with your selection as the query, already filtered — and the text sits selected in the input, so one keystroke replaces a wrong guess.
Memory led the session: SK Hynix carried the basket while Samsung faded, Tokyo Electron held its gap, and TSMC printed a quiet new high. The Robotics thesis absorbed all of it without a rebalance.
Not a component — a number, and where it came from.
MOTION_KIT.LAG · MOTION_KIT.cascade(nodes)The mark's strokes ride one sine with a per-line lag of 0.10 rad at a speed of 0.0013 rad/ms. That is 77ms. The load storyboard's rail-card stagger, chosen separately and by eye, is 70ms. They are the same figure. The interface has been arriving on the mark's own wave the whole time, and nobody wrote it down.
lineLag 0.10 ÷ speed 0.0013 = 77ms, from mountMark's frame loop.--mo-d.Nothing on this page has been wired into a product surface yet — this is the proposal. Ranked roughly by how much each one buys.
| element | surface | replaces |
|---|---|---|
| Arc | Trade ticket, between submit and clear; next-auction countdown in the top bar | The .pending tick-dot doing duty for a wait we can actually measure |
| Field | Lobby holdings · Asset metrics strip · Explore tiles · Scout panel | .kit-skel's shimmer bar |
| Think | Scout fetching · ⌘K searching · Asset news & earnings tabs | Nothing — these surfaces currently show a blank while they load |
| Ping | Live price on Asset and Home · market-open state · a thesis mid-rebalance | Nothing — "is this live?" is currently unanswerable |
| Rule · crest | Order filled · deposit landed · thesis copied | Toast alone, which scrolls past unnoticed |
| Trail | Deposit in flight · withdrawal settling (Activity + top bar) | A static "pending" chip |
| Tally | Deposit queue position · multi-step withdrawal | "Position 3 in queue" as bare text |
| Wash | Colorway change · range re-quote · portfolio refresh | A hard value swap |
| Time machine | Activity header · Story.html | A static date filter |
| Auction dial | Trade ticket · settling rows | Nothing — past clearings are currently invisible |
| Clipboard deposit | Deposit / withdraw sheets | Typing an amount you just copied |
| ⌘K inference | ⌘K everywhere | Retyping a name you can see |
| Clock hint | Wherever the world clock lands | An undiscovered drag |
| Ripple | Primary CTAs and the trade side buttons | Nothing — additive |
| Swim | Sidebar wordmark · boot splash · Save.html | Nothing — additive |
| Cascade | Everywhere the storyboard already staggers | 70ms → 77ms, and the number gets a reason |
DESIGN.md → Round-2 audit laws