Motion

Every specimen on this page has to be operated. A screenshot shows a frame and never a movement, so this is the one foundation the kit cannot print. Movement here does exactly three jobs, and a moment that cannot be given one of the three does not get a moment. The product had five durations before this stage and not one of them was a literal, so the work was never tidying loose numbers. It was finding one job wearing four of them.

2 durations, 2 curves, 1 declared linear 0 motion in a screen file, of 106 34 moments, 29 answered, 4 left status job: 482 marks, was 0 0 above 1ms of 163, net off 20 roles, 20 single numbers

Three jobs, and there is no fourth

The job is named before the animation is chosen. A moment for which none of the three can be named does not enter the inventory, and so it never gets movement. "It livens up the interface" is not a job. This is the same rule that throws an orphan feature out of a To-Be map and an orphan block out of the IA bank.

Connection: where did that come from

The surface arrives from the edge it will leave by, so the direction is the answer to the question. This is sheet-rise at stand scale, and in the product it carries 337 dialogs on 105 screens.

the page the sheet is over

A sheet, arriving from the bottom edge

Response: I was heard

A real control, taking its transition from the system rather than from this page. Hover it and hold it down. 18 of the product's 20 response declarations cross-fade a colour, a border or a ground, and two move something. The second specimen wore btn-quiet until 2026-08-18 and no such face exists, so it took the browser's own default button, grey 239 in Chromium and 192 in WebKit, and answered nothing: rest, hover and press read identical on every channel. The transition was there the whole time, five properties at .16s off .btn, which is why no reading of movement could see it. It is btn-secondary now, the face the product stands beside a primary 196 times, 530 counting the provider buttons that add to it.

Status: it is still working

It was not in the product until 2026-08-15, and this section said so for one step. The skeleton stood 482 times on 19 screens and every one was a flat box, so the only job about waiting was performed nowhere while nineteen screens were about waiting. It is sk-pulse now, an opacity cycle in skeleton.css, and the specimen below is the product's own rule rather than the stand's. A pulse is also the one thing a shorter duration makes worse: 1ms per period at infinite repeats is a flicker, so less motion REPLACES the cycle with a still tone instead of speeding it up. That is the first case the token override cannot reach, and it was found by writing the rule rather than by auditing for it.

Two durations, and the count is the finding

The stage asks for three, named by job. The inventory found two jobs with movement in them. The middle one, a change inside a component already on screen, has exactly one member: a chevron turning over when its market opens, which is feedback for a click and therefore a response. A third rung would have sat 20ms from the one above it. It arrives the day a row asks for it, said out loud, and not before.

Run them together. Separately they read the same; the difference only exists in the comparison, which is why they are on one button.

--dur-fast.16s, response
--dur-slow.25s, arrival

.16s is 10ms over the guideline and it was taken knowingly. A response slower than roughly 150ms stops reading as an answer and starts reading as lag, but that ceiling is about a control that MOVES under a finger, and 18 of the 20 here are a colour fade. The other direction was one line in tokens.css and 9,528 slots following it, which is the whole argument for the number living in one file.

What the product rendered before this stage

.16s9,528 slotsthe response, and 71 per cent of everything
.12s2,346 slotsa second response duration, 3 readers
.18s743 slotsa third, mostly hovers
.3s636 slotsa card hover and a bet sheet, one number for two jobs
.25s63 slotsthe dialog sheet

One role wore four of them. A hover is 160ms on a button, 180 on a photo tile, 250 on a trust plate and 300 on a card. Nobody decided that, and reading the files one at a time never shows it: the readings have to be grouped by job before the drift has a shape.

Two curves, and the third is refused with its reason

Same comparison, one duration, three shapes. The third lane is linear and it is here as the control: if the two above it looked alike you would have no way to know whether the page was working.

