altitude-design 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/altitude.mjs +120 -20
  3. package/dist/guidance/README.md +4 -0
  4. package/dist/guidance/agent-setup.md +3 -2
  5. package/dist/guidance/components/AILabel/README.md +25 -0
  6. package/dist/guidance/components/Accordion/README.md +7 -0
  7. package/dist/guidance/components/AgentFleet/README.md +34 -0
  8. package/dist/guidance/components/AgentLanes/README.md +21 -0
  9. package/dist/guidance/components/AgentRequests/README.md +29 -0
  10. package/dist/guidance/components/Alert/README.md +22 -0
  11. package/dist/guidance/components/AltitudeProvider/README.md +48 -0
  12. package/dist/guidance/components/AppShell/README.md +51 -0
  13. package/dist/guidance/components/AreaChart/README.md +43 -0
  14. package/dist/guidance/components/Artifact/README.md +30 -0
  15. package/dist/guidance/components/Avatar/README.md +34 -0
  16. package/dist/guidance/components/Await/README.md +80 -0
  17. package/dist/guidance/components/Badge/README.md +22 -0
  18. package/dist/guidance/components/BarChart/README.md +47 -0
  19. package/dist/guidance/components/BarList/README.md +29 -0
  20. package/dist/guidance/components/Board/README.md +31 -0
  21. package/dist/guidance/components/Breadcrumb/README.md +35 -0
  22. package/dist/guidance/components/Button/README.md +17 -0
  23. package/dist/guidance/components/Card/README.md +14 -0
  24. package/dist/guidance/components/ChartBrush/README.md +41 -0
  25. package/dist/guidance/components/ChartContainer/README.md +152 -0
  26. package/dist/guidance/components/ChartLegend/README.md +35 -0
  27. package/dist/guidance/components/ChartReference/README.md +41 -0
  28. package/dist/guidance/components/ChartTooltip/README.md +38 -0
  29. package/dist/guidance/components/Checkbox/README.md +11 -0
  30. package/dist/guidance/components/Checklist/README.md +71 -0
  31. package/dist/guidance/components/CodeBlock/README.md +28 -0
  32. package/dist/guidance/components/ColorPicker/README.md +30 -0
  33. package/dist/guidance/components/Combobox/README.md +16 -0
  34. package/dist/guidance/components/CommandPalette/README.md +27 -0
  35. package/dist/guidance/components/CommentThread/README.md +30 -0
  36. package/dist/guidance/components/Confirmation/README.md +34 -0
  37. package/dist/guidance/components/ContextMenu/README.md +30 -0
  38. package/dist/guidance/components/Conversation/README.md +45 -0
  39. package/dist/guidance/components/CopyButton/README.md +23 -0
  40. package/dist/guidance/components/DataGrid/README.md +82 -0
  41. package/dist/guidance/components/DatePicker/README.md +67 -0
  42. package/dist/guidance/components/Dialog/README.md +29 -0
  43. package/dist/guidance/components/DiffView/README.md +30 -0
  44. package/dist/guidance/components/DotPlot/README.md +38 -0
  45. package/dist/guidance/components/Drilldown/README.md +15 -0
  46. package/dist/guidance/components/DropZone/README.md +21 -0
  47. package/dist/guidance/components/DropdownMenu/README.md +19 -0
  48. package/dist/guidance/components/EditRegion/README.md +104 -0
  49. package/dist/guidance/components/EmptyState/README.md +17 -0
  50. package/dist/guidance/components/FeatureGate/README.md +28 -0
  51. package/dist/guidance/components/Field/README.md +10 -0
  52. package/dist/guidance/components/FilterBar/README.md +69 -0
  53. package/dist/guidance/components/FormattedNumber/README.md +37 -0
  54. package/dist/guidance/components/Heatmap/README.md +45 -0
  55. package/dist/guidance/components/HoldButton/README.md +16 -0
  56. package/dist/guidance/components/HoverCard/README.md +18 -0
  57. package/dist/guidance/components/InlineCitation/README.md +24 -0
  58. package/dist/guidance/components/InlineEdit/README.md +133 -0
  59. package/dist/guidance/components/Input/README.md +27 -0
  60. package/dist/guidance/components/InputOTP/README.md +30 -0
  61. package/dist/guidance/components/ItemGroup/README.md +93 -0
  62. package/dist/guidance/components/Kbd/README.md +9 -0
  63. package/dist/guidance/components/LineChart/README.md +47 -0
  64. package/dist/guidance/components/Link/README.md +28 -0
  65. package/dist/guidance/components/Markdown/README.md +32 -0
  66. package/dist/guidance/components/Meter/README.md +29 -0
  67. package/dist/guidance/components/NotificationCenter/README.md +28 -0
  68. package/dist/guidance/components/NumberInput/README.md +29 -0
  69. package/dist/guidance/components/PageHeader/README.md +29 -0
  70. package/dist/guidance/components/Pagination/README.md +25 -0
  71. package/dist/guidance/components/PasswordInput/README.md +34 -0
  72. package/dist/guidance/components/PermissionMatrix/README.md +28 -0
  73. package/dist/guidance/components/Pile/README.md +12 -0
  74. package/dist/guidance/components/PlanPicker/README.md +25 -0
  75. package/dist/guidance/components/Popover/README.md +12 -0
  76. package/dist/guidance/components/Progress/README.md +10 -0
  77. package/dist/guidance/components/PromptInput/README.md +40 -0
  78. package/dist/guidance/components/PropertyList/README.md +57 -0
  79. package/dist/guidance/components/Question/README.md +27 -0
  80. package/dist/guidance/components/RadioGroup/README.md +10 -0
  81. package/dist/guidance/components/RelativeTime/README.md +26 -0
  82. package/dist/guidance/components/RemovalReceipt/README.md +34 -0
  83. package/dist/guidance/components/Resizable/README.md +36 -0
  84. package/dist/guidance/components/SaveBar/README.md +27 -0
  85. package/dist/guidance/components/SavedViews/README.md +24 -0
  86. package/dist/guidance/components/ScatterChart/README.md +41 -0
  87. package/dist/guidance/components/Schedule/README.md +76 -0
  88. package/dist/guidance/components/ScrollArea/README.md +7 -0
  89. package/dist/guidance/components/SecretField/README.md +28 -0
  90. package/dist/guidance/components/Select/README.md +27 -0
  91. package/dist/guidance/components/Separator/README.md +26 -0
  92. package/dist/guidance/components/Sheet/README.md +32 -0
  93. package/dist/guidance/components/Skeleton/README.md +14 -0
  94. package/dist/guidance/components/Slider/README.md +25 -0
  95. package/dist/guidance/components/SortableList/README.md +11 -0
  96. package/dist/guidance/components/Sparkline/README.md +29 -0
  97. package/dist/guidance/components/SplitButton/README.md +26 -0
  98. package/dist/guidance/components/Stat/README.md +31 -0
  99. package/dist/guidance/components/Stepper/README.md +29 -0
  100. package/dist/guidance/components/Suggestions/README.md +29 -0
  101. package/dist/guidance/components/Surface/README.md +20 -0
  102. package/dist/guidance/components/Switch/README.md +9 -0
  103. package/dist/guidance/components/SyncIndicator/README.md +24 -0
  104. package/dist/guidance/components/Table/README.md +76 -0
  105. package/dist/guidance/components/Tabs/README.md +18 -0
  106. package/dist/guidance/components/TagInput/README.md +66 -0
  107. package/dist/guidance/components/Terminal/README.md +27 -0
  108. package/dist/guidance/components/ThreadList/README.md +37 -0
  109. package/dist/guidance/components/TimeInput/README.md +26 -0
  110. package/dist/guidance/components/Timeline/README.md +32 -0
  111. package/dist/guidance/components/Toaster/README.md +16 -0
  112. package/dist/guidance/components/Toggle/README.md +33 -0
  113. package/dist/guidance/components/Toolbar/README.md +37 -0
  114. package/dist/guidance/components/Tooltip/README.md +12 -0
  115. package/dist/guidance/components/Tour/README.md +29 -0
  116. package/dist/guidance/components/Trace/README.md +37 -0
  117. package/dist/guidance/components/Tree/README.md +86 -0
  118. package/dist/guidance/evidence.json +18 -0
  119. package/dist/guidance/index.json +811 -7
  120. package/dist/guidance/llms.txt +153 -2
  121. package/dist/guidance/manifest.json +133 -12
  122. package/dist/guidance/patterns/screen-layout.md +1 -1
  123. package/dist/guidance/references/build/checks/agent-setup.mjs +7 -0
  124. package/dist/guidance/references/build/checks/guidance.mjs +8 -2
  125. package/dist/guidance/references/build/checks/source-check.mjs +101 -0
  126. package/dist/guidance/rules.json +155 -0
  127. package/dist/guidance/visual/ActionsInPlace.md +463 -0
  128. package/dist/guidance/visual/Charts.md +119 -0
  129. package/dist/guidance/visual/Distinctiveness.md +106 -0
  130. package/dist/guidance/visual/Elevation.md +1 -1
  131. package/dist/guidance/visual/Mobile.md +64 -0
  132. package/dist/guidance/visual/Motion.md +225 -0
  133. package/dist/guidance/visual/README.md +98 -0
  134. package/dist/guidance/visual/Research.md +1 -1
  135. package/dist/guidance/visual/Theming.md +349 -0
  136. package/package.json +1 -1
