Capstone 25 / 25

25 Your motion system 25 Your motion system 25 Your motion system 25 Your motion system

A motion system is a handful of decisions, made once, written down, and kept everywhere.

Chapter 24 took apart the motion system of this site. This chapter asks you to build one of your own, and to use everything before it while you do.

You will choose a product and design its motion system in the five layers of chapter 24: tokens, primitives, orchestration, patterns and policy. You will build one screen with it, with six moments: an enter, an exit, a shared element, a gesture, a canvas moment, and an animated infographic. Then you will publish it the way this course publishes everything, as figures with captions. A worked example runs through the chapter, so you can see each step done once before you do it yourself. You can do all of it alone, in a plain HTML page and two or three files. Every step ends with a Done when line: a check you can run yourself, so you know you can move on.

One system, playing

These are four moments from a finished system, made for an imaginary notes app. Hover or click one to play it again. Watch how each one starts and how it leaves.

New note Rises a little and settles. Leaves quicker than it came.
Open a note The card grows into the editor, and back into its card.
Saved A receipt, from the edge it lives on.
Day's goal Fills to its value and stops there.
Fig. 25.1 — A notes app's moments, from one system

Watch them as a set. Nothing bounces. Nothing travels far. Whatever leaves does so faster than it arrived (chapter 9). The note that opens and the goal bar don’t come or go at all: they change in place (chapters 10 and 23). No single moment here is remarkable. Together they sound like one voice, and that is what a system is for.

The same screen has two more moments that need a finger and a loop: a swipe to archive a note, and a small moment drawn on a canvas. They come later in the chapter.

Which one fits a notes app?

Three versions of the same moment. Each would be right for some product. Only one fits this one. Decide by eye, as you did in chapter 22.

Feel

A new note appears in an app people write in for an hour at a time. Which one belongs there?

The brief, as a checklist

Eight steps. Each one points back to the chapters that taught it. Do them in order: each uses the last one’s output. Expect the first five to be writing and the sixth to be the only real build.

  1. Pick a voice. Three words for how the product moves, and one feeling to avoid (chapter 22).
  2. Write the tokens. Durations, curves, springs, distances, a stagger step. Keep the scale small (02, 03, 05, 09).
  3. Make primitives. Single motions written only in tokens, each with an interruption rule (chapter 24).
  4. Compose patterns for the moments your screen needs, using orchestration such as stagger (08, 24) and shared elements (10).
  5. Set the policy: reduced motion, a frequency budget, and what may run at once (21, 22, 24).
  6. Build the screen’s six moments: an enter, an exit, a shared element, a gesture, a canvas moment, an animated infographic.
  7. Document it: a spacing chart for every primitive (03), captioned like a figure, and a specimen sheet.
  8. Critique it with the protocol from chapter 22 and the Eye Trainer. Save specimens in the Journal. Export with the Export desk.

1 · A voice

Choose a product, real or imagined, and describe how it moves in three words. Pick words you could check against a spacing chart: calm can be seen, fun can’t. Then name one feeling to avoid. That word is the useful one, because it turns into rules.

The example is a notes app. Its words are calm, clear, quiet. The feeling to avoid is busy. Chapter 22 called this a personality: the same tools, chosen on purpose.

  • Calm: nothing starts with a jolt, and nothing overshoots (chapter 3, chapter 5).
  • Clear: every motion says where a thing came from and where it went (chapter 9, chapter 11).
  • Quiet: short distances, and durations short enough that nobody waits for them (chapter 2).
  • Never busy: one moment on stage at a time, and no motion at all while you type.

Done when: each of your three words has a line like these, a rule you could check against a spacing chart or a stopwatch, and the feeling to avoid has one too. If a rule can’t be checked, the word is too vague. Swap it.

2 · Tokens

Tokens are the only place a number may live. Every duration, curve, spring, distance and stagger step in the product is one of them. Keep the scale small: if you can’t tell two tokens apart by eye, you only need one of them. This site gets by on fourteen, listed in chapter 24 and on its specimen sheet.

The notes app has ten, and one rule. It keeps this site’s names, --ease-out and the rest, with values of its own.