And linear is the one bare keyword the system keeps, in one place, with its reason beside it. The scroll edge fade binds a mask to distance rather than to time, so the identity is the only correct function there and anything else runs the fade ahead of or behind the thumb. A --ease-linear read once, whose value can never be anything else, is a name for a constant. The stage published "0 bare keywords" and the true number is 1: its own check read transition: and animation: and never looked at the longhand. The second instrument is what found it.

--ease-standardease, 12,821 slots
--ease-entersharp start, long settle
linearthe control, not a token

95.6 per cent of the product's easing read no token at all. 12,821 of 13,406 rendered slots are the bare keyword ease, typed out 54 times in the source and defaulted the rest. That is where this stage found its drift, and not in the durations. --ease-standard keeps the same value on purpose: renaming a curve is free, redrawing one changes how every hover in the product feels, and no row of the inventory asked for that.

There is no --ease-exit. Nothing in this product animates a departure: a dialog closes at once, the condensed category band collapses through the rule that opened it, and no state anywhere in the painted tree has a leaving face. A token with no reader fails the idle control as loudly as an undeclared case. It arrives with its first exit.

No springs and no overshoot, and that is a default rather than a taste: they read as "something went wrong" in precisely the states where a person least wants to be asked a question. One arrives with a row of the inventory that asks for it.

Distance, and why it is not a token here

The stage asks for --move-sm and --move-md. This system answered that question on 2026-08-13 with a census and a different shape, and adding the pair now would be a fourth answer to a settled question.

Of the system's 20 transform declarations, only 5 are movement. Nine are resting geometry, a chevron drawn by rotating a square and a knob centred on itself; two are a state that carries meaning rather than motion; the rest are the harness drawer and two keyframes. So *:hover{transform:none} under reduced motion would have flattened .market-chevron for exactly as long as a pointer rested on it, and an open market would have pointed the wrong way.

When a family cannot be selected, put the switch in the value the declaration already reads. The five distances are 3px on a card, 2 on a badge and 1 on a provider button, which are three decisions and not a ladder, and each stays in the file that makes it. Every one multiplies itself by --motion, which is 1 normally and 0 under reduced motion, so a distance that no selector could reach is turned off by a number.

Screen states, patterns, and the crossing between documents

A state is a PAGE here, so state transitions do not exist. Both trees hold one document per state, 105 each, by a convention five stages older than this one. Loading, empty, error and success are not conditions a screen moves into, so there is no before and after inside one document to move between. The one state family that does live inside a document is the dialog, which makes the tone check a check of five surfaces rather than of twenty.

The tone check: four quantities against the line the screen says

The tone is set by voice.md and this stage does not get to choose it. Duration, curve, amplitude, direction, each named separately and put beside the microcopy of the same state.

loading1.4s period, standard curve, .55 opacity, no travelthe rule is stay silent or name the thing, never hype. A shallow even breath with no movement is the visual form of staying silent
the win.25s, enter curve, one axis, up from the bottom edgeprinciple 4 is "mark the win without lighting a fuse" and the anti-example is confetti. The win arrives on the same rule as every other sheet
the lossidentical to the win, and that IS the verdicttwo different arrivals would be the product having an opinion about your result before you had finished reading it
errorno movement at allthe rule asks for short, no spring, small amplitude. Every error surface is a page, so the amplitude is zero, and nothing is calmer than nothing
emptyno movement at allthe case against a shift from below is that it implies something is loading, and a page that simply IS cannot imply it

0 disagreements, and the microcopy was not edited. The row that could have gone wrong is the win, and it did not, because the rule that would have broken it was written five stages ago and the outcome dialog never had a face of its own.

Patterns: 0 of 6, and the zero is what the rung IS

Read from the comment-stripped source of all six files: not one declares a selector that toggles at run time. Five have no state selector at all, and the sixth has a structural presence check rather than a state that changes while a person looks at it. The composition never recomposes, so an arrangement that never changes has nothing to move between. This is the invariant that keeps colour out of the rung, seen from the other side.

