makeover-timing 0.1.1

The time axis of the make-family design system: a duration is named by what it is waiting for, never by a number. Four intents, one duration each, the same on every renderer.
Documentation
# makeover-timing

The time axis of the make-family design system.
[`makeover`](https://makenot.work/git/max/makeover) resolves colour,
[`makeover-geometry`](https://makenot.work/git/max/makeover-geometry) resolves
distance, and
[`makeover-layout`](https://makenot.work/git/max/makeover-layout) names what a
thing is without resolving anything. This crate answers the remaining question
of the same shape: how long.

## A duration is named by what it is waiting for

| Intent | What it waits for | Resolves to |
|---|---|---|
| `revert` | a control returning to its resting label after confirming | 1500ms |
| `clear` | a status line emptying itself | 2000ms |
| `dismiss` | a transient notice's lifetime | 3000ms |
| `debounce` | typing settling before the work starts | 150ms |

Whether a toast should live 3000ms or 3500ms is unanswerable on its own.
Whether a message is a toast or a banner is not. That is the whole argument for
the layer, and it is the same one `gap-peer` makes about six pixels.

## One duration per intent, on every renderer

An intent resolves to exactly one duration everywhere. Not one per renderer,
not one per theme.

Distance has a renderer axis because a fingertip is coarser than a cursor and a
terminal cell is coarser than a pixel: the surface differs. Time does not
differ that way. A second is a second in a browser, in egui and in a terminal,
and the reader waiting it out is the same reader.

What looked like a renderer disagreement in the tree turned out to be drift.
MNW debounced one typeahead at 150ms and two others at 200ms, in one repo, on
one renderer, and a per-renderer table would not have caught it. So a
divergence here is a bug report, not an axis.

## What is deliberately not a timing intent

**A race is not an intent.** Waiting 300ms before navigating because the write
"should" have landed, or 150ms before closing a dropdown so a mousedown can
beat the blur, is a synchronisation bug wearing a duration's clothes. Naming
those would launder them into design decisions and give every future one a
token to hide behind. They get fixed per screen.

**Severity is not an intent, and it is not a fifth number.** Two renderers
already reached for one: MNW gives an error toast 6000ms against an ordinary
one's 3000ms, and audiofiles' footer never expires an error at all. Read
together those are one statement, not two durations. A message the user must
not miss does not go away on its own, `makeover-layout` already has the word
for that (such a message is not `Notice::transient`), and a banner has no
lifetime.

**A poll interval is not an intent.** A retry backoff, a health check, an
update check: those are answerable from what they talk to, not from what a
reader can follow.

## Motion is a separate axis

`Intent` says how long a state lasts. `Motion` says how long a change takes.
CSS itself draws that line, a `setTimeout` against a `transition-duration`, and
folding the two together is what makes a "timing scale" unusable: 300ms of fade
and 3000ms of toast are not two rungs of one ramp.

`Motion` has one rung, `fade` at 300ms, because the tree has one measured
transition. It grows when something is measured, not when a scale looks short.

## Using it

Web surfaces bake the stylesheet in at build time. Nothing here changes at
runtime, so there is no load-time JS step:

```rust
use makeover_timing::timing_css;

std::fs::write("static/timing.css", timing_css())?;
```

```css
:root {
  --timing-revert: 1500ms;
  --timing-clear: 2000ms;
  --timing-dismiss: 3000ms;
  --timing-debounce: 150ms;

  --motion-fade: 300ms;
}
```

egui and ratatui surfaces resolve through `Duration` instead, which is the
reason this is a crate rather than a stylesheet:

```rust
use makeover_timing::{Intent, Motion, notice_lifetime};

let settle = Intent::Debounce.duration();
let gone = notice_lifetime(true).map(|life| life + Motion::Fade.duration());
```

`notice_lifetime` is the seam the crate was built for. `makeover-layout`
documents `Notice::Toast` as "transient, stacked, dismisses itself" and says
nothing about when; this says when, on the renderer's side of the line. It
takes the bool rather than the enum so this crate stays off `makeover-layout`'s
dependency graph, and the bool is exactly what the description asserts.

## Where the numbers came from

Every value is a count from the tree, taken 2026-08-18 and re-checked
2026-08-21, not a preference:

```text
revert     1500ms   MNW: 6 hand-rolled sites, plus core/clipboard.ts's own default
clear      2000ms   MNW: 4 sites
dismiss    3000ms   MNW: the toast renderer's lifetime
debounce    150ms   audiofiles SEARCH_DEBOUNCE, MNW docs-search.js
fade        300ms   MNW: TOAST_FADE_MS, matching the .fade-out transition
```

The one contested value is the debounce, where MNW's two category typeaheads
sit at 200ms against everything else's 150ms. 150 wins on the count and on the
cross-renderer agreement, and the 200s conform.

## Licence

MIT.