TokenValueUsed for
--dur-quick160ms, exit 112mstoggles, menus, cross-fades
--dur-base240ms, exit 168msnotes arriving, leaving, and moving
--dur-scene360ms, exit 252msopening a note, the week chart
--ease-outcubic-bezier(.3, .6, .2, 1)enter
--ease-incubic-bezier(.4, 0, 1, 1)exit
--ease-inoutcubic-bezier(.65, 0, .35, 1)on-screen moves
spring.settleresponse 0.35, bounce 0anything under a finger
--dist-nudge6pxenters and exits
--dist-travel16pxthe Archived toast, from the bottom edge
--stagger-step24msrows on launch

The rule is the one from chapter 9 and chapter 24: an exit takes 0.7× the time of its enter, which the build writes as each duration’s -exit twin. Nothing is special about 0.7. Keep whatever ratio you like, as long as exits stay shorter.

Each value comes from the voice, starting from this site’s value in chapter 24 and moved by feel until the Export desk below showed the voice you chose. Move one value at a time. The durations sit under this site’s, by 11 to 25%, and both distances are smaller: a notes app is used all day, and the more often a thing is seen, the less it should move (chapter 22). Calm lives in the enter curve. A cubic-bezier’s opening speed is y1 / x1 times linear speed, the slope of its first handle (chapter 3). This site’s enter, cubic-bezier(.2, .8, .2, 1), opens at four times; the notes app’s, cubic-bezier(.3, .6, .2, 1), at twice. In the first quarter of its time, this site’s enter covers 77% of the way and the notes app’s covers 61%. It arrives without a jolt. To check this for your own curve, divide its y1 by its x1: a bigger number is a sharper start.

There is one spring, and it has no bounce, because calm rules out overshoot. A note thrown by a finger can still pass its place a little. That overshoot is honest: it is the finger’s speed, not the spring’s (chapter 5). With mass 1, the spring is stiffness 322.3 and damping 35.9, and its duration is a result, not a choice: it settles in 544ms. (Chapter 5 gives the formulas: stiffness is (2π ÷ response)² × mass, damping is 4π ÷ response × mass, and the Export desk works them out for you.)

Done when: you have a tokens.json like the one under The notation, with your own values, and for every pair of durations, curves or distances you can tell them apart by eye in the Lab. If you can’t, delete one.

3 and 4 · Primitives and patterns

A primitive is one motion, written only in tokens. It can take a token’s name as an argument, never a raw number. And it declares what happens when something interrupts it, the contract from chapter 24. There are four answers:

  • Reverse. Turn back from the frame on screen, without jumping to an end first.
  • Retarget. Head for the new target from where it is now. A spring keeps its velocity as it turns.
  • Queue. Wait until the current motion settles. Only for small moments that block nothing.
  • Finish. Jump to the end state, then run the new motion.

The notes app has three primitives. Their names are its own, chosen for this product; chapter 24’s fade and rise belong to that chapter’s system, and yours will have its own names too.

  • appear fades and rises into place. Going out, it sinks and fades, quicker. Tokens: --dur-base, --ease-out and --ease-in, --dist-nudge. If interrupted: reverse.
  • slide carries a thing to its new place, FLIP-style. Tokens: --dur-base, --ease-inout. If interrupted: retarget.
  • settle is a spring to a target that starts at the finger’s speed (chapter 12). Token: spring.settle. If interrupted: retarget, keeping its velocity.

A pattern is a moment of the product, built from primitives. Name it after the moment, not the motion: noteOpen, not scaleAndFade. The moment is what will still be true when you change the motion. The notes app needs two.

  • noteOpen. The tapped card becomes the editor: a shared element (chapter 10). The card travels on --ease-inout for --dur-scene, because it is the longest trip on the screen. Everything else cross-fades over --dur-quick. It is built on the View Transitions API, where starting a new transition skips the running one to its end. So its rule is finish, and it works where the API exists; elsewhere the editor simply appears.
  • noteArchive. The swipe. The row follows the finger 1:1. On release, settle carries it off at the finger’s speed, or back into place if the throw was too weak (chapter 12, chapter 6). The rows below slide up together, as one (chapter 8). Then an Archived toast with Undo rises from the bottom edge: appear again, with --dist-travel in place of --dist-nudge.

Done when, for primitives: each one is a function that contains no number of its own, only token names, and you can say its interruption rule aloud. Test it by calling it twice quickly on one element: it must turn back, not restart. Done when, for patterns: each has a name from the product’s vocabulary, a list of the primitives it calls, and one sentence on what happens if it is interrupted.

5 · Policy

Policy is the set of rules every motion obeys, whoever wrote it. Write it in sentences first. Code it second. One rule is already set: each primitive and pattern above declares its interruption rule.