The crossing between documents: A, and it was measured before it was chosen

Every screen is a separate document, so the move from a feed card to its detail is a navigation and an ordinary transition cannot see it. Two test documents carrying @view-transition { navigation: auto }, navigated for real, asking whether pagereveal arrived with a transition attached:

Chromiumfires over http, fires from disk
WebKitfires over http, does not fire from diskthe same origin rule that has already cost this repository a sprite, a mask and two fonts

The answer is A, do not animate the crossing. Four reasons, in order. A cross-fade points at no source, and the connection job is defined as showing what came from what, so it would be movement with no inventory row behind it, which is the one thing this stage cuts. Doing it properly is a markup change in two trees: a shared name on the card and on the detail head, unique per document, on a feed that ships up to nine cards, so the name has to be written at click time. It would add an obligation the tokens cannot reach, since a view transition reads no duration token. And on the path this repository actually reads from, one engine of two would not show it, so half the reviews would be of a feature the reviewer cannot see. The line is one line, the measurement is banked, and the decision does not have to start from zero if the named pair is ever built.

Two more rulings, both no

A rung does not animate its own rebuild. Two surfaces qualify for the exception, the bet panel arriving at DETAIL and the sub-category rail at RAIL, and both are refused on the same ground: they arrive only while somebody drags a window edge. A person on a desk never sees the arrival and a person on a phone never has it. An entrance nobody can be present for is not a connection, it is a frame that happens to differ.

The scroll edge fade stays, and stays out of the register. It binds a mask to the element's own scroll rather than to time, reads no duration token, and is driven by the reader's own hand: it changes only while they scroll and by exactly as much. Measured at three scroll positions in both engines and both settings, the left edge closes from 0 to 32px and the right opens to 100 per cent, including under reduced motion where the blanket net forces the duration to 1e-05s, because a scroll timeline takes its progress from distance and not from that duration. Movement a person makes themselves is not movement done to them, so it needs no reduced-motion treatment and gets none.

And the first reading of it was wrong in this stage's signature way. The probe set scrollLeft and read the mask in the same task, and reported it identical at every position in both engines, which reads exactly like a feature that does not work. Two frames later the same probe reports it moving in all four combinations. A scroll is not a paint, any more than a resize is a layout.

The cost of a frame

transform and opacity are the two properties a browser animates without laying the page out again. Everything else makes it do the work on every frame, and the device that can least afford it is the one most people are holding. transition: all is 0 and will-change is 0, and both were 0 before this stage as well. Eight expensive properties were found: two converted, one refused with its reason, five kept with a count.

convertedthe toggle knob travelled by leftnow translateX(16px). 22 minus 6 is 16, and the geometry is identical to the pixel: the knob's left edge reads 6 off and 22 on, in both engines
convertedthe how-it-works dot grew by widthnow scaleX(1.75), because 28 over 16 is exactly 1.75, so two dots of every three are drawn undistorted. The cost is named: the selected dot's end caps go from a 2px radius to 3.5 on a bar 4px tall
refusedthe condensed band collapses by max-heightthere is no transform that removes a box from the flow, and this band's whole job is to stop taking room. The cost is one element, once: one band per document, relaid for 250ms
kept x5box-shadow on button, card, hero, icon button, inputit loads painting rather than layout, and the case against it is twenty cards painting at once. All five sit on the element's own hover or focus, so at most ONE element in the document animates a shadow at a time. A pointer is in one place

Both conversions were misread once before they were believed. The toggle read translateX(0) in both faces, then 0.56px in one engine and 3.82 in the other, because the probe was reading a transition in flight; finished first, both read exactly 16. The dot read transform: none on all nine of its placements on the how-it-works screen, which looks exactly like a rule that failed to apply, and those nine render at no width at all: they live inside the shut dialog. Where six of them do render, the selected one reads matrix(1.75, 0, 0, 1, 0, 0). A computed value on a box that was never laid out is not a reading of the rule.