@@ -0,0 +1,119 @@
1
+ # Charts
2
+
3
+ A chart is the one place where Altitude's own vocabulary is dangerous. On a plot, a bar's *length* is already height on the screen, and Altitude's height (shadow, wall, sheen, tonal lift) is exactly the channel **Elevation** says must never carry an amount. So charts follow one rule before any other: **the data is the record.** It lies on the ground. Everything live about a chart (the readout, the legend you toggle, the window you drag, the drill you take) uses a build Altitude already has.
4
+
5
+ Read **Elevation** and **Motion** first; this chapter applies their laws to data. The evidence is in **Research** ("Charts").
6
+
7
+ ## The rules
8
+
9
+ 1. **The data is the record.** The plot is ground, and its marks are flat ink on it: no cast shadow, no wall, no sheen, no tonal lift and no gradient, ever. Length and position are the most accurately read magnitude channels and depth is one of the weakest; shadowed and 3D bars cost accuracy. A sheen on a bar would also say "press it" (E4), which a bar usually can't honour.
10
+ 2. **Bars stand on the ground line.** The zero baseline is the chart's ground line, a 1px `ink-muted` rule, the same line the `Stepper` and the `Drilldown` staircase stand on. Bar charts always include zero; a bar that starts above zero misstates its length.
11
+ 3. **Height belongs to the chart's chrome, not to its data.** Every live part of a chart is a component you already know (see **Every part, by its build** below). Nothing new is invented for charts.
12
+ 4. **Hover is a promise, in charts too.** On a static chart the crosshair and readout appear and the marks don't respond. A grid has no crosshair, so a `Heatmap` outlines the cell under its readout in 2px `ink` instead, with no pointer cursor: that's the crosshair, not a promise. Only marks that do something when clicked show a hover outline and a pointer cursor. Marks never lift: a bar lifted off the ground line, or one casting a shadow below its top edge, misstates its value.
13
+ 5. **Colour does one job per chart.** Pick one:
14
+ - **identity**: the categorical palette, `data-1` to `data-6` in config order, then `data-other`;
15
+ - **magnitude**: the sequential ramp `data-seq-1` to `data-seq-7`;
16
+ - **polarity**: the diverging ramp `data-div-*` around a meaningful midpoint;
17
+ - **status**: `positive`, `caution`, `critical`, only when the series *means* good or bad, and then with an icon and a word, as status always has.
18
+
19
+ Colour follows the entity: hiding or filtering a series never repaints the others. **Focus** is the one exception, and it isn't a filter: `focus` is the author's statement of what the chart is about, fixed for the chart rather than toggled by the reader. The focused series or records keep their identity colours and everything else takes one shared neutral, `data-context`, with its words in `ink-muted`; it's said in words too ("Highlighted: EU."), and a change of focus cross-fades. A reader's version is `relevance`, which recedes misses to 35% and repaints nothing, so "the author's context" and "your filter" stay two readings (E105). Text wears `ink` and `ink-muted`, never a series colour. `accent` and the `presence-*` hues stay reserved for what they already mean. `data-1` shares the accent's hue so a chart's first series looks like the product, but it's a different colour from `accent` and isn't a control. The values and their gates are in **Theming** ("The data palette").
20
+ 6. **One axis.** No dual-axis charts: two scales on one plot invite a correlation the data never made. Two measures of different scale are two charts, small multiples, or indexed to a common base.
21
+ 7. **Spend changes carefully.** Data that changes glides to its new value at one level, as a re-sort does: it's one change. New data gets an afterglow within the attention budget. Nothing grows from the baseline on first render for decoration; the chart is simply there.
22
+ 8. **A chart never travels alone.** As height never travels alone: every chart has a written summary, a keyboard path through its data, a table view, and direct labels or patterns so no series is told apart by colour alone.
23
+ 9. **Say one thing first.** A chart that makes a point leads with it: a `takeaway` is the chart's heading ("Cedar deployed nearly three times as often as Delta"), and its title becomes the small label above. Readers fixate on and recall the words at the top, and take away what they say, even against the chart (E102). So the takeaway is authored, never generated: write it from the data shown, as a trend or a comparison rather than a restatement of the encoding ("Deploys by team" is a title, not a takeaway). The application owns its truth, as it owns persistence. A row of equal dashboard cards can do without one; a chart in a report rarely should.
24
+
25
+ ## Choosing a form
26
+
27
+ Choose by the job the data does, not by what looks rich.
28
+
29
+ | Job | Form |
30
+ |---|---|
31
+ | Change over time | `LineChart`; `AreaChart` for a total and its parts |
32
+ | Compare amounts across categories | `BarChart`; `BarList` for a ranked top-N |
33
+ | One headline number | `Stat`, with a `Sparkline` for its trend |
34
+ | A value within a known range | `Meter` (a password's strength too, in `PasswordInput`) |
35
+ | A value per cell over two dimensions | `Heatmap` |
36
+ | How two measures relate | `ScatterChart` |
37
+ | Values where zero isn't meaningful, averages with their spread, or before and after per category | `DotPlot` |
38
+ | Part of a whole | a stacked `BarChart`, or a segmented `Meter` |
39
+
40
+ **No pie or donut charts.** Angle and area are read less accurately than position and length, so a part of a whole is a stacked bar or a segmented meter. **No 3D, ever**, for the reasons in rule 1.
41
+
42
+ `Progress` and `Meter` look alike and mean different things. `Progress` is a task under way, which is live, so its fill rises out of a trench. A `Meter` is a reading (storage used, a quota, a score), which is the record, so its fill is flat ink in a flush track.
43
+
44
+ ## Every part, by its build
45
+
46
+ | Part | Tier | Built like |
47
+ |---|---|---|
48
+ | The plot, gridlines and axes | ground | flat; gridlines are `line`; axis text a 13px caption in weight 400, `ink-muted` and tabular, lighter than a label on a control; y labels in a gutter, or inline just above their gridlines on a halo |
49
+ | The zero baseline | ground | the ground line: 1px `ink-muted`, as under `Stepper` and `Drilldown` |
50
+ | Marks (bars, lines, points, cells) | ground | flat ink in a series colour, no shadow, no wall, no sheen |
51
+ | A scatter's dots | ground | flat `data-*` with a marker shape per group, ringed 2px in the plot's colour so overlaps stay apart; outliers named in `ink` with a 1px leader; no word, the trend's included, covers a dot |
52
+ | A heatmap's cells | ground | flat `data-seq` (or `data-div`) steps parted by 2px of the plot's colour; no data is a 1px `line` outline, not a well |
53
+ | A clickable mark, hovered | ground | a 2px `ink` outline and a pointer cursor; it never lifts |
54
+ | The readout | floating, level 3 | a `Tooltip`, rising from the side facing its mark |
55
+ | The crosshair | ground | a 1px `line-control` rule, lighter than a reference line's `ink-muted` because it's a pointer, not something to read against; it doesn't move the marks |
56
+ | A legend you can toggle | sunk and raised | a choice, so it's built like the Switch: a trench with a raised mark when the series is on |
57
+ | A legend that only labels | ground | a plate set flush, like a `Badge`; better still, label the series directly |
58
+ | An overview brush | sunk and raised | the range `Slider`: the series a flat line in a trench, the chosen window a raised lens (level 1, `accent` rim, clear face) whose grips are the thumbs; outside it the overview recedes to 35% |
59
+ | Drilling in | ground, stacked | a `Drilldown`: the detail is laid on top, the staircase shows the way back |
60
+ | Annotations and reference lines | ground | plates set into the ground, like a `Badge`; reference lines are solid 1px `ink-muted`, because dashes mean projected |
61
+ | Loading | sunk | an imprint (`Await`), at the chart's own size |
62
+ | No data | sunk | an opening (`EmptyState`) that says why and what to do |
63
+ | Projected or incomplete data | ground | not yet the record: dashed or hatched, and labelled as such |
64
+ | A chart on a dashboard | raised, level 1 | a `Card` on the ground |
65
+
66
+ ## Signals a chart can carry
67
+
68
+ These are the elevation signals a chart adds to **Elevation**'s table:
69
+
70
+ - **Amounts** are never height. They are length and position on a flat plot standing on the ground line.
71
+ - **Marks that act.** Only where a click selects or drills do marks respond to the pointer, with a 2px `ink` outline and the action in the readout. A selection keeps its colour and recedes the rest, and a drill opens the child chart in `Drilldown` (E41).
72
+ - **Filtered in place.** A filter recedes misses to 35% where they stand and holds the axis, so positions survive; the frame says how many match (E40).
73
+ - **Live.** A live chart shows a change at once and leaves a fading `ink` outline on the mark; a whole refresh glows the "Updated" caption instead. Its window glides with time, and it holds still while someone reads it, with a switch to pause it (E39).
74
+ - **Read against.** Targets, limits, normal ranges and events (`ChartReference`, `ChartAnnotation`) lie flush with the marks: a solid 1px line, a flat `line` band, a `Badge` plate with a leader. Their words are laid out together so they never overlap each other or cover a mark: plates sit in headroom above the data, and a label moves to its line's other side or end before it covers a line or a bar (E38, E101).
75
+ - **Not yet the record.** A projected value or an incomplete period (this month so far) is dashed or hatched and says so in its label and readout. It's the record to come, so it's drawn as an imprint of the record, not raised as live. Built as `status` on `BarChart`, `LineChart` and `AreaChart`, with a "Today" rule where the record ends and an uncertainty `band` on `LineChart` (E37).
76
+ - **A window on the record.** An overview sits in a trench and the chosen window is a raised lens in it, so the part you're looking at is the part you can move. The lens is raised because you move it, not because it's data: the series inside it stays flat.
77
+ - **Read against the record before.** `compare` sets each value against the period before it. Both are settled, so both lie flat on the ground; the earlier one is quieter: a 2px `ink` tick across a bar at its earlier value (the bullet graph's comparative measure), or a 1.5px solid `ink-muted` trace under a line, named at its end. The difference is said in words in the readout, each point's name, the table and the summary (E106).
78
+ - **Changed since.** Read against a snapshot (`since`), what changed while the reader was away is marked flat at its old value (a tick on a bar, a hollow ring on a line), counted in words in a switch that filters to it, and glows once on arrival within the attention budget (E108).
79
+ - **Recency.** A value that just arrived, or just changed, gets the afterglow, within the attention budget, never a lift.
80
+ - **Steps of one ramp.** A heatmap's value is a binned step of the sequential ramp (or the diverging one around a midpoint), each step's range in words in the legend, the cell's name and the table, and a pattern density when colour is lost (E42).
81
+ - **Selection** is a tint and an outline, never a height. Selected marks keep their place on the ground line; unselected ones mute rather than sink.
82
+
83
+ ## Motion
84
+
85
+ The chart's choreography is in **Motion** ("Charts"). In short: changed data glides at one level on `ease-glide`; new data settles with an afterglow; the readout rises like a tooltip; nothing grows from the baseline for decoration; under reduced motion values change in place and nothing travels.
86
+
87
+ ## Dashboards
88
+
89
+ - Filters sit in one row above the charts they scope (`FilterBar`, and `DateRangePicker` with presets), and every chart, stat and table below re-renders against the same slice. Say which slice in words ("Last 30 days · EU").
90
+ - Each chart sits in a `Card` on the ground. The cards are raised because they're things you can work with, not because their data is.
91
+ - The one-summit rule is unchanged: a drill-down is a `Drilldown` or a `Sheet`, never a dialog on a dialog.
92
+ - Headline numbers come first, as `Stat`s, then the charts that explain them, then the table that is the record.
93
+
94
+ ## Accessibility
95
+
96
+ A chart is accessible when someone can get the same answer from it without seeing it. Check every chart against these five, after Chartability (Elavsky, Bennett & Moritz, 2022):
97
+
98
+ 1. **Visual.** Marks hold 3:1 against the plot and text holds 4.5:1; no series is told apart by colour alone (direct labels, patterns or markers).
99
+ 2. **Keyboard.** The chart is one tab stop; arrow keys move through the data, and every readout a pointer gets, a keyboard gets.
100
+ 3. **Screen reader.** The chart has a name and a written summary of what it shows, and each point announces its series, label and value.
101
+ 4. **Reduced motion.** Nothing travels; values still change and the afterglow still tints.
102
+ 5. **Forced colours.** Marks keep their patterns and the baseline and outlines remain. Shadows were never there to lose.
103
+
104
+ Every chart also offers a table view of its data, built with `Table`.
105
+
106
+ ## Charts Altitude doesn't draw
107
+
108
+ Charts already built with shadcn/ui or Recharts can take Altitude's palette and most of these rules: **Theming** ("With shadcn/ui", "Charts") maps the colours and lists the props that carry each rule, and what doesn't carry over.
109
+
110
+ For what Altitude's SVG charts don't draw (tens of thousands of points, maps, sankeys, treemaps), `altitude-design/chart-themes` themes ECharts and Chart.js from the active scheme's tokens: **Theming** ("Charts Altitude doesn't draw"). The rules above still apply; a theme can't keep them for you.
111
+
112
+ ## Don't
113
+
114
+ - Don't give a mark a shadow, a wall, a sheen, a gradient or a glow, "because Altitude is about height".
115
+ - Don't lift a hovered or selected mark.
116
+ - Don't start a bar's axis above zero.
117
+ - Don't put two scales on one plot.
118
+ - Don't colour text with a series colour, or a series with `accent`, `presence-*`, or a status hue that doesn't mean status.
119
+ - Don't animate a chart into place on first render.
@@ -0,0 +1,106 @@
1
+ # Distinctiveness
2
+
3
+ Interfaces built by agents now have a look people recognise and distrust: purple gradients, Inter, a centred hero, three icon cards, a sidebar and a grid of equal cards on every screen. Altitude is made for apps that agents write, so it can fall into that look twice: through its own defaults, and through what agents compose from them. This chapter says how it stays out, and how we keep checking, because the look keeps moving.
4
+
5
+ The evidence is in **Research** ("Generated interfaces converge"). Every tell, with its sources and where Altitude stands, is in [`tells.json`](https://github.com/richlaconte/altitude/blob/v0.4.0/project/tells.json). Looking generated is half the problem; the other half is breaking established rules (hierarchy, labelling, recovery, honesty), which [Fundamentals](Fundamentals.md) covers.
6
+
7
+ ## Why it happens
8
+
9
+ A model writing an interface picks the likeliest choice, and the likeliest choices are the safe ones that fill its training data. UI kits used as shipped make it worse. Four findings shape everything below:
10
+
11
+ - **Structure gives it away more than colour.** Across generative UI tools given the same prompt, layout and overall appearance converge more than palette. Models also vary a design far less than designers do when the same brief moves to a different product.
12
+ - **Banning a tell creates the next one.** With purple banned, output moves to cream and terracotta, near-black with one acid green, Instrument Serif and all-caps mono labels. A blocklist goes stale in months.
13
+ - **Familiar is good.** People rate simple, prototypical pages as most appealing within a fiftieth of a second. Avoiding the generated look doesn't mean being strange.
14
+ - **It's behaviour too.** Generated interfaces skip status, recovery and control, and a quarter of the design reasons a tool gives never reach the screen.
15
+
16
+ ## The rules
17
+
18
+ 1. **Familiar controls, specific structure.** Buttons, fields, menus and dialogs work the way people expect. The arrangement of a screen comes from its content and task, not from a template.
19
+ 2. **Name the task before the layout.** Before composing a screen, write down who's there, what they came to do, the one thing that leads, and the one primary action. A sidebar with a card grid is a choice to justify, not a starting point. A list with detail, a focused editor, a guided flow or a single decision view usually fits better.
20
+ 3. **Height is the signature.** Altitude's distinctiveness is the elevation language: raised slabs, sunk wells, a page that recedes. Repeat it, and don't add decoration to look designed: no gradients, glows, frosted panels or coloured edge stripes. Every shadow is a tier with a meaning and a non-height cue, which is what separates Altitude's depth from the rounded, soft-shadowed default (CM1).
21
+ 4. **One thing leads.** Every screen has a focal point. A grid of equal cards is right only when the items really are equal. Spacing groups: tight within a group, wider between groups.
22
+ 5. **States are the design.** Empty, loading, failed, partial and stale states get as much care as the full one. The framework patterns' contracts say which apply.
23
+ 6. **Copy names things.** The content rules hold, and on top of them: no marketese ("unlock", "seamless", "supercharge", "empower"), no "Welcome back!", no arrows on calls to action. Name the object and the outcome.
24
+ 7. **Check claims against the build.** If the reasoning behind a screen says something ("the primary action stands out", "errors are recoverable"), the screen must show it. Look before saying it's done.
25
+ 8. **Judge, and fix, in clusters.** One gradient proves nothing; a cluster of tells does. Swapping a colour doesn't fix a generic layout, and a fix that only bans this year's default lands on next year's.
26
+
27
+ ## The tells
28
+
29
+ `tells.json` groups the tells by area: colour, type, layout, components, motion, icons and imagery, copy, states, behaviour and consistency. Each has an id (`CO4`, `LA5`), its sources with their strength (peer-reviewed, preprint, vendor or practitioner), how it can be caught, and Altitude's status:
30
+
31
+ | `detect` | What catches it |
32
+ |---|---|
33
+ | `lint` | Source text: hues, font families, `background-clip: text`, uppercase labels, easing, emoji, banned words. `build/checks/tells-lint.mjs` has a rule for each one, and fails if a rule and this file disagree. |
34
+ | `render` | A rendered preview: equal-weight grids and spacing rhythm (`build/checks/render-tells.mjs`, warnings only), and contrast (the existing checks) |
35
+ | `measure` | The [distinctiveness benchmark](https://github.com/richlaconte/altitude/blob/main/evals/distinctiveness/README.md): how far screens sit from what default tools generate ([#322](https://github.com/richlaconte/altitude/issues/322)) |
36
+ | `human` | Review and the study with people ([#326](https://github.com/richlaconte/altitude/issues/326)) |
37
+
38
+ ## Where Altitude stands
39
+
40
+ The 2026-10-30 audit found 4 tells exposed; the two schemes and the landing page's labels were fixed the same day. The tells lint (#320) then found one the audit couldn't: Artifact and Terminal, added afterwards, had a coloured failure stripe. The rendered check (#321) then cleared LA6, and the stripe was removed (#374). The screen layout pattern (#323) moved LA5 from exposed to watch. That left 33 of 47 clear, 12 to watch, 2 unchecked and none exposed. On 2026-11-01 Fundamentals added five tells from research on the rules generated interfaces break (CP5, CP6, CM9, BH3, BH4): 34 of 52 are clear, 15 to watch, 2 unchecked and 1 exposed. Nothing was rendered for it: it read `tokens.json`, `bundle.css`, `build/previews.txt`, `components/src` and `site/` at commit `6d40bf2`, with a regex prototype for the `lint` tells and the rest by hand. On 2026-11-02 the previews were labelled (#394), clearing CM9: 35 of 52 are clear and none exposed.
41
+
42
+ **Clear.** No purple, gradient text, coloured stripes, glows, bounce, count-ups, emoji, sparkles or marketese anywhere in the library or previews. The content rules, the forbidden light glows, the rejected severity bars (E8) and Charts' "the data is the record" already rule most of the tells out.
43
+
44
+ **Retired.** Mesa was terracotta on cream (`#b4441b` on `#f6f1ea`), the most-cited replacement for purple (CO4). Alpine was near-black with one mint accent (`#5fd6b2` on `#0f1513`, CO5). Both were removed rather than retuned, on the side of caution ([#324](https://github.com/richlaconte/altitude/issues/324)). Their names render as Daylight and Nightfall. Their figures in earlier Research experiments (E23 among them) are kept as the record of what was measured.
45
+
46
+ **Fixed on the landing page.** The uppercase, letter-spaced kicker above every heading is gone, and its panel labels are in sentence case (TY4). Its accent swatches, which were Tailwind's orange-700, blue-700 and pink-700, are now its own: Harbor `#2a6f9e`, Rosewood `#b13a68` and Olive `#6b7d1e`, each at least 3:1 on both grounds (a graphite swatch was dropped: 1.9:1 on Nightfall).
47
+
48
+ **Measured.** The benchmark's first run ([report](https://github.com/richlaconte/altitude/blob/main/evals/distinctiveness/results/2026-10-31/REPORT.md), E154) found that building with Altitude doesn't yet move Claude's screens measurably away from what plain Claude or shadcn produce: beyond the noise between repeat builds of one prompt, they sit no further from the baselines than the baselines sit from each other, on any of the three distances. They do move toward Altitude's own previews, because agents start from the pattern fixtures. Up close (E155), the builds are no more distinct, and nearer to shadcn's builds than to the previews. On colour, Daylight and shadcn's neutral theme are the closest pair measured (CO8). The structure tells a different story (E160): Altitude builds set 78% of their text on the ground, where shadcn's builds put 92% inside outlined boxes, and the embeddings don't register that difference. The baseline so far is only Claude: v0 and Lovable builds are the next step.
49
+
50
+ **Fixed in the previews: CM9, placeholders as labels.** `Field` gives every input a visible label, but several previews rendered a bare `Input` with only a placeholder, and agents copy previews. They now use `Field`, or an `aria-label` in the compact galleries ([#394](https://github.com/richlaconte/altitude/issues/394)).
51
+
52
+ **Watch:**
53
+
54
+ - **CO3, Nightfall:** periwinkle on dark.
55
+ - **CO8, Daylight:** its blue sits near Tailwind's.
56
+ - **TY2 and IC3:** Instrument Sans and Lucide, kept after review ([#325](https://github.com/richlaconte/altitude/issues/325)). Instrument Sans isn't on the catalogued lists; its serif sibling is, as the display accent Altitude doesn't use. The type rules (labels at 500, titles at 600, never 700, tracking that tightens with size, no all-caps labels) already differ from the generated defaults. Lucide is there because it shares the shipped icons' 24px grid and 1.75 stroke, and the icon tells that matter (CM4 icons in tinted squares, IC2 sparkles) are clear. Changing either would change every app built on Altitude, on practitioner evidence alone. Each quarterly audit checks whether generators have started reaching for either one.
57
+ - **CM6:** props that mirror shadcn.
58
+ - **CM2:** the dialog scrim's blur.
59
+ - **Composition-dependent:** LA4 (equal-weight grids), LA7 (stat rows), BH1 (design theater), BH3 (deceptive patterns), BH4 (controls that do nothing) and CP6 (invented data) depend on how apps compose the parts.
60
+ - **LA5, one frame for every screen:** the app skeleton was the only frame offered, and agents copied it for every screen. [Screen layout](../patterns/screen-layout.md) now chooses the frame from the task (records, list and detail, focused editor, guided flow, decision, report), with the skeleton as the records frame ([#323](https://github.com/richlaconte/altitude/issues/323)). The benchmark's first run put task range (how far a system's frames differ between tasks) in Altitude's favour but within noise, so it stays on watch until runs with more samples per brief.
61
+
62
+ ## How it's checked
63
+
64
+ | When | What | Gates? |
65
+ |---|---|---|
66
+ | Every push | `checks/tells-lint.mjs` (`npm run check:tells-lint`) and `checks/render-tells.mjs` | The lint fails; equal-weight grids and flat spacing warn on the run |
67
+ | Every push | `npm run check:tells`: is the catalog due a review or audit? | Warns |
68
+ | Every pull request that changes how something looks | The review list below | Reviewer |
69
+ | Each release, and quarterly | The [benchmark](https://github.com/richlaconte/altitude/blob/main/evals/distinctiveness/README.md) against a rebuilt baseline | Recorded, not gated |
70
+ | Yearly | The blind study with people (#326) | Recorded in Research |
71
+
72
+ For a pull request that changes how something looks, the reviewer asks:
73
+
74
+ - Does any `exposed` or `watch` tell in `tells.json` get closer, or a new one appear?
75
+ - Does the screen have one focal point and one primary action?
76
+ - Is every shadow a tier, with its word, icon, ARIA state or border?
77
+ - Are the empty, loading and failed states designed, with words that name the object?
78
+ - Does every link, button, field, image and heading name what it is? Can the person decline, close or leave as easily as they accept ([Fundamentals](Fundamentals.md))?
79
+ - Is anything decorative (a gradient, a glow, an edge stripe, an animation) doing a job, or there to look designed?
80
+
81
+ An intentional exception is marked where it is, with its reason: `/* tells-ignore: CM2 the scrim recedes the page behind a dialog */` on the line or the line above. A suppression without a reason doesn't count. In a preview or a page, a rendered warning is suppressed the same way, as an attribute on the element that contains it: `data-tells-ignore="LA4 a gallery of states, to compare"`. Use one only when the pattern does a job (the ColorPicker's hue track is the whole spectrum, purple included), and never to hide a tell that's waiting on a decision without naming its issue (`tells-ignore: CM3 pending a decision in #374`).
82
+
83
+ ## Keeping current
84
+
85
+ The look moves with each model release and each popular anti-slop prompt, so the catalog is refreshed on a schedule. Nothing runs the refresh for us; the repository says when it's due. `tells.json` records when the catalog was last reviewed and last audited, and how often each is due (`refresh`). `npm run check:tells` runs on every push. It warns when either is overdue and lists tells not seen for six months, as annotations on the run and in its summary. It never fails a build for being overdue, only for a malformed catalog; `--strict` makes overdue a failure locally.
86
+
87
+ | When | What | Output |
88
+ |---|---|---|
89
+ | Monthly | Read the practitioner catalogs (avoid-ai-design and others), the avoid-lists in the major frontend design skills (they say where generators will go next), new preprints on generative UI homogeneity, and design press | New tells added to `tells.json` with sources; `lastSeen` updated on the ones still common; new `lint` rules filed against #320 |
90
+ | Quarterly, after a major model release, and before each minor release | Rebuild the [benchmark](https://github.com/richlaconte/altitude/blob/main/evals/distinctiveness/README.md) baseline with current tools and models (the Claude conditions with `generate.mjs`, v0 and Lovable by hand under `baselines/<date>/`), rerun it, and re-audit the schemes, type and skeleton against the current defaults | The benchmark as a numbered experiment in Research; the audit in "Where Altitude stands"; status changes in `tells.json` |
91
+ | Yearly | The study with people | A numbered experiment; new tells from what participants named |
92
+
93
+ When you finish a review or an audit, set `refresh.reviewed` or `refresh.audited` to the date, in the same pull request as its changes. A major model release makes both due at once: set the dates back to say so.
94
+
95
+ Retire a tell that two quarterly reviews in a row no longer find in generated output. A tell that's gone from generators but still in Altitude isn't a problem; one that's common in both is.
96
+
97
+ ## For agents building with Altitude
98
+
99
+ Before composing a screen:
100
+
101
+ 1. Write the task, the one thing that leads, and the primary action (rule 2).
102
+ 2. Choose the frame from the task, not from the app skeleton by default.
103
+ 3. Use the scheme's elevation for emphasis; add no decoration.
104
+ 4. Build every state the pattern lists.
105
+ 5. Write copy that names the object and the outcome.
106
+ 6. Compare the result with what you said you'd build, and with the tells in `tells.json`, before saying it's done.
@@ -187,7 +187,7 @@ The rule an agent can look up is `ALT-VISUAL-003` (`altitude explain ALT-VISUAL-
187
187
 
188
188
  [Actions in place](ActionsInPlace.md) applies these existing signals to everyday work. `InlineEdit` keeps a single value in its location: its well opens under the value's own text, using the existing field and button treatments. Only a dispatched save uses pending, a known failure retains input, and accepted canonical content settles back to the ground.
189
189
 
190
- An item that leaves its list is **left in place, then closed** ([RemovalReceipt](components/RemovalReceipt/README.md)): the result shows in the item's slot, on the ground, at the item's height, so nothing moves while it's read. It's a trace of what happened, not a live surface, so it doesn't rise; only its exit lifts off, as an Alert's does. Its Undo is backed by the application's real inverse operation and a durable recovery destination, and moves to a Toast once the slot has closed. A Dialog can keep its background stationary while retaining the level 5 panel, dimming, isolation and focus ownership. This adds no elevation tier and changes no meaning of the record.
190
+ An item that leaves its list is **left in place, then closed** ([RemovalReceipt](../components/RemovalReceipt/README.md)): the result shows in the item's slot, on the ground, at the item's height, so nothing moves while it's read. It's a trace of what happened, not a live surface, so it doesn't rise; only its exit lifts off, as an Alert's does. Its Undo is backed by the application's real inverse operation and a durable recovery destination, and moves to a Toast once the slot has closed. A Dialog can keep its background stationary while retaining the level 5 panel, dimming, isolation and focus ownership. This adds no elevation tier and changes no meaning of the record.
191
191
 
192
192
  ## On a phone
193
193
 
@@ -0,0 +1,64 @@
1
+ # Mobile
2
+
3
+ A phone is a finger, 320–430 CSS px of width, an on-screen keyboard and a notch. None of that changes what height means. **The ground is the record, at any size.** What changes is where things come from and how big they are to touch.
4
+
5
+ Altitude assumed a mouse in places the guide never said: hover was the first signal, some things were revealed only by hover, moving things meant dragging, and floating panels assumed room to float. A finger lands and presses in one motion, has to pick a thing up before it can carry it, covers what it touches, and the screen's edge is close. These are the laws, restated for the hand.
6
+
7
+ ## The laws for the hand
8
+
9
+ 1. **Press is the first signal.** A finger has no hover, so on touch the press *is* the feedback: the slab goes to ground the moment the finger lands. Hover lift exists only under `@media (hover: hover)`, so nothing stays raised after the finger has gone.
10
+ 2. **Nothing is hover-only.** Whatever a pointer can reveal, a finger can reach: by a tap, a long press, a visible control or focus. Hover may preview; it never gates. A timer that pauses for a hovering reader pauses for a touching one (`Toaster` fans out and pauses on a tap).
11
+ 3. **Faces are sized for eyes, hit areas for fingers.** The visible slab keeps its size and tier. Its hit area grows to 44×44 CSS px under a coarse pointer, and is never under 24×24 anywhere. Where whole controls must grow (fields, rows, menu items) they grow together, by one density step from the `size` tokens.
12
+ 4. **Pick it up before you carry it.** On touch a swipe belongs to the page. A thing is lifted into your hand (level 3, as with a mouse) only after a press-and-hold, then carried. "In your hand" begins when the hand closes on it. A grip may pick up at once, because it is a handle and nothing else.
13
+ 5. **The page scrolls one way.** At 320 CSS px nothing scrolls the page sideways. Content that needs two dimensions (a table, code, a board) scrolls in its own frame. Content that doesn't fit changes form (a pane becomes one at a time, a week becomes a day, a trail collapses, tabs scroll) and never shrinks into ellipses or one letter per line.
14
+ 6. **The screen's edge is part of the ground.** Floating things stay inside the visible viewport: they flip, then shift, then shrink. On a narrow touch screen a menu or select may come up from the bottom edge as a sheet, where the thumb is: same tier, same meaning, different edge. Fixed layers respect the safe area and the keyboard.
15
+ 7. **Fields don't move the page.** Field text is at least 16px on touch, so the browser never zooms on focus, with no `maximum-scale` workaround. Overlays size to what's visible above the keyboard.
16
+ 8. **Hints match the hands.** Keyboard shortcuts are shown where there's a keyboard. Behaviour is never removed: the shortcut still works when its hint is hidden.
17
+ 9. **Height still reads small.** Tiers, shadows and the attention budget don't shrink on a phone.
18
+ 10. **Space steps down; it doesn't stack.** A desktop's insets, repeated at every level of nesting (page, frame, card, message, indent), leave a phone's text a column down the middle. Below the compact breakpoint each inset eases one step toward `space-4`: the page gutter, a dialog and a sheet from 24px, a card from 20px. An indent that aligns detail to a heading gives way to one small step in a narrow slot. Gaps between components stay as they are: grouping is meaning, padding is chrome.
19
+
20
+ ## The constraints
21
+
22
+ Each is testable, and each names what holds it.
23
+
24
+ | # | Constraint | How it's met | Checked by |
25
+ |---|---|---|---|
26
+ | **M1** | No sideways page scroll at 320 CSS px; nothing outside a scroll container reaches past the viewport. | container queries and narrow forms (below) | `checks/phone.mjs` (warns) |
27
+ | **M2** | Targets ≥ 24×24 CSS px everywhere (or WCAG 2.5.8's spacing exception), ≥ 44×44 under `(pointer: coarse)`, by hit area. Inline text links are exempt. | `size-target-min`; `::after` hit areas on small controls; one density step on coarse pointers | `checks/phone.mjs`, `checks/density.mjs` |
28
+ | **M3** | Hover styles only under `@media (hover: hover)`. | every `:hover` rule wrapped | `checks/hover-media.mjs` |
29
+ | **M4** | Nothing reachable only by hover. | tools shown under `(hover: none)`; tap paths for `Toaster` and `Schedule` events | `checks/toaster-touch.mjs`, `checks/touch-drag.mjs`, `checks/schedule-narrow.mjs` |
30
+ | **M5** | Every drag works with touch. Pick-up after a hold (400ms, ≤ 8px of movement, close to `ContextMenu` and `ChartTooltip`); a swipe without a hold scrolls; `touch-action: none` only on a handle or a gesture-only surface, never a row, card or list. | `touchHold` in `core.tsx`, used by `SortableList`, `Board` and `Tree` | `checks/touch-drag.mjs` |
31
+ | **M6** | Field text ≥ 16px under `(pointer: coarse)`. | `max(16px, 1em)` on fields under a coarse pointer | `checks/phone.mjs` (fails CI) |
32
+ | **M7** | Floating panels stay inside the visual viewport with an 8px margin; on narrow touch screens a menu may open as a bottom sheet. | `useFitFloating`; `presentation` on `DropdownMenu`, `Select`, `Combobox` | `checks/float-fit.mjs`, `checks/menu-sheet.mjs` |
33
+ | **M8** | Overlays size to the visible viewport. | `--alt-vvh` and `--alt-vv-top` from `visualViewport`, falling back to `100dvh` | `checks/visible-viewport.mjs` |
34
+ | **M9** | Fixed layers add `env(safe-area-inset-*)`. | `Toaster`, `SaveBar`, `Sheet`, `Dialog` | `checks/safe-area.mjs` |
35
+ | **M10** | Components respond to their container at the shared narrow width, not the viewport. | `breakpoint-narrow` (480px) and `breakpoint-compact` (640px); only these appear in width queries | `checks/density.mjs` |
36
+ | **M11** | Insets step down on a phone: the page gutter, a dialog's and a sheet's inset 16px, a card's 16px, at 375 CSS px; unchanged from 640px up, a narrow card included. What reaches through an inset (a page header, a sheet's header and footer, a folder tab strip) lines up with it. | `--alt-inset-page` and `--alt-inset-box`, which follow the viewport like a density; Trace detail's indent follows its step's width | `checks/narrow-insets.mjs` |
37
+
38
+ ## Narrow forms
39
+
40
+ Under 480px of its own width, each of these changes form rather than squeezing.
41
+
42
+ | Component | Narrow form | Opt out |
43
+ |---|---|---|
44
+ | `Tabs` | the list scrolls within itself, fading the edge with more tabs | n/a |
45
+ | `Breadcrumb` | middle steps collapse behind "…" until the trail fits | `fit="wrap"` |
46
+ | `Stepper` | `orientation="auto"` goes vertical when the row doesn't fit | `orientation="horizontal"` (the default) |
47
+ | `PropertyList` | labels stack over values; proposals stack their parts | n/a |
48
+ | `ItemGroup` | `ItemActions` move under the content | `narrow="inline"` |
49
+ | `ResizablePanelGroup` | one pane at a time (`narrowPanel`), or stacked | `narrow={false}` |
50
+ | `Schedule` | one day at a time, paged by day | `narrowView={false}` |
51
+ | `DataTable`, `DataGrid` | columns keep 6rem (or `minWidth`) and the table scrolls in its frame; `sticky` columns stay | n/a |
52
+ | `InputOTP` | slots give way to a 28px floor | n/a |
53
+
54
+ ## Input modality
55
+
56
+ Read modality from media queries (`hover`, `pointer`, `any-pointer`), never from the user agent. `useKeyboardLikely()` says whether a keyboard is probably in hand (a fine pointer, or a key pressed since load). Approach (see **Motion**) follows a mouse or pen, never touch.
57
+
58
+ ## Still open
59
+
60
+ These were found in the same audit and are not done yet: auto-scrolling a `Board` while a card is carried near its edge; a non-drag "Move to…" path for `Tree`, `Board` and `SortableList` for people with neither keyboard nor fine pointer (WCAG 2.5.7); `Schedule`'s day strip, tap-to-create and press-and-hold to move; `ItemActions` folding into a menu; `DataTable`'s stacked-record form and column `priority`; pixel `minSize` on resizable panes. The phone check still warns on a few pattern previews at 320px.
61
+
62
+ ## Sources
63
+
64
+ WCAG 2.2: 1.4.10 Reflow, 1.4.13 Content on Hover or Focus, 2.5.1 Pointer Gestures, 2.5.7 Dragging Movements, 2.5.8 Target Size (Minimum), and 2.5.5 Target Size (Enhanced) as the coarse-pointer target. Media Queries Level 4 (`hover`, `pointer`) and CSS Containment Level 3 (container queries). The measurements behind each rule are in **Research**, E110–E131.
@@ -0,0 +1,225 @@
1
+ # Motion
2
+
3
+ Motion in Altitude does one job: it shows a surface changing level. Nothing slides in from the side, bounces for fun or spins for attention. If a change doesn't move something up or down the ladder, it's a cross-fade or nothing.
4
+
5
+ ## Duration is distance
6
+
7
+ Every transition takes 100ms plus 40ms for each level it travels. A hover lift (one level) is `dur-travel-1`, 140ms; a menu opening from a level-0 trigger to level 3 is `dur-travel-3`, 220ms; a dialog rising to level 5 is `dur-travel-5`, 300ms. Because the rule is shared, a tooltip and a dialog feel like the same physics at different heights.
8
+
9
+ | Token | Value | Use |
10
+ |---|---|---|
11
+ | `dur-press` | 90ms | Pressing down to ground. Fast, so the press feels immediate. |
12
+ | `dur-travel-1` | 140ms | Hover lift, color and fill changes, checkbox and radio state. |
13
+ | `dur-travel-2` | 180ms | Tab indicator, switch thumb, menu highlight, field messages. |
14
+ | `dur-travel-3` | 220ms | Tooltips, popovers, menus, accordion. |
15
+ | `dur-travel-4` | 260ms | Toasts, sheets. |
16
+ | `dur-travel-5` | 300ms | Dialogs, and the page receding behind them. |
17
+ | `dur-sweep` | 1600ms | Light passing over a skeleton imprint, and the indeterminate progress loop, per pass. |
18
+ | `dur-breath` | 4800ms | One breath of a waiting imprint under `loading="breath"` (experimental): deepening over 40%, releasing over the rest. |
19
+ | `dur-wave` | 1800ms | How long the breath's swell takes to cross the view from top to bottom. |
20
+
21
+ Signal timings, each set from research or measurement (see **Research**):
22
+
23
+ | Token | Value | Use |
24
+ |---|---|---|
25
+ | `dur-grace` | 300ms | A write that confirms within this never shows as pending. Under about 0.1s feels instant and under 1s keeps flow, so fast saves stay invisible and only slow ones hover. |
26
+ | `dur-onset` | 500ms | Non-predicted menu items fade in over this; predicted ones are there at once. 500ms helped in Findlater et al.'s studies, 250ms didn't. |
27
+ | `dur-hold` | 1000ms | How long a HoldButton must be held: long enough to be deliberate, short enough not to feel like a punishment. |
28
+ | `dur-hover` | 1800ms | One slow breath of a pending surface. Slower than any transition, so it reads as waiting, not as motion to follow. |
29
+ | `dur-linger` | 2000ms | How long a removed item's receipt holds in its slot before the gap closes: about a second to read four words, and a second to act. A judgement, not yet measured; see **Research**. |
30
+ | `dur-settle` | 8000ms | Afterglow: full strength for the first second, noticeable for about four, gone by eight (measured). |
31
+ | `dur-arrive` | 1100ms | Under `loading="breath"`, raised content that lands rises a level past its rest and settles back over this long. A judgement from prototype review, not yet measured. |
32
+ | `dur-plate` | 600ms | Under `loading="breath"`, the plate under content that arrives into a view already showing. Short and untinted, so it isn't read as an afterglow. |
33
+
34
+ Exits run at 70% of their entry: attention has already moved on.
35
+
36
+ ## Curves
37
+
38
+ - `ease-rise` for anything entering or gaining height: it decelerates into place, like something lifted and set down.
39
+ - `ease-sink` for anything leaving or losing height: it accelerates away.
40
+ - `ease-settle` for things that snap into a slot (switch thumb, tab indicator, menu highlight, a button released after a press). It overshoots by 2–3%, felt more than seen.
41
+ - `ease-glide` for changes of size or position at one level: accordion height, progress width.
42
+ - `ease-decay` for the afterglow fading, after its first second at full strength.
43
+
44
+ ## Choreography
45
+
46
+ - **Rise from where it's anchored.** Popovers and menus grow from their trigger's edge (`transform-origin` on that side), tooltips from the side facing their anchor.
47
+ - **Shadow travels with the surface.** On enter, a floating surface starts at `elev-1` and grows to its level while it moves; on exit it shrinks back. The shadow is part of the motion, not a separate fade.
48
+ - **Small distances.** Surfaces travel 4–16px and scale from 0.96, never more. Larger moves read as navigation, not elevation.
49
+ - **Stagger only lists that arrive together**: 16ms per menu item, capped by the list's length.
50
+ - **One shared indicator.** Segmented tabs and menus have one raised pill or highlight that glides to the new choice, instead of each option fading in and out.
51
+ - **Press is fast, release settles.** Down in `dur-press` with a plain ease-out; back up on `ease-settle`.
52
+ - **Rises are an attention budget.** An abrupt onset captures attention whether the person wants it or not, and people track only about four moving things at once. Altitude lets at most four afterglows begin in any 250ms window and staggers the rest by 90ms; apply the same budget to anything you make rise. Things that merely change state cross-fade instead of rising.
53
+ - **Animate only what is congruent with the change.** Animation helps when its motion *is* the change (a surface gaining height, a view descending into a child) and adds nothing when it's decoration. If a transition doesn't move something along the ladder or through the hierarchy, it fades.
54
+ - **Stacks, not slides.** `Drilldown` lays the child on top of the parent and presses the parent beneath it; going back lifts the top sheet off. The new breadcrumb step drops onto the staircase. Animated transitions like this measurably help people rebuild where things are.
55
+ - **Carried, not teleported.** When there's one of something (the radio thumb, the switch thumb, the segmented pill, the menu highlight), it travels to its new place on `ease-settle` so the eye can follow where the choice went; the radio's accent pours in as the thumb arrives.
56
+ - **Stopped, not bounced.** A range `Slider`'s thumb pushed against the other stops where the gap begins, with no overshoot, because the other thumb is a limit, not a spring. Only the thumb in hand floats to level 3; dragging the fill carries both, at the pointer's pace with no easing.
57
+ - **Brought to the top.** Folder tabs don't slide: the chosen tab's sheet comes up from behind (its tab rises 10px into place over `dur-travel-3`, the panel's shadow swells from `elev-1`) while the old tab drops back on `ease-sink`.
58
+ - **Filled, not swapped.** When an empty field gets its first character, its floor rises flush on `ease-settle` and the text rises with it; an invalid field drops to trench depth on `ease-sink` instead of shaking.
59
+ - **Filled by choosing.** A Select's floor rises flush when a value is chosen, as the list sinks back and the value settles in with a brief afterglow (a fifth of `dur-settle`). A Combobox's floor waits for the choice: typing doesn't fill it.
60
+ - **Opened in place.** An `InlineEdit` value sits in a slot with its control's inset and font, so opening it moves nothing: the well's floor, walls and border fade in around text that stays put (`dur-travel-2`, `ease-sink`). Whatever does change size glides on the real box, so what's below is pushed, not jumped: `dur-travel-3` up to 120px of travel, a little longer per pixel beyond (capped at `dur-travel-3` + 140ms), on `ease-glide` (`ease-sink`, in 70% of the time, when closing). Notes that arrive under it (an error, a key hint) fade in as the moving edge reaches them, and the input grows sideways from its text. With `trigger="field"` the field is drawn on hover, so it doesn't fade in again on click. A multiline textarea then grows with what's typed without animating each line: typing is not a change of level.
61
+ - **Chosen, then settled.** An `InlineEdit` with `commit="on-choose"` saves as soon as its Select or Combobox reports a value. The list sinks and the field's floor rises flush first; only when that motion has finished does the editor settle back to the ground with its afterglow. The two never move at once, as in the Stepper.
62
+ - **Lifted to edit, settled when saved.** An `EditRegion` rises one level as it opens (`dur-travel-1`, `ease-rise`), but its padding and border were already there under a negative margin, so the lift is colour and shadow, never a move. Its height glides as InlineEdit's does, and the actions arrive with the edge. In a form layout the fields fade in together, never staggered. In place (`layout="in-place"`) nothing slides: each field holds its position in the editor on every frame while the box grows, a field whose text opens where its value was simply appears, and one that opens elsewhere fades in there. An accepted save settles the region with one afterglow, however many fields changed, so it spends one rise. An editor's afterglow fades over 5 seconds rather than `dur-settle`'s 8: the person just made the change and is looking at it, so it only needs confirming, not finding. `settleDuration` sets it on `InlineEdit` and `EditRegion`. Under reduced motion levels and tints still change; the height changes at once.
63
+ - **Stamped in, lifted off.** A tag added to a `TagInput` is stamped into the field's floor (down 3px, `ease-settle`); several from one paste are stamped together, as one change. The first Backspace arms the last tag (it lifts one level on a critical tint, `dur-travel-1`); the second lifts it off as an alert is dismissed (up 8px, fading), then the gap closes. Reduced motion keeps the tint and depth and drops the travel.
64
+ - **Carried across the month.** In a `Calendar` the chosen day's raised mark is carried to the newly chosen day (`ease-settle`, `dur-travel-2`), as the radio thumb is; the panel then sinks back and the field's floor rises flush (**Filled by choosing**). Changing month is lateral, so the grid cross-fades (`dur-travel-1`) instead of sliding. Under reduced motion the mark appears in place.
65
+ - **Carried through the hierarchy.** A `Tree` node carried by keyboard is shown where it will land at each step; carried by pointer, a floating copy follows the pointer with no easing while slots open at the target's depth. Resting over a closed node for 600ms opens it (a spring-loaded folder). Expanding turns the chevron (`ease-settle`) and reveals the children without a rise: they're existing records. Renaming sinks the label into a well (`dur-travel-1`, `ease-sink`); a saved or added node settles with an afterglow.
66
+ - **Written, then pressed in.** In a `Checklist`, Enter opens the next line as a well under the current one (`ease-sink`, `dur-travel-1`). Checking an item pours the checkbox, then draws the strike left to right as the text mutes (`dur-travel-2`). Indenting is a change of hierarchy, the one place a line moves sideways; it takes its new indent at once, without travel. A removed line glides shut (`dur-travel-2`, `ease-glide`) with no hold and no lift-off: the caret has already moved up, and the line was empty or its text has visibly joined the line above. Under reduced motion the state changes remain and nothing travels.
67
+ - **Chosen, then asked.** Adding a `FilterBar` condition: the field list sinks, and only then does the new condition's value picker rise (the Stepper's press-then-rise ordering). A changed condition is re-stamped, as a Badge is, and the results change their relevance in place within the rise budget.
68
+ - **Drawn, then risen.** In a `Schedule`, dragging across empty time draws a well that follows the pointer with no easing; releasing raises it into an event (`dur-travel-2`, `ease-rise`) and focuses its title. Carrying an event is Board's: the days open into wells, the event follows the pointer, and the slot under it deepens. A new week cross-fades. Under reduced motion levels and tints still change and nothing travels.
69
+ - **Opened in the cell, settled in the row.** A `DataGrid` cell entering edit drops its floor to a well (`dur-travel-1`, `ease-sink`) with its text in place; a committed value rises flush and settles, and a pasted block settles as one region rather than one rise per cell.
70
+ - **Lifted from the list.** In an `ItemGroup`, Edit lifts one slat into its editor while the neighbours are pushed down by the height change, not animated on their own. The add slat rises out of its well into an empty editor; once added, the new slat settles with an afterglow and a fresh add slat is ready below. A removed slat cross-fades to its receipt in the same slot and holds; then the receipt lifts off and the gap glides shut (**Left in place, then closed**). Undo during the hold cross-fades the slat back with an afterglow, and nothing moves. Undo from the toast after the close glides the slot open again before the slat settles. A confirmed removal has no receipt: the slat glides shut.
71
+ - **Added at the foot.** A Board column's add slot rises out of its well into a new card (`dur-travel-2`, `ease-rise`); once the card is created the field stays, empty, for the next one. Renaming sinks the card's title into a well; saving settles it.
72
+ - **Opened under the row.** A DataTable row's detail glides open beneath it with no rise (it's the record, revealed). Editing adds the one-level rise to the row and its region together; Cancel or Save settles them back, and a saved row whose sort position changed is then carried to its new place (FLIP), after it has landed, never during the edit.
73
+ - **Chosen, then settled.** An `InlineEdit` with `commit="on-choose"` saves as soon as its Select or Combobox reports a value. The list sinks and the field's floor rises flush first; only when that motion has finished does the editor settle back to the ground with its afterglow. The two never move at once, as in the Stepper.
74
+ - **Changed where it sits.** A `PropertyList` value rises into its InlineEdit editor (`dur-travel-1`); with `commit="on-choose"` the picker's list rises from it with no editor row in between. An unset value's well fills flush when it's set (**Filled, not swapped**), then settles. Edit all lifts the list into one raised EditRegion.
75
+ - **Imprinted, then risen.** A component's own imprint (shown while its content is undefined or a promise) waits `dur-grace` before it fades in, so a quick load shows nothing and lands without moving. A slower one rises 8px out of its imprint (`dur-travel-3`, `ease-rise`) as the imprint fills in behind it, within the attention budget; containers leave no imprint, their content just rises. In a `RevealGroup`, parts that land early stay imprints until the slowest arrives, then rise together.
76
+ - **Breathed, then risen** (experimental, `AltitudeProvider loading="breath"`). No light sweeps the imprint; it breathes. Its floor deepens from `well` to `trench` over 40% of `dur-breath` and releases over the rest, slower and less even than a pending write's breath, so the two can't be confused. In Nightfall, where well and trench are nearly one tone, its back wall (`lip-top`) breathes instead. Every imprint takes its phase from one page clock, lagged by its height in the view, so imprints change in step (one moving thing for the attention budget) and a swell crosses the view in `dur-wave`. Ten seconds in, the breath calms to a shallow one; words stay with the app. When content lands, raised content (`Await settle="raised"`, a card) rises 8px to level 2 and settles to its rest over `dur-arrive`; flush content fills past the ground (its floor passes just above it in twice `dur-travel-3`, then fills in flush, carrying the text 1.5px past its line); content that arrives into a view already showing gets an untinted plate that dissolves over `dur-plate`. A card's parts land together, and a group takes one place in the budget, its parts 16ms apart. Chart, stat and meter imprints hold still, and a chart's marks still fade in place. Under reduced motion the breath stops and landings lose their travel.
77
+ - **Pressed into the record.** Advancing a Stepper presses the current step to the ground (`ease-sink`) and stamps its check in, then, about 150ms later, raises the next step out of its imprint (`dur-travel-3`, `ease-rise`, within the attention budget). The two never move at once, so the eye follows the press and then the rise.
78
+ - **Sunk in, never begging.** An `EmptyState` appears by sinking from flush to its well (`dur-travel-1`, `ease-sink`): the inverse of content arriving lifted. It never pulses or loops. Under reduced motion it still changes depth, as every surface does, but nothing travels.
79
+ - **Lifted off.** Dismissing an alert lifts the sheet 8px as its shadow spreads, fades it, and only then closes the gap. In an `AlertStack` the next alert comes up from under it.
80
+ - **Left in place, then closed.** An item leaving a list (`RemovalReceipt`) cross-fades to its result in its own slot (`dur-travel-2`), at its own height, so nothing moves. The receipt holds for `dur-linger`, paused while it's pointed at or focused, and waiting (up to `dur-settle`) while a mouse is still over the list, so the next row doesn't slide under a pointer reaching for it. Then it lifts off as an alert does (8px, in 70% of `dur-travel-3`, `ease-sink`), and only then does the gap glide shut (`dur-travel-3`, `ease-glide`); the rows below are pushed by the height, not animated on their own. Receipts due within 250ms close together, as one change. An item brought back from elsewhere glides its slot open, then settles. Under reduced motion the receipt still fades and holds, and the gap closes at once.
81
+ - **Stamped and relit.** A badge whose label changes is re-stamped onto the page (down 3px, settling); one whose status changes relights its lamp (the flat dot fills in from an empty ring, `dur-travel-3`). A count pops up when its number changes.
82
+ - **Gap, not glow.** An avatar's height is animated through one registered property, so its face and its shadow move as one: as the face leaves the page its shadow appears beneath it, then shrinks and fades the higher it goes; someone live drifts over `dur-hover`, and a pressable avatar lifts on hover.
83
+ - **Fanned out, not opened.** Opening a `Pile` slides its items out from under the top card, 22ms apart, floating over the page below; closing slides them back under.
84
+ - **Risen at the edge.** A sheet appears where it's anchored: it travels 16px out of its edge as it fades in and its shadow grows to `elev-4` (`dur-travel-4`). It never crosses the screen. Closing lifts it off: up 8px, 101%, shadow spreading, in 70% of the time.
85
+ - **Carried and landed.** A card picked up off a board floats at level 3, tilted 1.2°, and follows the pointer with no easing at all (the hand leads); everything else moves on `ease-settle`. The slot it will land in opens as it goes; let go and it's carried into the slot with a slight overshoot (`ease-settle`, `dur-travel-3`) while its shadow sinks back. The columns open (`ease-sink`) when carrying starts and close (`ease-rise`) when it ends.
86
+ - **Hover intent.** A HoverCard waits 400ms before rising and 250ms before going; neighbours open at once while warm (Tooltip's warm-up, shared), and one hands over to the next.
87
+ - **Rows are carried.** When a table's order changes (a sort, a move), each row travels from where it was to where it now belongs (FLIP, `dur-travel-3`, `ease-glide`), all at once and at one level: a re-sort is one change, not many arrivals, so it doesn't spend the rise budget. Reduced motion re-sorts in place.
88
+ - **Results are carried.** When a list changes under a query (the command palette), rows that stay travel to their new places (FLIP: measured before and after, then animated from the old place, `dur-travel-2`, `ease-glide`) and the list's height glides to fit. New rows near the top rise 4px within the attention budget; the rest cross-fade. Under reduced motion they all cross-fade.
89
+ - **Charts glide, they don't grow.** A chart's data changes at one level, staged, by one engine every chart shares: see **Charts** below.
90
+ - **Repeat the warm state.** After one tooltip has shown, its neighbours open instantly for 400ms. Moving along a toolbar shouldn't make you wait at every button.
91
+
92
+ - **A menu that's a sheet rises from the edge.** On a narrow touch screen a menu or list opens at the bottom edge (#249): it rises 24px from the edge over `dur-travel-3` on `ease-rise`, as a Sheet does, and sinks back on close. Under reduced motion it fades.
93
+
94
+ ## Charts
95
+
96
+ Every chart moves by one engine, built into `ChartContainer`, so no chart type has its own easing. Marks have no level, so none of this is height: it's position, length and presence changing on the ground.
97
+
98
+ 1. **A data change happens at one level.** Marks glide to their new values on `ease-glide` over `dur-travel-3`, all at once. It's one change, not many arrivals, so it doesn't spend the attention budget.
99
+ 2. **A rescale comes first.** If y's domain changes, the axis and gridlines glide to it first (`dur-travel-2`), with the marks keeping their old values so they move only with the scale; then the marks glide to their new values on a still axis (`dur-travel-3`). The two never happen at once, as a `Stepper` presses before it rises. Ticks that leave fade out as the new ones fade in.
100
+ 3. **Re-sorted bars are carried** to their new places, as table rows are. A stack's segments glide together, so the parts stay stacked while the total changes.
101
+ 4. **No decorative entrances.** On first render the plot fades in place (`dur-travel-1`). Data that lands out of a loading imprint fades in place too: it never rises out of it as `Await`'s content does, because a rising bar would leave the ground line. A refetch never imprints; after `dur-grace` the marks drop to half strength until the new data glides in. A bar grows from the ground line only when its value really goes from zero to something.
102
+ 5. **Lines morph only when x is the same.** If the x values change under a line (a new range, a new interval), the old plot and the new one cross-fade instead. A line is never drawn on, and its end label moves with its end. Areas follow the same rule, and a stack's bands keep their config order, so a band never jumps to a new position when values change.
103
+ 6. **Records and series that enter or leave cross-fade.** Exits take 70% of the entry's time, so what's leaving is gone before what's arriving has settled. A series toggled from the legend leaves and returns the same way, and its key's swatch drains or fills over `dur-travel-1`, like a Checkbox.
104
+ 7. **An interruption retargets** from where the marks are, so nothing jumps back to the old destination first.
105
+ 8. **Chart and table cross-fade.** Switching a chart's view fades one out as the other fades in, in the same box, over `dur-travel-2`; it never slides, because they're two views of one record, not two places.
106
+ 9. **Focus moves at once.** The keyboard's ring jumps to the next point with no transition, because it's focus; the readout opens on it as it does under the pointer, and while a series is focused the others recede over `dur-travel-1`.
107
+ 10. **The readout steps, it doesn't drift.** It rises like a `Tooltip` the first time (4px, `elev-1` to `elev-3`, `dur-travel-3`) and shares its 400ms warm-up; after that it steps between x positions with no easing and the crosshair snaps with it. It follows the data, not the pointer's every pixel, so it never reads as something drifting. It leaves in 70% of its entry.
108
+ 11. **Past 500 marks** changing at once, the plot cross-fades instead of gliding.
109
+ 12. **A period that completes is pressed into the record.** When a bar's `status` goes (this month closes), its hatching cross-fades to a solid fill over `dur-travel-3`, in place, as a `Stepper`'s done step settles. It never rises or grows. A line's or an area's boundary moves with the data, as any change does.
110
+ 13. **References move with the scale.** A reference line or band is drawn in the plot's coordinates, so it glides with a rescale, and a mean or median glides to its new value with the marks. Annotation plates move with their x; they never rise or drop in.
111
+ 14. **Live windows glide; changes glow.** When a live chart's window moves on, x glides one step over `dur-travel-2` on `ease-glide`, after any rescale, and the marks ride along: the motion is time passing. A new or changed mark's afterglow is an `ink` outline, full for a second and decaying over `dur-settle` on `ease-decay`, within the attention budget; more than four at once is one change, and the "Updated" caption glows instead. While the plot is being read, nothing moves: updates wait. Under reduced motion the window updates in place, and the afterglow stays, because it's tint, not travel.
112
+ 15. **Filtering cross-fades.** A mark that stops or starts matching recedes to 35% or comes back over `dur-travel-1`, in place. Positions hold, so nothing travels, and it spends none of the attention budget. With `rescale`, the axis fits the matches as any rescale does: first, then the marks.
113
+ 16. **Hover outlines, selections cross-fade.** Where marks act, the outline on the record under the pointer appears over `dur-travel-1` and steps with the readout. Selecting cross-fades the rest to 35%, as a filter does. A drill follows `Drilldown`: the child is laid on as a sheet, and the breadcrumb gains a step.
114
+ 17. **Cells cross-fade in place.** A heatmap's cell whose value changes step cross-fades its fill over `dur-travel-1`; its value never glides through the steps between. A whole refresh is one change: in a live chart it lights the "Updated" caption, not every cell. Under reduced motion the fill changes at once.
115
+ 18. **Dots glide by identity.** A scatter's dots glide to their new places on both axes when records keep a stable id, after any rescale, as bars are carried. Without ids there's nothing to follow, so the plot cross-fades.
116
+ 19. **A brush moves the window, the plot cross-fades.** A `ChartBrush`'s lens and grips follow the pointer with no easing and step by a record from the keyboard, as a range Slider's thumbs do; the held part floats to level 3. The chart takes each new window as new x values, so its plot cross-fades to it (rule 5) rather than sliding, and its axis spans the window exactly.
117
+ 20. **A pinned reading lands.** Pinning (#190) moves the readout's surface from level 3 to the ground as a plate: its shadow falls from `elev-3` and it drops 6px over `dur-travel-3` on `ease-settle`, three levels, then it takes the afterglow once, decaying over `dur-settle`. Under reduced motion it appears in place and the tint stays.
118
+ 21. **Reduced motion:** values change in place, as a cross-fade between the old chart and the new one, and nothing travels. Resizing never animates, with or without it.
119
+
120
+ **Word-sized charts don't move.** A `Sparkline` has no motion of its own: it lands out of its imprint like any inline `Await` content, and when its value changes in a live table the row gets the afterglow, not the line.
121
+
122
+ **Numbers change, they don't count.** A `Stat`'s changed value cross-fades over `dur-travel-2` (the old figure fading out over the new) and the stat takes the afterglow within the attention budget. Counting up is never offered: it delays the number for decoration. A stat whose value loads lands out of its imprint like `Await` content; in a `StatGroup` they land together.
123
+
124
+ **A number in a line changes the same way.** A `FormattedNumber` cross-fades over `dur-travel-2` and takes a ground-level tint that decays over `dur-settle` on `ease-decay`, the afterglow without its lift, because a word in a line can't rise. Under reduced motion the figure changes in place and the tint remains.
125
+
126
+ **Time passing isn't a change.** A `RelativeTime` ticking from "3 minutes ago" to "4 minutes ago" swaps its words with no transition. Only a new moment (the record edited again) takes the ground-level tint over `dur-settle`.
127
+
128
+ **A reading glides.** A `Meter`'s fill glides to a new reading on `ease-glide` over `dur-travel-3`, as `Progress` does, but at no level: it's ink moving across a flush track. Crossing a threshold cross-fades the fill's colour and relights the state's lamp. Under reduced motion the fill changes in place.
129
+
130
+ **A re-rank is carried.** When a `BarList`'s values change, its rows are carried to their new ranks (FLIP, `dur-travel-3`, `ease-glide`) all at once, as table rows are on a re-sort, and the bars glide to their new lengths. Under reduced motion both change in place.
131
+
132
+ `checks/chart-motion.mjs` samples every frame of these changes and fails if the axis and the marks move at once.
133
+
134
+ ## The frame
135
+
136
+ **The current destination's pill glides.** In a `Sidebar`, the raised pill under the current destination glides to the new one over `dur-travel-3` on `ease-glide`, as segmented `Tabs`' pill does. Collapsing to a rail changes the sidebar's width over `dur-travel-3` on `ease-glide`, with no change of level. The top bar rises over `dur-travel-1` while content passes under it. Under reduced motion the pill appears in place and the width changes at once. Nothing rises on a route change, except the new content's own afterglow.
137
+
138
+ ## Drafts
139
+
140
+ **A draft rises from the edge.** A `SaveBar` rises from the bottom edge to level 3 over `dur-travel-3` on `ease-rise`, 600ms after a change settles into the draft. Saved, it lowers away on `ease-settle` as the saved fields begin their afterglow. A failed save drops it to a trench over `dur-travel-5` on `ease-sink`. Under reduced motion it fades in and out, and its levels still change.
141
+
142
+ ## Panes
143
+
144
+ **A grip is picked up.** A `ResizableHandle`'s grip lifts to level 1 over `dur-travel-1` when pointed at, rises to level 3 over `dur-travel-2` on `ease-rise` while held, and settles on `ease-settle` when let go. Collapsing a pane changes its size over `dur-travel-3` on `ease-glide`. Under reduced motion collapsing is instant; the grip's levels still change.
145
+
146
+ ## Pages
147
+
148
+ **The page thumb travels.** A `Pagination`'s thumb glides to the new page over `dur-travel-1` on `ease-glide`, and is held pressed (flat, no sheen) while the next page loads. Under reduced motion it appears in place.
149
+
150
+ ## Colours
151
+
152
+ **The mark travels between swatches.** A `ColorPicker`'s white mark glides to the chosen swatch over `dur-travel-3` on `ease-glide`, as `RadioGroup`'s thumb does. Under reduced motion it appears in place.
153
+
154
+ ## Latching
155
+
156
+ **A toggle sinks when it turns on.** A `Toggle` turning on sinks into its well over `dur-travel-2` on `ease-sink` while the accent pours in. Turning off rises on `ease-rise`. A single-choice `ToggleGroup`'s pill glides between options on `ease-glide` over `dur-travel-3`, as segmented `Tabs`' does. Under reduced motion the pill appears in place and the fill still changes.
157
+
158
+ ## Confirming a press
159
+
160
+ **A copy stays pressed for a moment.** A `CopyButton` presses to the ground on `dur-press` like any button, then stays there for `dur-linger` while its icon and words cross-fade (`dur-travel-1`) to a check and "Copied". Then it springs back on `ease-settle`. Nothing rises, so reduced motion changes only the cross-fade's travel, which is none.
161
+
162
+ ## Links
163
+
164
+ **A link doesn't move.** Hovering a `Link` thickens its underline from 1px to 2px over `dur-travel-1` on `ease-rise`. Nothing travels, so reduced motion changes nothing.
165
+
166
+ ## Codes
167
+
168
+ **A code fills slot by slot.** Each `InputOTP` slot's floor rises flush as its character lands, on the `Input`'s own transition. A pasted or autofilled code fills left to right, 35ms a slot, so six slots take about 200ms, within the attention budget. A rejection drops the row to a trench on `ease-sink`. Under reduced motion floors change in place and the caret doesn't blink.
169
+
170
+ ## Approach
171
+
172
+ Openings respond to the pointer *before* it arrives; nothing else does. One shared tracker (started by any component that uses it) follows a mouse or pen, never touch, and writes `--near` (0 at the edge of the element's radius, 1 when touching, smoothstepped) and, while hovered, `--mx`/`--my` (where the pointer is on the element, −1…1).
173
+
174
+ | Component | Radius | Response |
175
+ |---|---|---|
176
+ | `Input`, `Textarea`, `Select`, `Combobox` | 56px | an empty field's inside wall deepens by up to 2px, and the placeholder sinks with it; once focused, it holds that depth until something is put in it |
177
+ | `Sheet` (bottom, dismissible) | 48px | the finger pull deepens by up to 1px |
178
+
179
+ Raised things never use it. A button, card, crumb, step, pile or board card stays exactly where it is until the pointer is on it, and then changes once (its hover state); it doesn't climb toward an approaching pointer or lean after it across its face. An earlier version did both, and in use it read as the control drifting on its own, as if something were wrong. An opening deepening is different: the field itself doesn't move, only its depth does, and that previews what focusing it will do.
180
+
181
+ Approach only adds depth, never size or position, so nothing reflows. Values update once per frame and every response rides the element's existing transition. Mark your own element with `data-approach` (and call `useApproach()`) to use the same variables, and keep to the same rule: deepen openings, never move what's raised. `AltitudeProvider approach={false}` turns it off for a subtree.
182
+
183
+ ## Holding to pick up
184
+
185
+ On touch a thing is picked up by a press-and-hold: 400ms without moving more than 8px. When the hold completes it lifts to level 3 on its usual `dur-travel-2` and `ease-rise`, the same lift a mouse pick-up gets, so the person knows they can move it; under reduced motion it lifts without travel. Moving before the hold completes gives the gesture to the page, which scrolls. See **Mobile**.
186
+
187
+ ## The queue
188
+
189
+ A queued message appears in place, with no rise: it's the person's own act, and nothing has arrived in the record. When it's sent it leaves the queue and joins the log as a person's message.
190
+
191
+ ## Checkpoints
192
+
193
+ Restoring a checkpoint is a write: its label hovers as pending after `dur-grace`, then lands with one afterglow, or drops into a trench if it fails. The turns set aside leave without travel; the app removes them (and brings them back on "Return to where you were").
194
+
195
+ ## Suggestions
196
+
197
+ Follow-ups under an answer cross-fade in over `dur-onset` (opacity only); they don't rise, because they only became available. Starters in an empty conversation are simply there. Under reduced motion follow-ups appear at once.
198
+
199
+ ## Thinking
200
+
201
+ Reasoning that's still arriving has no motion of its own: no shimmer, no pulsing dots, no counter ticking up. When it completes, "Thinking…" cross-fades to "Thought for 12s" over `dur-travel-1`; under reduced motion it swaps at once.
202
+
203
+ ## Commands
204
+
205
+ `Terminal` output appends without animation. Finishing settles the frame with one afterglow, as a completed answer does. "Jump to latest output" emerges at level 3 when you scroll up while it runs, as `ConversationScrollButton` does. Under reduced motion nothing travels.
206
+
207
+ ## Questions
208
+
209
+ A question arrives lifted and settles with the afterglow, like any new content that needs notice. Answering presses it to the ground: its shadow and surface fall from 1 to 0 over `dur-travel-2` as it collapses to its one-line record.
210
+
211
+ ## Reviewing changes
212
+
213
+ Deciding a change lowers it from 1 to 0 over `dur-travel-1` with `ease-sink`; an accepted one settles with one afterglow, a rejected one collapses to a line after `dur-linger`. Under reduced motion it changes in place and a rejected change collapses at once.
214
+
215
+ ## Artifacts
216
+
217
+ The pane appears at its size (no slide): panes lie on the ground. Completing a version settles it with one afterglow. Switching versions or views cross-fades the content over `dur-travel-1`; nothing travels. Under reduced motion everything changes in place.
218
+
219
+ ## Answers in Markdown
220
+
221
+ `Markdown` has no motion of its own: text arrives without per-word animation (Conversation's rule), and a table row that completes appears in place rather than rising. An image rises out of its imprint over `dur-travel-1` with `ease-rise` (2px of travel and its opacity); under reduced motion it appears in place.
222
+
223
+ ## Reduced motion
224
+
225
+ `--alt-travel` multiplies every translate and scale distance. It is 1 by default and 0 under `prefers-reduced-motion` or `AltitudeProvider motion="reduced"`. At 0, surfaces still change level, tint and shadow, and still fade, so state is never lost; they just don't travel. Every approach effect is multiplied by it too, so it disappears (a focused field doesn't deepen either); an avatar's shadow still shrinks with its height even though the face no longer moves. The skeleton sweep and indeterminate travel stop, and a pending surface holds at level 2 instead of breathing.