Reduced motion: reduce, don’t remove (chapter 21). Both distances become 0, so every appear is a fade. noteOpen names nothing, so nothing travels and the screen cross-fades. The swipe still follows the finger, because that motion is the user’s own, but on release the row fades out instead of flying. The canvas moment doesn’t play.

A frequency budget (chapter 22). The more often a moment is seen, the less it may move.

How oftenMomentsBudget
All the timetyping, scrolling, the cursorno motion
Oftentoggles, menus, Saved, a new note--dur-quick or --dur-base, --dist-nudge
Sometimesopening a note, archiving, switching weeksup to --dur-scene
Once a daythe day’s goal is metthe one expressive moment

One moment at a time. A moment may move many elements: a list of notes arriving is one moment. But two moments never overlap. If a note is opening, the Saved toast waits until it settles. Its rule is queue. As with the site’s concurrency budget in chapter 24, this is a rule for the people who write and review motion until you build code that enforces it.

Cheap properties (chapter 20). Whatever the app animates itself, it animates with transform and opacity. Rows close a gap with FLIP, never by animating height, and the chart’s bars grow with scaleY from their baseline.

Done when: the policy fits on one screen as sentences, and you have tested the first rule. In your browser’s developer tools, or in your system settings, switch on reduce motion and reload: nothing may travel, and every fade must still be there.

6 · One screen, six moments

The screen is the list of notes, with the week’s writing charted above it. Each moment uses the system; none brings its own numbers. Build one moment at a time, in the table’s order, and check each before starting the next. Use plain HTML, tokens.css, and the appear.js from the notation: the enter and the exit need nothing else. Choose the medium for each one (chapter 19), and give each property one owner (chapter 24): CSS for state, JavaScript for choreography and physics, a canvas for what is drawn frame by frame.

MomentIn the notes appBuilt from
EnterOn launch, the notes rise in, one stagger step apartappear and --stagger-step · chapters 08, 09
ExitA deleted note sinks and fades where it stands; the rows below close the gapappear out, then slide · chapter 09
Shared elementThe tapped card becomes the editor; run the other way, it goes back into its cardnoteOpen · chapter 10
GestureSwipe a note to archive itnoteArchive and settle · chapters 12, 06
Canvas momentWhen the day’s goal is met, a few specks of ink rise from the word count and fadea loop with dt, and particles · chapters 14, 17
Animated infographicWords per day, this week. Switch weeks and each day’s bar grows or shrinks to its new value--ease-inout for --dur-scene, bars keyed by weekday · chapter 23

Done when, for each moment: it plays from a button, it runs on tokens only (search your files for a raw number of milliseconds; there should be none outside tokens.json), it can be interrupted without a jump, and it obeys reduced motion. The chart’s bars also pass one more check: switch weeks twice quickly and no bar may jump or overshoot.

Two of these need a word more.

The specks are the one moment the tokens don’t fully describe, because canvas motion is procedural. Policy still holds. The loop runs on dt, so it looks the same at any refresh rate (chapter 14). Its randomness is seeded, so every replay is identical. It happens once a day, which is what earns it the right to be the most expressive thing in the product.

The chart is information first. Each bar is keyed to its weekday, so Tuesday stays Tuesday when the week changes: the bar moves, it is never replaced. It never overshoots, because a bar that passes its value claims, for a moment, a number that isn’t true (chapter 23). And the value labels appear when the bars settle, not while they travel.

7 · Publish it

Publish the system the way this course publishes every idea: as figures with captions. Make one spacing chart per primitive in the Spacing chart, number your figures, and caption them in small caps, like Fig. 25.2 — Appear, enter, 14 fr (fr is frames: the chart shows 14 of them at 60Hz). This is the notes app’s.

appear · in14 fr · 240ms01234567814appear · out10 fr · 168ms02345678910slide14 fr · 240ms03456789101114settle · spring33 fr · 544ms0123456789101112141633
00/60 fr 0ms
Fig. 25.2 — The notes app's primitives, published

Read it as someone joining your team would. The enter’s ghosts crowd at the end, where it settles. Its first three gaps grow before they shrink: it gathers speed instead of jumping, and that is what calm looks like in a spacing chart. Take the first frame at 60Hz over 240ms: this site’s enter curve would cover 28% of the way in it, the notes app’s 15%. The exit’s ghosts crowd at the start, and it is gone sooner. The slide is slow at both ends, because it starts and ends on screen. The spring, drawn from its physics with no duration typed in, comes to rest without passing its line.