Less motion is an obligation, and it has one mechanism

Not a product setting and not a preference: the operating system already carries the answer, and for some people this is nausea and migraine rather than taste. The mechanism is one block that redeclares the tokens. Every component reading a var() obeys without knowing the block exists, and so does the component nobody has written yet.

Reducing motion never cancels a state. What appears still appears; the movement goes and the arrival stays. An element that stops appearing at all under the setting is a worse defect than any amount of unnecessary animation.

less motion is on: the sheet still arrives, the pulse is a still tone

the sheet arrives either way

A sheet, arriving from the bottom edge

This switch is a demonstration and not the check. It redeclares the two tokens on a scope instead of on the root, so what you see is the mechanism working. It cannot tell you about a rule that reads no token, and a rule that reads no token is the entire thing the real reading is looking for. That reading is taken with the browser emulating the system setting, on every element of every document.

1ms and not 0s, because a duration of zero removes the transition rather than shortening it, so transitionend never fires and a script waiting on it stalls. 1ms keeps the event and removes the movement, and it is the value the check looks for: anything above 1ms is a rule that is not reading these tokens.

@media works here and could not at Responsive, and that is not a contradiction. A width query cannot READ a custom property, which is why the rung ladder is kept as a registry. This query does not read one, it redeclares one inside itself. Asking a media query for a variable's value and redefining a variable within a media query are opposite operations, and confusing them is expensive.

The blanket net on * was hiding this check, and it is gone. base.css carried one from before this stage. Measured with it in place over 163 documents in two engines: 6,555 moving elements in the normal pass and 115,028 under the setting, the difference being every element in the document taking a duration from that one rule, with 0 rows above 1ms of 230,056. Under !important on *, a component that reads no token is indistinguishable from one that reads every token. A sweep that cannot fail is not a sweep.

Taken off and re-measured, control first. An injected element carrying transition: opacity 999ms linear, which reads no token of ours, read back 999ms in both engines, so the probe can see a rule that does not obey. Then the same 163 documents with the setting emulated: 1,392 elements above 1ms, and 0 of them in the system. Every one was the STAND, a .15s literal in _page.css reaching 690 elements per engine over ten kit pages, plus the demonstration pulse on this page. Both were repaired at source rather than covered, and the second reading is 0. Every rule in the system obeyed on its own, which is the fact the net had been standing on top of for as long as it existed. It is not put back: its only remaining argument was code that does not exist yet.

And the state survives, measured. Three sheets opened at 390px, settled, both engines, both settings: in the normal pass they are climbing at 21 and 25 per cent 60ms in; under the setting they read translate: 0 0%, arrived, opacity 1. 0 states lost. The skeleton is the same answer from the other side: the marks stop moving and stay fully opaque rather than disappearing.

What the tokens cannot reach is three things and they are written down. A cycle, which needs REPLACING rather than shortening because 1ms per period is a flicker. The grey tree, which links no stylesheet at all, so no token declared anywhere reaches it: measured with the setting on, 105 elements above 1ms over 105 of 105 documents, and the honest answer there is a declared boundary difference rather than 105 copies of a media query. And a cross-document view transition, which is one of the four reasons there is not one.

The inventory of moments

Corpus: all 105 grey screens, the flows, and every state selector in the system. Each moment carries exactly one job, and a second rule applies on top of the first: the state has to already exist. Movement is laid on the states the Tokens and Components stage built; it does not invent them. A moment whose state is missing is an order on that stage, not a class drawn here that the product will never wear.

