Diamond Stone: motion, evaluated and completed
Scope (11 November 2026): the staff app. The public website and client portal move
differently, and are specified in docs/SITE-MOTION.md.
Date: 2026-11-11. Revision 1. Drawn in docs/component-map.html (sections 6 to 9), with
the earlier live curves in docs/ui-build.html sections 4 to 10.
Motion was planned in three places: docs/DESIGN.md section 11, docs/FRONTEND.md
section 3 and docs/ui-build.html. This document evaluates all three together, lists
what is wrong, unclear or missing, and gives the completed specification. Where this
document and the earlier ones differ, this one is the plan. The feel does not change:
quick and quiet, nothing bounces, one small overshoot on a completed step.
Nothing here has been run on a phone. Every number marked "start" is a starting value to be tuned on the S25 and A55 in Stage 0. Browser facts are from memory of the platforms as of mid-2026 and are re-checked on the phones.
1. Verdict
Sound and kept: a closed vocabulary of tokens; only position, scale and opacity animate; springs after a finger lets go; figures cross-fade and never roll; one moving thing at a time; a real less-motion mode; the reasons given in terms of the person.
Not good enough: the plan describes how things should feel but leaves most of the decisions a developer must make unstated. Twenty-six findings follow. The seven that matter most:
- The springs are written in units no tool uses directly. They cannot be coded as written.
- Half the components have no motion defined. Developers would invent it, which is how interfaces drift.
- Drags have a feel but no numbers: no thresholds, no resistance at the ends, no rule against the page scrolling at the same time.
- Nothing says what happens when a movement is interrupted, and on a phone they are interrupted constantly.
- Screen changes can animate twice on a swipe back, once by the phone and once by the app.
- Glass has a quality ladder and motion does not. A slow phone lowers its glass but keeps every movement.
- There is no way to test motion. The checks measure frames, not whether the right thing moved.
2. Findings
Severity: A must be settled before any component is built; B before screens; C during building.
Wrong or inconsistent
| # | Sev | Finding | Fix |
|---|---|---|---|
| 1 | A | Springs are given as "stiffness 1400, fully damped". CSS has no springs; Svelte's own Spring uses a different 0-to-1 scale; neither takes these numbers |
One spring definition and one small solver of our own (section 3). CSS curves are generated from it at build time |
| 2 | A | sheet is "280 in, 200 out" in one place while leave is a separate 200 ms token in another. settle is "about 480 ms" in one and a spring in the other |
One table (section 3). sheet is 280; all exits use leave; settle is the spring, and 480 ms is only how long it takes to come to rest |
| 3 | A | Durations have curves described as "ease out" with no values | Four named curves with values (section 3) |
| 4 | B | "Halo card to record: the card closes, then the record opens, in sequence under 450 ms" makes the person wait for an exit before anything arrives | Exits and entrances overlap. The record starts arriving at once; the card leaves beneath it. Nothing ever waits for an exit |
| 5 | B | "Grey placeholders after 300 ms" with a moving shimmer would break the rule of no looping at rest, on every loading screen at once | Placeholders are still. One slow pulse of opacity on the whole group, and only after one second (section 8) |
| 6 | B | "Every animation holds 60 frames a second on the A55 or it is simplified" does not say how | The motion ladder (section 6) |
| 7 | C | "Timing is the same on 60 and 120 Hz" is true for CSS but not for a spring stepped once per frame | The solver works from elapsed time, not frames, and limits any single step to 32 ms so a paused tab does not jump |
Unspecified
| # | Sev | Finding | Fix |
|---|---|---|---|
| 8 | A | No tokens for distance, scale, opacity, delay or stagger. "Shrinks slightly", "slides", "forms from smaller" are not values | Section 3 |
| 9 | A | Interruption | Section 4 |
| 10 | A | Drag thresholds, resistance, direction lock, snap choice, conflict with scrolling and the phone's own edge gestures | Section 5 |
| 11 | B | Order when several things move (stagger, how many at once) | Section 4 |
| 12 | B | What plays on first load, on going back, and on restoring a screen | Section 4: nothing plays for content that was already there |
| 13 | B | How long before each kind of waiting signal | Section 8 |
| 14 | B | An action shown as done before the server answers: what does failure look like? | Section 8 |
| 15 | B | How attention is drawn without looping | Section 8 |
| 16 | B | Duration does not scale with distance or size: a chip and a full-height sheet both "move" | Three sizes of travel, each with its token (section 3) |
| 17 | C | Haptics: three patterns named, not mapped to events | Section 9 |
| 18 | C | Sound | None, anywhere. Stated as a rule |
Missing
| # | Sev | Finding | Fix |
|---|---|---|---|
| 19 | A | Motion defined for about 12 elements; the component plan has 96 | Every component has a motion row in docs/COMPONENTS.md; the summary by family is in section 7 |
| 20 | A | Swipe back on iPhones, and the Back gesture on Android, are animated by the phone. A view transition on top plays a second animation | When the browser reports it already animated the navigation, the app's transition is skipped. Checked on both phones |
| 21 | A | No motion ladder | Section 6 |
| 22 | A | No way to test motion | Section 10 |
| 23 | B | A less-motion switch exists only in the phone's settings, where few people find it | A switch under Me → Appearance as well: Full, Simple, Minimal. The phone's setting sets the starting value |
| 24 | B | Things that move by themselves: the truck on the map, a reply timer, a live count | Section 8 |
| 25 | B | Keyboard, scroll position on Back, pull to refresh, and images arriving | Sections 5 and 8 |
| 26 | C | Layer discipline: promoting many elements to their own layer exhausts memory on a mid-range phone | An element is promoted only while it moves. A budget of layers per screen, checked on the A55 |
3. The vocabulary, completed
Replaces the tables in DESIGN section 11 and FRONTEND section 3. Nothing outside it may be used; a build check enforces it.
Time
| Token | Value | For |
|---|---|---|
tap |
90 ms | Press feedback |
quick |
150 ms | Colour, opacity, small parts (a tick, a chip) |
move |
220 ms | A part of a screen arriving or changing shape |
sheet |
280 ms | A sheet, drawer or whole screen arriving |
leave |
150 ms small, 200 ms large | Anything going away |
fade.reduced |
120 ms | Every spatial movement when less motion is asked for |
Curve
| Token | Value | For |
|---|---|---|
curve.enter |
cubic-bezier(0.05, 0.7, 0.1, 1) |
Arriving: fast start, soft landing |
curve.exit |
cubic-bezier(0.3, 0, 0.8, 0.15) |
Leaving: slow start, fast finish |
curve.standard |
cubic-bezier(0.2, 0, 0, 1) |
Moving from one place to another on screen |
curve.linear |
linear | Opacity alone, and progress that reports real work |
These are the calm ("emphasized") curves Material publishes, as starting values.
Spring
One definition: stiffness, damping ratio, mass 1. A small solver of our own (about
forty lines, exact for any elapsed time) does two jobs: it drives anything released by
a finger, starting from the finger's speed; and at build time it writes each spring out
as a CSS linear() curve with its time to rest, for movements that do not start from a
finger. Svelte's Spring is not used for these, because its numbers are on another
scale; it remains available for simple cases.
| Token | Stiffness | Damping ratio | Comes to rest in | For |
|---|---|---|---|---|
spring.snappy |
1400 | 1.0 | about 150 ms | Toggle, chip, press release |
spring.standard |
700 | 1.0 | about 220 ms | Halo band, sheet, drawer after release |
spring.gentle |
300 | 0.9 | about 350 ms | A full-height surface settling |
settle |
300 | 0.75 | about 480 ms, 3 % past the end once | A completed step, and nothing else |
The rest times follow from the numbers and happen to match quick, move and sheet,
so a released drag and a tapped button arrive in the same time.
Distance, scale, opacity
| Token | Value | For |
|---|---|---|
shift.s |
8 px | A part appearing in place: it rises this far while fading in |
shift.m |
24 px | A card or row entering a list |
shift.full |
The surface's own size | Sheets, drawers, screens |
press.scale |
0.97; 0.99 for anything wider than half the screen | Pressed |
appear.scale |
from 0.94 | A ring, badge or tick forming |
dim |
0.4 black | Behind a sheet or dialog |
disabled |
0.38 | Disabled content |
Order
| Token | Value | For |
|---|---|---|
stagger |
24 ms between items, first 6 only, never more than 120 ms in total | A list that is new to the person |
overlap |
An entrance starts at once; the exit beneath it runs at the same time | Every replacement |
delay.hint |
300 ms | Before any waiting signal |
4. Rules for what moves, and when
Interruption.
- Anything that follows state (open or closed, on or off) is coded as a transition from
its current position, so reversing mid-way turns round smoothly. Fixed animations
that restart from the beginning are not used for these.
- A drag can be caught again at any moment; the spring stops and the finger takes over.
- A screen change cannot be interrupted, so it is short, and a second navigation during
it ends the first at once.
- Input is never blocked by a movement. settle plays while the next action is already possible.
Several things at once. - One focal thing moves. Others may fade. - At most three shared elements in a screen change (the photo, the title, the amount). - A list staggers only when it is new to the person; never on Back, never on refresh, never when it came from the saved copy.
What never plays. - Nothing animates in on first paint, on Back, on a restored screen, or for content that was already there. Movement is for change the person caused or needs to notice. - Text size never animates. Layout never shifts under a finger. - Nothing loops while the screen is at rest. No sound. No parallax. No zoom larger than the press scale except a photo the person opens.
Direction. - Deeper is toward the end of the reading line; Back is the reverse. Sheets rise from the bottom; the Halo falls from the top. An area change (Place) is a cross-fade, since areas have no position relative to each other. - All sideways movement is written with start and end, not left and right, so Arabic mirrors without a second specification.
5. Drags
Starting values; each is tuned on the phones.
| Matter | Rule |
|---|---|
| When a drag begins | After 8 px of travel. Until then it may still be a tap |
| Direction lock | The first 8 px decide: within 30° of the surface's axis it is a drag of that surface; otherwise the page scrolls and the surface ignores it |
| Following | The surface is placed at the finger each frame. No smoothing, no delay |
| Past the end | It still follows, at about a third of the finger's travel, to at most 48 px |
| Choosing where to rest | Where it would stop if thrown: position plus speed × 150 ms. The nearest rest point to that is chosen |
| A flick | Faster than 0.5 px per ms goes to the next rest point in that direction, however short the drag |
| Release | spring.standard, starting at the finger's speed |
| Cancel | The phone taking the touch away (a call, a system gesture) returns it to where it started |
| Scrolling inside | A sheet with a list drags only from its handle and top area, or when its list is already at the top |
| The page behind | Does not scroll while a surface is dragged; the browser's own pull-to-refresh is switched off inside the app |
| Screen edges | No drag starts within 24 px of the left or right edge: that belongs to the phone's Back gesture |
| The Halo at the top | Pulls only from the band itself, which sits below the status bar, so it does not compete with the notification shade. To be confirmed on both phones |
| Swipe on a row | Reveals at most two actions; a full swipe never performs a destructive one |
| Every drag has a button | Required by the accessibility standard, and by a screen reader, which takes swipes for itself |
Surfaces that drag: the Halo band (three rest points), sheets (half, full, closed), a row (actions), a photo (zoom and pan), the photo set (sideways), a map. Nothing else.
Keyboard. When it opens, the Omnibar leaves with leave and the form's own action
sits above the keys. The screen is not animated to follow the keyboard; the field is
simply kept in view.
Scroll. Back returns to the same scroll position, with no movement. The Halo band shrinks with scrolling by the browser's scroll-linked animation, and where that is missing it simply switches at a threshold.
6. The motion ladder
Glass has levels chosen per phone; motion gets the same, chosen by the same monitor but independently, so a phone may have plain surfaces and full motion or the reverse.
| Level | What plays | When |
|---|---|---|
| Full | Everything here: shared elements between screens, springs, stagger, settle |
The phone holds its frame rate |
| Simple | Slides and fades only. No shared elements, no stagger. Springs after a drag stay | Frames drop during screen changes; power saving is on |
| Minimal | 120 ms fades. Drags still follow the finger and then fade to rest. settle becomes a tick with no travel |
The person asked for less motion, in the phone or in the app |
The monitor counts long frames per kind of movement, so a phone that struggles only with shared elements loses only those.
7. Motion by component family
The row for each component is in docs/COMPONENTS.md. The pattern by family:
| Family | Enters | Leaves | Changes | Pressed |
|---|---|---|---|---|
| Actions (button, chip, toggle, segment) | Never animates in | Never | Colour with quick; a thumb or selection slides with spring.snappy |
Scale, same frame |
| Inputs | Never | Never | Focus ring and label with quick; an error message rises shift.s |
None; the caret is the feedback |
| Display (row, card, figure, chip) | shift.m and fade with move, only if new |
Height closes with leave; neighbours slide |
Values cross-fade with quick |
Scale if it opens something |
| Progress (stage tracker, bar, ring) | Never | Never | The new segment fills once with move; on completion settle |
None |
| Feedback (toast, banner, receipt strip) | From the edge it belongs to, move |
leave, by itself after 5 s, pausing while touched |
Text cross-fades | Scale on its action |
| Overlays (sheet, drawer, dialog, menu) | From their source: sheet for large, move for a menu; the dim fades with quick |
leave; faster than arriving |
Sheet between half and full follows the finger | n/a |
| Shell (Halo, Omnibar) | Present from the first paint | Omnibar leaves only for the keyboard | Capsules exchange with a cross-fade and width change, move; the accent button does not move |
Scale |
| Media (photo, map, chart) | Photo fades over its placeholder colour, quick; a chart draws once, move, only when new |
Never | The truck moves smoothly between known positions; a chart's values cross-fade | A photo opens by growing from its thumbnail |
| Screens | Slide from the reading-line end with the shared elements, sheet |
Reverse, leave |
n/a | n/a |
8. Waiting, failing, and things that move by themselves
Waiting.
| Time waited | What shows |
|---|---|
| Under 100 ms | Nothing. The press state was the answer |
| 100 to 300 ms | The control stays pressed or shows "busy" in place. No placeholder |
| 300 ms to 1 s | Still placeholders in the shape of the content. No movement |
| 1 to 10 s | One slow pulse of the whole placeholder group; for an action, a progress line and a word for what is happening |
| Over 10 s | The person may leave. The work continues, and the Halo tells them when it is done |
A saved copy is shown at once with its age instead of any of this, wherever one exists. A bar that fills is used only for real, measured progress (an upload). Otherwise the line is indeterminate.
Done before the server answers. Approve, send, mark as received and similar actions
show as done at once and go to the outbox. If the server refuses: the item returns to
where it was with move, its status turns to the warning tone, and a banner says why
in words. Nothing shakes. Money actions are the exception: they wait for the server.
Drawing attention. A new Halo ring forms once (appear.scale, move). A count
cross-fades. A row that changed while in view is tinted for two seconds and fades. No
pulsing, no badges that wobble, and never more than one attention movement in any
three seconds; later ones wait and merge.
By themselves. - The truck on a map glides between known positions over the gap between reports, in a straight line, and stops when reports stop. It never guesses ahead. - Timers show minutes, not seconds, until the last minute. A figure changing every second is movement at rest. - Live counts change at most once every few seconds, by cross-fade.
Images. A photo's space is reserved before it loads, filled with the stone's own average colour, and the photo fades in over it. Nothing jumps.
9. Haptics
Android only; never the only signal; a switch under Me → Appearance.
| Pattern | When |
|---|---|
| Light tick | A drag reaches a rest point; a toggle changes; a code is read by the scanner |
| Confirm (one firmer tick) | A completed step, with settle |
| Double tick | An action was refused |
| None | Typing, scrolling, navigation, anything arriving by itself |
10. Testing motion
| Check | How |
|---|---|
| The right thing moves, in the right order | Each movement is recorded at its start, middle and end with the clock frozen, and compared with approved pictures. The same for Simple and Minimal |
| Tokens only | A build check on every duration, curve and spring |
| Frames | A trace on the S25 and A55 for every movement in the component workshop; long frames fail it |
| Touch to first change | Measured on the phones, under 100 ms |
| Interruption | A scripted test reverses every open-and-close mid-way and catches every drag mid-spring |
| Nothing moves at rest | A test watches each screen for five idle seconds |
| No double animation on Back | Tried by hand with the swipe gesture on both phones and an iPhone |
| Less motion | The full screenshot run with the setting on |
| Feel | A slow-motion switch (5 times slower) in test builds; the owner tries the four shell movements on both phones before any screen is built |
11. Decisions
- Our own spring solver rather than Svelte's
Spring, so the tokens mean one thing everywhere. Proposed: yes. - A motion switch in the app (Full, Simple, Minimal) besides the phone's own setting. Proposed: yes.
- Whether lists stagger at all. Proposed: yes, first six items, only when new. It is the cheapest thing to drop.
- Whether actions show as done before the server answers. Proposed: yes, except money.
- Haptics on by default on Android. Proposed: yes.
12. What stays unknown until the phones
Whether shared elements between screens hold the frame rate on the A55; whether the phone's Back gesture can be told apart from a tap on Back in an installed web app on Android; whether the Halo pull is comfortable below the status bar; whether the starting values in section 5 feel right. Each is on the Stage 0 list.
Reconciled values and three states of "done" (12 November 2026)
docs/CRITIQUE.md section 4 found this document, DESIGN.md, FRONTEND.md and the
mockup giving different values under the same names. The table in CRITIQUE.md
section 4 ("One set of values") is now the source. In short: settle is a spring of
about 480 ms with about 3 % overshoot (the mockup's 400 ms curve is corrected);
leave is 200 ms, 150 for small things; a spring rests within 0.5 px and 0.05 px per
ms.
Done has three states. Sent: a quick tick, no settle, no vibration. Confirmed
by the server: settle and the vibration, and only on the last step of a record.
Refused: a banner, and the item returns. This replaces "shown as done at once" in
section 8, and agrees with DESIGN.md section 7.
Also settled there: one meaning for press-and-hold per part; the Halo pulls only from its handle; our own pull-to-refresh; the Omnibar hides for the keyboard; one place for Undo; one manager for overlays and Back; no movement that implies something is live when it is not; fewer movements at a desk.