A finished document has five parts:

  1. The voice. Three words, and the feeling to avoid.
  2. The token table, with what each token is for.
  3. A spacing chart for every primitive, with its spec sentence from the Export desk underneath.
  4. A specimen sheet. Every moment of the screen, playable, with a one-line note, like Figure 25.1.
  5. The policy, in sentences.

Done when: a stranger could rebuild one of your motions from the document alone, without opening your code. Give it to someone, or reread it a day later and try.

This site publishes its own on its specimen sheet.

8 · Critique it

Then look at the screen the way chapter 22 taught: name the feeling, find its cause, name the parameter, change one thing.

In the example, launch felt slow. The cause: with twelve notes, the last row started eleven stagger steps late and landed just over half a second after launch (chapter 8: the count multiplies the step). The parameter: the stagger. The change: rows after the eighth arrive with the eighth. That brings the last one in at 408ms. One change, then look again: if the feeling is gone, stop; if a new one has appeared, name that one and repeat. Done when the critique names one feeling, one cause and one parameter, and the change leaves your tokens file with a new value or a new rule, not a special case in one component.

Sharpen your eye before each critique with a few rounds of Blind A/B in the Eye Trainer. And keep a record. Every instrument has Save to Journal: below is the journal itself. Save each token as you settle it, and name it with the words it earned. Over time the Journal shows what you mean by calm, in milliseconds.

Your motion vocabulary

Each word, and what you meant by it in numbers: typical duration, curve, and ζ (damping) for springs. Click a word to see its specimens.

Specimens

Export the system

The Export desk writes one motion in every notation: CSS, WAAPI, Motion, GSAP and Canvas, plus design-token JSON and a sentence a designer can read. It opens on the notes app’s appear: a rise of 6px over 240ms on --ease-out.

  • Read the sentence in the handoff card. That line goes under the spacing chart of appear in your document.
  • Type the exit into Any easing, cubic-bezier(.4, 0, 1, 1) with 168 beside it, and press Convert. Every notation rewrites itself. They are the same numbers written differently.
  • Now type spring(response .35 bounce 0). The duration box no longer matters: a spring’s time comes from its physics, 544ms here. The token JSON keeps both pairs, stiffness and damping, response and bounce.
  • Do the same for each of your own tokens, or choose a specimen from your Journal. Done when every token in your document has an exported line next to it that a designer could check.
box · y14 fr · 240ms
00/60 fr 0ms
CSS
/* Export */
.box {
  transform: translateY(6px);
  transition: transform 240ms cubic-bezier(0.3, 0.6, 0.2, 1);
}

.is-on .box {
  transform: translateY(0px);
}
WAAPI
// Export
const box = document.querySelector('.box');

box.animate(
  [{ transform: 'translateY(6px)' }, { transform: 'translateY(0px)' }],
  {
    duration: 240,
    easing: 'cubic-bezier(0.3, 0.6, 0.2, 1)',
    fill: 'both',
  },
);
Motion
// Export
import { animate } from 'motion';

animate('.box', { y: [6, 0] }, {
  duration: 0.24,
  ease: [0.3, 0.6, 0.2, 1],
});
GSAP
// Export
import { gsap } from 'gsap';
import { CustomEase } from 'gsap/CustomEase';

gsap.registerPlugin(CustomEase);

gsap.fromTo('.box', { y: 6 }, {
  y: 0,
  duration: 0.24,
  ease: CustomEase.create('ease0', 'M0,0 C0.3,0.6 0.2,1 1,1'),
});
Canvas
// Export
// update(dt) moves things; draw(ctx, width, height) paints them. dt is in seconds.

const box = { x: 0, y: 6, scale: 1, opacity: 1, rotate: 0 };
const from = 6, to = 0;
const duration = 0.24;   // seconds
const ease = cubicBezier(0.3, 0.6, 0.2, 1);
let time = 0;

function update(dt) {
  time += dt;
  const t = Math.min(Math.max(time / duration, 0), 1);
  box.y = from + (to - from) * ease(t);
}

function draw(ctx, width, height) {
  ctx.clearRect(0, 0, width, height);
  drawBox(ctx, box, 60, height / 2);
}