response23 moments, 23 answered6 were given movement on 2026-08-15: navitem 1,068 placements, position 996, state-block 152, footer, cookie-consent, toc. The seventh is the focus ring and it stays instant on purpose
connection9 moments, 5 done, 4 left349 details elements still expand with no transition; the toast and Load more have no state to move between and are an order on another stage
status1 moment, 1 donesk-pulse, 482 marks on 19 loading screens, and it was 0 until 2026-08-15
held apartthe scroll edge fade, ruled 2026-08-15a mask bound to scroll position rather than to time, driven by the reader's own hand: none of the three jobs describes it, so it stays and stays out of the register
total34 moments, 29 answered, 4 leftthe first report said 35 and 21, and 35 counted the edge fade in two rows at once. The 4 are the expansion of 349 details elements, the contents rail, and the two that need a state before they can have a movement

Two rows say the state is missing, the toast arriving and Load more appending, and both are orders on another stage rather than work for this one. 35 moments over 47 components is under one each, so nothing here is decoration wearing a job's name.

The full record, both halves of the transcript and the reasons behind every number, is in docs/motion.md. Width has its own foundation next door: responsive, and the ruling about whether a rung animates its own rebuild is taken there and recorded here.

What this stage found, in order of what it cost

  1. 1The check for less motion was already blind. The blanket net was in place before the stage opened, which is the one order the pack forbids, and it makes the reading green on empty ground.
  2. 2The status job was performed nowhere. Nineteen loading screens and 482 skeleton placements, all of them flat, and a plate that does not move is not distinguishable from an empty block. Closed 2026-08-15, and its own reduced-motion rule is the first thing the token mechanism could not reach.
  3. 3The drift was in the curve, not the duration. Five durations and every one a token; 95.6 per cent of easing slots on a bare keyword.
  4. 4Two animations returned zero and the zero was the probe. No screen ships an open dialog, so a static sweep never sees the two sheet keyframes. Opened with a script they read .25s and .3s: one job, two numbers, two mechanisms and two spellings of the same movement.
  5. 5The most expensive trap was already shut and nothing said so. 0 of 106 painted documents carry a transition of their own. The rule that protects that is written now, and what it protects is a tree that already stands.
  6. 6The instrument was wrong four times before its numbers were believed, and every one is the same shape. 1,750 of 2,263 matches for "transform" in the grey tree are text-transform, and 14 matches for "animation" there are 0 declarations. The scroll fade read identical at every position until it was given two frames. The toggle knob read 0 in both faces, then 0.56 and 3.82, because the probe was reading a transition in flight. And the step dot read transform: none on nine placements that render at no width at all.
  7. 7The two-viewport walk found a hole the one-width walk could not. The rise is inside a phone block, so above the desk rung 337 dialogs on 105 screens appeared on one frame with the inventory row marked done. Direction and amplitude depend on width and duration does not, which is the whole reason the closing sweep walks two.
  8. 8And the stage's own instrument could not see a control that does not move, 2026-08-17. Every reading above starts from a duration and groups it by role, so a component that declares no transition at all contributes no rows. Reported by a person looking at the category strip. The inverse question, does the element that CHANGES a painted property carry that property in its own transition-property, gave 77 declarations and 20,864 property-on-placement readings changing with nothing in between, and the repeating shape was an ATOM writing no response while some of its FACES wrote their own: chip answered on two faces of five, iconbtn on four of six, and .btn-provider re-declared the list to add a transform and lost color and box-shadow, because a transition declaration replaces the one it out-specifies rather than extending it. One declaration on the atom, carrying the union of everything any face changes.
  9. 9A fade makes every z-index underneath it inert, and the one that broke was present and correct, 2026-08-18. ::details-content fades from opacity:0, and a box under full opacity IS a stacking context, so for the length of the arrival the filter panel's --z-menu was resolved inside that group instead of against the page. 14 of 14 panels on 6 screens opened UNDER the first card, in Chromium and WebKit both, and jumped on top when the fade landed. Reported by a person on a screenshot, which is the fifth time. The header's two menus were already clean and that is the fix: they open inside a sticky header that is its own lifted group, 0 of 5 each. The lift belongs to the element the fade turns into a context, not to the panel inside it.