function drawBox(ctx, o, x, y) {
  ctx.save();
  ctx.globalAlpha = o.opacity;
  ctx.translate(x + o.x, y + o.y);
  ctx.rotate((o.rotate * Math.PI) / 180);
  ctx.scale(o.scale, o.scale);
  ctx.fillStyle = '#FF3B1F';
  ctx.fillRect(-20, -20, 40, 40);
  ctx.restore();
}

// cubic-bezier(), solved the way browsers do: find t for x, then read y.
function cubicBezier(x1, y1, x2, y2) {
  const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
  const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
  const X = (t) => ((ax * t + bx) * t + cx) * t;
  const dX = (t) => (3 * ax * t + 2 * bx) * t + cx;
  return (x) => {
    let t = x;
    for (let i = 0; i < 8; i++) {   // Newton: fast while the slope is healthy
      const d = dX(t);
      if (Math.abs(d) < 1e-6) break;
      t -= (X(t) - x) / d;
    }
    if (!(t >= 0 && t <= 1) || Math.abs(X(t) - x) > 1e-7) {
      let lo = 0, hi = 1;   // bisection: slow and certain
      for (let i = 0; i < 40; i++) {
        t = (lo + hi) / 2;
        if (X(t) < x) lo = t;
        else hi = t;
      }
    }
    return ((ay * t + by) * t + cy) * t;
  };
}
Design handoff · tokens + spec

box · y: 6px → 0px over 240ms (14 frames), cubic-bezier(0.3, 0.6, 0.2, 1).

Tokens in the W3C Design Tokens format (2025.10): durations as a value and a unit, curves as cubicBezier.

{
  "motion": {
    "box-y": {
      "duration": {
        "$type": "duration",
        "$value": {
          "value": 240,
          "unit": "ms"
        }
      },
      "delay": {
        "$type": "duration",
        "$value": {
          "value": 0,
          "unit": "ms"
        }
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": [
          0.3,
          0.6,
          0.2,
          1
        ]
      },
      "from": {
        "$type": "dimension",
        "$value": {
          "value": 6,
          "unit": "px"
        }
      },
      "to": {
        "$type": "dimension",
        "$value": {
          "value": 0,
          "unit": "px"
        }
      }
    }
  }
}

The notation

The whole example, written down: the token file, what it compiles to, one primitive, and one pattern in its CSS and JavaScript halves. The shape is this site’s own. One JSON file is the single source of truth; a short build script compiles it to CSS variables and a JavaScript module, and springs become linear() on the way. Nothing else in the code holds a number.

25 · Your motion system: one source of truth Open in Lab
{
  "$description": "Motion tokens for a notes app. Calm, clear, quiet. Never busy.",
  "duration": {
    "quick": { "value": 160, "use": "toggles, menus, cross-fades" },
    "base": { "value": 240, "use": "notes arriving, leaving, and moving" },
    "scene": { "value": 360, "use": "opening a note, the week chart" }
  },
  "exitRatio": 0.7,
  "easing": {
    "out": { "value": [0.3, 0.6, 0.2, 1], "use": "enter" },
    "in": { "value": [0.4, 0, 1, 1], "use": "exit" },
    "inout": { "value": [0.65, 0, 0.35, 1], "use": "on-screen moves" }
  },
  "spring": {
    "settle": { "response": 0.35, "bounce": 0, "use": "anything under a finger" }
  },
  "distance": {
    "nudge": { "value": 6, "use": "enters and exits" },
    "travel": { "value": 16, "use": "the Archived toast, from the bottom edge" }
  },
  "stagger": {
    "step": { "value": 24, "use": "rows on launch" }
  }
}
25 · Your motion system: the tokens as CSS, with the policy Open in Lab
/* tokens.css. Compiled from tokens.json: don't edit by hand. */
:root {
  /* Durations. Exits run at 0.7× the enter. */
  --dur-quick: 160ms; /* toggles, menus, cross-fades */
  --dur-quick-exit: 112ms;
  --dur-base: 240ms; /* notes arriving, leaving, and moving */
  --dur-base-exit: 168ms;
  --dur-scene: 360ms; /* opening a note, the week chart */
  --dur-scene-exit: 252ms;

  /* Curves */
  --ease-out: cubic-bezier(0.3, 0.6, 0.2, 1); /* enter */
  --ease-in: cubic-bezier(0.4, 0, 1, 1); /* exit */
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1); /* on-screen moves */

  /* spring.settle: response 0.35, bounce 0 (anything under a finger).
     Sampled into linear() over its settle time. */
  --spring-settle: linear(0, 0.0045 1%, 0.0168 2%, 0.0428 3.33%,
    0.0771 4.67%, 0.1173 6%, 0.1614 7.33%, 0.3624 13%, 0.4629 16%,
    0.5146 17.67%, 0.5629 19.33%, 0.6408 22.33%, 0.6791 24%, 0.714 25.67%,
    0.7457 27.33%, 0.7743 29%, 0.8001 30.67%, 0.8275 32.67%, 0.8691 36.33%,
    0.9063 40.67%, 0.9352 45.33%, 0.9578 50.67%, 0.9749 57%, 0.986 64%,
    0.9931 72.33%, 1);
  --spring-settle-dur: 544ms;

  /* Distances */
  --dist-nudge: 6px; /* enters and exits */
  --dist-travel: 16px; /* the Archived toast, from the bottom edge */

  --stagger-step: 24ms;
}

/* Policy: reduce, don't remove. Movement stops; fades stay. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dist-nudge: 0px;
    --dist-travel: 0px;
  }
}
25 · Your motion system: a primitive with its interruption rule Open in Lab
// appear.js. One motion, written only in tokens.
// tokens.js is the JavaScript half of the same build.
import { duration, exitDuration, easingCss, distance } from './tokens.js';

const running = new WeakMap();

// Interruption rule: reverse. Caught mid-flight, it turns back from
// the frame on screen. It never jumps to an end first.
export function appear(el, dir = 'in', { dist = 'nudge', delay = 0 } = {}) {
  // Policy: reduce, don't remove. No travel; the fade stays.
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const y = reduce ? 0 : distance[dist]; // a token's name, never a number
  const hidden = { opacity: 0, transform: `translateY(${y}px)` };
  const shown = { opacity: 1, transform: 'none' };

  const prev = running.get(el);
  const now = getComputedStyle(el);
  const from =
    prev?.playState === 'running'
      ? { opacity: now.opacity, transform: now.transform } // where it is
      : dir === 'in' ? hidden : shown;
  prev?.cancel();

  const timing =
    dir === 'in'
      ? { duration: duration.base, easing: easingCss.out }
      : { duration: exitDuration.base, easing: easingCss.in };
  const anim = el.animate([from, dir === 'in' ? shown : hidden], {
    ...timing,
    delay,
    fill: 'both',
  });
  running.set(el, anim);
  return anim;
}

// The Archived toast rises from the bottom edge: same primitive, another token.
// appear(archived, 'in', { dist: 'travel' });
25 · Your motion system: a pattern's timing Open in Lab
/* noteOpen. The card's trip is an on-screen move: the in-out
   curve, at scene length, the longest trip on the screen. */
::view-transition-group(note) {
  animation-duration: var(--dur-scene);
  animation-timing-function: var(--ease-inout);
}

/* Everything unnamed cross-fades as "root". Keep it quick. */
::view-transition-group(root) {
  animation-duration: var(--dur-quick);
}
25 · Your motion system: a pattern, noteOpen Open in Lab
// noteOpen.js. A moment of the product, built on the system.
// Its timing lives in CSS, on ::view-transition-group(note).
// Interruption rule: finish. Starting a new view transition
// skips the one in progress to its end state.
// Closing is the same call with the two elements swapped.
export async function noteOpen(card, editor, showEditor) {
  if (!document.startViewTransition) {
    showEditor(); // no API: just change
    return;
  }

  // Policy: under reduced motion nothing is named, so nothing
  // travels. The screen still cross-fades.
  const share = !matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (share) card.style.viewTransitionName = 'note';

  const transition = document.startViewTransition(() => {
    card.style.viewTransitionName = ''; // one element per name
    if (share) editor.style.viewTransitionName = 'note';
    showEditor(); // your own update: the editor is now on screen
  });
  await transition.finished;
  editor.style.viewTransitionName = '';
}

Blind A/B: a mixed final round

Two versions of a moment from anywhere in the course. Pick the better one, then say why in one word. That word is where every critique starts.

Eye trainer · choose Blind A/B

Two versions. Which is better? Then say why, in one word.Taste: judging, then naming the reason.

5 trials. Judge with your eyes first; the numbers come after. Three right in a row makes trials harder, a miss eases them.

Full rounds and your calibration in the Eye trainer

When all eight are done, you have a system another person could join. Every token in your system answers a question this course taught you to ask: how long, how the frames are spaced, how far, how heavy. The same loop builds all of them: see it, feel it, tune it, then write it down.