transitions-refine 0.1.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 (32) hide show
  1. package/.agents/skills/refine-live/SKILL.md +205 -0
  2. package/.agents/skills/transitions-dev/01-card-resize.md +53 -0
  3. package/.agents/skills/transitions-dev/02-number-pop-in.md +119 -0
  4. package/.agents/skills/transitions-dev/03-notification-badge.md +110 -0
  5. package/.agents/skills/transitions-dev/04-text-states-swap.md +97 -0
  6. package/.agents/skills/transitions-dev/05-menu-dropdown.md +105 -0
  7. package/.agents/skills/transitions-dev/06-modal.md +94 -0
  8. package/.agents/skills/transitions-dev/07-panel-reveal.md +81 -0
  9. package/.agents/skills/transitions-dev/08-page-side-by-side.md +100 -0
  10. package/.agents/skills/transitions-dev/09-icon-swap.md +78 -0
  11. package/.agents/skills/transitions-dev/10-success-check.md +169 -0
  12. package/.agents/skills/transitions-dev/11-avatar-group-hover.md +200 -0
  13. package/.agents/skills/transitions-dev/12-error-state-shake.md +202 -0
  14. package/.agents/skills/transitions-dev/13-input-clear-dissolve.md +276 -0
  15. package/.agents/skills/transitions-dev/14-skeleton-reveal.md +149 -0
  16. package/.agents/skills/transitions-dev/15-shimmer-text.md +95 -0
  17. package/.agents/skills/transitions-dev/16-tabs-sliding.md +146 -0
  18. package/.agents/skills/transitions-dev/17-tooltip.md +103 -0
  19. package/.agents/skills/transitions-dev/18-texts-reveal.md +110 -0
  20. package/.agents/skills/transitions-dev/19-card-tilt.md +170 -0
  21. package/.agents/skills/transitions-dev/20-plus-menu-morph.md +167 -0
  22. package/.agents/skills/transitions-dev/21-accordion.md +124 -0
  23. package/.agents/skills/transitions-dev/SKILL.md +225 -0
  24. package/.agents/skills/transitions-dev/_root.css +204 -0
  25. package/README.md +89 -0
  26. package/bin/cli.mjs +264 -0
  27. package/demo.html +2531 -0
  28. package/package.json +37 -0
  29. package/server/inject.mjs +116 -0
  30. package/server/motion-tokens.mjs +106 -0
  31. package/server/refine-agent.mjs +86 -0
  32. package/server/relay.mjs +421 -0
@@ -0,0 +1,149 @@
1
+ # Skeleton loader and reveal
2
+
3
+ ## When to use
4
+
5
+ A placeholder that loads then reveals real content — list rows, cards, profile headers. The skeleton pulses, then both layers cross-fade with a matching cross-blur. Bring your own bars / avatar / text; the skeleton stays in the same slot as the content so the swap is layout-free.
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <div class="t-skel" data-state="loading">
11
+ <div class="t-skel-skeleton is-pulsing">…</div>
12
+ <div class="t-skel-content">…</div>
13
+ </div>
14
+ ```
15
+
16
+ State:
17
+ - Mount with `.is-pulsing` on the skeleton so it pulses
18
+ --pulse-count times.
19
+ - When data arrives, add `.is-revealed` to .t-skel — the
20
+ skeleton fades out + blurs and the content fades in +
21
+ un-blurs over --reveal-dur.
22
+ - To replay the loading state without animating the
23
+ reverse: add `.is-resetting` to .t-skel, remove
24
+ `.is-revealed`, force a reflow, then drop `.is-resetting`.
25
+
26
+ Bring your own avatar / text / wrapping. The skeleton stays
27
+ in the same flex slot as the content so the swap is
28
+ layout-free.
29
+
30
+ ## Tunable variables
31
+
32
+ | Variable | Default | Notes |
33
+ | --- | --- | --- |
34
+ | `--pulse-dur` | `1000ms` | sourced from `--p14-pulse-dur` |
35
+ | `--pulse-count` | `1` | sourced from `--p14-pulse-count` |
36
+ | `--pulse-min` | `0.5` | sourced from `--p14-pulse-min` |
37
+ | `--reveal-dur` | `400ms` | sourced from `--p14-reveal-dur` |
38
+ | `--reveal-blur` | `2px` | sourced from `--p14-reveal-blur` |
39
+ | `--reveal-ease` | `ease-in-out` | sourced from `--p14-reveal-ease` |
40
+
41
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
42
+
43
+ ```css
44
+ :root {
45
+ --pulse-dur: 1000ms;
46
+ --pulse-count: 1;
47
+ --pulse-min: 0.5;
48
+ --reveal-dur: 400ms;
49
+ --reveal-blur: 2px;
50
+ --reveal-ease: ease-in-out;
51
+ }
52
+ ```
53
+
54
+ ## CSS
55
+
56
+ ```css
57
+ /* The wrap stacks two layers on the same coordinates. The
58
+ skeleton owns the cold pulse + the fade-out side of the
59
+ reveal; the content owns the fade-in side. They share the
60
+ same duration / ease so the swap reads as one motion. */
61
+ .t-skel { position: relative; }
62
+ .t-skel-skeleton,
63
+ .t-skel-content {
64
+ position: absolute;
65
+ inset: 0;
66
+ }
67
+
68
+ .t-skel-skeleton {
69
+ z-index: 1;
70
+ opacity: 1;
71
+ filter: blur(0);
72
+ transition:
73
+ opacity var(--reveal-dur) var(--reveal-ease),
74
+ filter var(--reveal-dur) var(--reveal-ease);
75
+ }
76
+ .t-skel-content {
77
+ z-index: 2;
78
+ opacity: 0;
79
+ filter: blur(var(--reveal-blur));
80
+ transition:
81
+ opacity var(--reveal-dur) var(--reveal-ease),
82
+ filter var(--reveal-dur) var(--reveal-ease);
83
+ }
84
+ .t-skel.is-revealed .t-skel-skeleton {
85
+ opacity: 0;
86
+ filter: blur(var(--reveal-blur));
87
+ }
88
+ .t-skel.is-revealed .t-skel-content {
89
+ opacity: 1;
90
+ filter: blur(0);
91
+ }
92
+ /* Snap-back when replaying: kill transitions so the reverse
93
+ (revealed → skeleton) is instant. Drop `.is-resetting`
94
+ after a forced reflow and the next reveal animates again. */
95
+ .t-skel.is-resetting .t-skel-skeleton,
96
+ .t-skel.is-resetting .t-skel-content {
97
+ transition: none !important;
98
+ }
99
+
100
+ /* Pulse: place the animation on the bar/avatar children, not
101
+ on the skeleton itself, so the skeleton's opacity / filter
102
+ stay free for the cross-fade transition above. */
103
+ .t-skel-skeleton.is-pulsing > * {
104
+ animation: t-skel-pulse var(--pulse-dur) ease-in-out var(--pulse-count);
105
+ }
106
+ @keyframes t-skel-pulse {
107
+ 0%, 100% { opacity: 1; }
108
+ 50% { opacity: var(--pulse-min); }
109
+ }
110
+
111
+ @media (prefers-reduced-motion: reduce) {
112
+ .t-skel-skeleton, .t-skel-content {
113
+ transition: none !important;
114
+ }
115
+ .t-skel-skeleton.is-pulsing > * { animation: none !important; }
116
+ }
117
+ ```
118
+
119
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
120
+
121
+ ## JavaScript orchestration
122
+
123
+ ```js
124
+ const skel = document.querySelector(".t-skel");
125
+ const skeleton = skel.querySelector(".t-skel-skeleton");
126
+ const cs = getComputedStyle(document.documentElement);
127
+ const num = (name, fb) => {
128
+ const v = parseFloat(cs.getPropertyValue(name));
129
+ return Number.isFinite(v) ? v : fb;
130
+ };
131
+
132
+ // Call when async data arrives:
133
+ function reveal() {
134
+ skel.classList.add("is-revealed");
135
+ }
136
+
137
+ // Demo replay: snap back, pulse, then reveal.
138
+ function replay() {
139
+ skel.classList.add("is-resetting");
140
+ skel.classList.remove("is-revealed");
141
+ skeleton.classList.remove("is-pulsing");
142
+ void skeleton.offsetWidth;
143
+ skel.classList.remove("is-resetting");
144
+ skeleton.classList.add("is-pulsing");
145
+ const total = num("--pulse-dur", 1000) * num("--pulse-count", 1);
146
+ setTimeout(() => skel.classList.add("is-revealed"), total);
147
+ }
148
+ ```
149
+
@@ -0,0 +1,95 @@
1
+ # Shimmer text
2
+
3
+ ## When to use
4
+
5
+ A loading / "thinking" label that shimmers — streaming status, "Generating…", any in-progress copy that should feel alive without a spinner. Pure CSS: duplicate the string into `data-text` on `.t-shimmer` and tune `--shimmer-base` / `--shimmer-highlight` per theme.
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <!-- Duplicate the visible string into data-text so the
11
+ ::before layer can mask the gradient onto the same
12
+ glyphs. Keep them in sync if the text changes. -->
13
+ <span class="t-shimmer" data-text="Planning next moves">
14
+ Planning next moves
15
+ </span>
16
+ ```
17
+
18
+ Pure CSS — no JS, no class toggling. Tune --shimmer-base /
19
+ --shimmer-highlight in your own theme rules so the colors
20
+ follow light / dark mode.
21
+
22
+ ## Tunable variables
23
+
24
+ | Variable | Default | Notes |
25
+ | --- | --- | --- |
26
+ | `--shimmer-dur` | `2000ms` | sourced from `--p15-dur` |
27
+ | `--shimmer-base` | `#7c7c7c` | sourced from `--p15-base` |
28
+ | `--shimmer-highlight` | `#0d0d0d` | sourced from `--p15-highlight` |
29
+ | `--shimmer-band` | `400%` | sourced from `--p15-band` |
30
+ | `--shimmer-ease` | `linear` | sourced from `--p15-ease` |
31
+
32
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
33
+
34
+ ```css
35
+ :root {
36
+ --shimmer-dur: 2000ms;
37
+ --shimmer-base: #7c7c7c;
38
+ --shimmer-highlight: #0d0d0d;
39
+ --shimmer-band: 400%;
40
+ --shimmer-ease: linear;
41
+ }
42
+ ```
43
+
44
+ ## CSS
45
+
46
+ ```css
47
+ /* Two-layer construction:
48
+ 1. The base text renders normally in --shimmer-base.
49
+ 2. ::before duplicates it via content: attr(data-text),
50
+ paints a transparent → highlight → transparent gradient
51
+ onto it, and clips that gradient to the glyphs via
52
+ background-clip: text. Animating background-position
53
+ sweeps the band across the text. */
54
+ .t-shimmer {
55
+ position: relative;
56
+ display: inline-block;
57
+ color: var(--shimmer-base);
58
+ }
59
+ .t-shimmer::before {
60
+ content: attr(data-text);
61
+ position: absolute;
62
+ inset: 0;
63
+ pointer-events: none;
64
+ background-image: linear-gradient(
65
+ 90deg,
66
+ transparent 0%,
67
+ transparent 40%,
68
+ var(--shimmer-highlight) 50%,
69
+ transparent 60%,
70
+ transparent 100%
71
+ );
72
+ background-size: var(--shimmer-band) 100%;
73
+ background-repeat: no-repeat;
74
+ -webkit-background-clip: text;
75
+ background-clip: text;
76
+ color: transparent;
77
+ -webkit-text-fill-color: transparent;
78
+ animation: t-shimmer var(--shimmer-dur) var(--shimmer-ease) infinite;
79
+ }
80
+ @keyframes t-shimmer {
81
+ 0% { background-position: 100% 0; }
82
+ 100% { background-position: 0% 0; }
83
+ }
84
+
85
+ @media (prefers-reduced-motion: reduce) {
86
+ .t-shimmer::before { animation: none !important; }
87
+ }
88
+ ```
89
+
90
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
91
+
92
+ ## JavaScript orchestration
93
+
94
+ None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app.
95
+
@@ -0,0 +1,146 @@
1
+ # Tabs sliding
2
+
3
+ ## When to use
4
+
5
+ A segmented control / tab bar where the active pill slides between options — view switchers, filter segments, small mutually-exclusive button sets. JS writes the active tab's `offsetLeft` / `offsetWidth` onto the pill; CSS owns the tween.
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <div class="t-tabs" role="tablist">
11
+ <span class="t-tabs-pill" aria-hidden="true"></span>
12
+ <button class="t-tab" role="tab" aria-selected="true">Plan</button>
13
+ <button class="t-tab" role="tab" aria-selected="false">Debug</button>
14
+ <button class="t-tab" role="tab" aria-selected="false">Ask</button>
15
+ </div>
16
+ ```
17
+
18
+ Wire-up:
19
+ On click, flip aria-selected on each tab and write the
20
+ active tab's offsetLeft / offsetWidth onto the pill:
21
+ pill.style.transform = `translateX(${tab.offsetLeft}px)`;
22
+ pill.style.width = `${tab.offsetWidth}px`;
23
+ On first paint and resize, write the same values WITHOUT
24
+ a transition (suspend with `transition: none`, force a
25
+ reflow, restore) so the pill snaps to position before any
26
+ animation can run.
27
+
28
+ ## Tunable variables
29
+
30
+ | Variable | Default | Notes |
31
+ | --- | --- | --- |
32
+ | `--tabs-dur` | `250ms` | sourced from `--p16-dur` |
33
+ | `--tabs-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p16-ease` |
34
+ | `--tabs-text-muted` | `rgba(15, 15, 15, 0.8)` | sourced from `--p16-text-muted` |
35
+ | `--tabs-text-active` | `#0f0f0f` | sourced from `--p16-text-active` |
36
+ | `--tabs-bar-bg` | `#f1f1f1` | sourced from `--p16-bar-bg` |
37
+ | `--tabs-pill-bg` | `#ffffff` | sourced from `--p16-pill-bg` |
38
+
39
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
40
+
41
+ ```css
42
+ :root {
43
+ --tabs-dur: 250ms;
44
+ --tabs-ease: cubic-bezier(0.22, 1, 0.36, 1);
45
+ --tabs-text-muted: rgba(15, 15, 15, 0.8);
46
+ --tabs-text-active: #0f0f0f;
47
+ --tabs-bar-bg: #f1f1f1;
48
+ --tabs-pill-bg: #ffffff;
49
+ }
50
+ ```
51
+
52
+ ## CSS
53
+
54
+ ```css
55
+ /* The bar is just a flex container with padding for the pill
56
+ to sit inside. Tabs sit on z-index: 1, the pill on z-index: 0,
57
+ so labels read above the pill background. */
58
+ .t-tabs {
59
+ position: relative;
60
+ display: inline-flex;
61
+ align-items: center;
62
+ gap: 3px;
63
+ padding: 3px;
64
+ border-radius: 48px;
65
+ background: var(--tabs-bar-bg);
66
+ }
67
+ .t-tab {
68
+ position: relative;
69
+ appearance: none;
70
+ border: 0;
71
+ background: transparent;
72
+ height: 30px;
73
+ padding: 4px 12px;
74
+ color: var(--tabs-text-muted);
75
+ cursor: pointer;
76
+ border-radius: 48px;
77
+ z-index: 1;
78
+ transition: color var(--tabs-dur) var(--tabs-ease);
79
+ }
80
+ .t-tab:not([aria-selected="true"]):hover,
81
+ .t-tab[aria-selected="true"] {
82
+ color: var(--tabs-text-active);
83
+ }
84
+
85
+ /* The pill: width + transform are written inline by JS so
86
+ the transition tweens between the previous and next
87
+ measured positions. */
88
+ .t-tabs-pill {
89
+ position: absolute;
90
+ top: 3px;
91
+ left: 0;
92
+ height: 30px;
93
+ width: 0;
94
+ background: var(--tabs-pill-bg);
95
+ border-radius: 48px;
96
+ transform: translateX(0);
97
+ transition:
98
+ transform var(--tabs-dur) var(--tabs-ease),
99
+ width var(--tabs-dur) var(--tabs-ease);
100
+ will-change: transform, width;
101
+ z-index: 0;
102
+ pointer-events: none;
103
+ }
104
+
105
+ @media (prefers-reduced-motion: reduce) {
106
+ .t-tabs-pill, .t-tab { transition: none !important; }
107
+ }
108
+ ```
109
+
110
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
111
+
112
+ ## JavaScript orchestration
113
+
114
+ ```js
115
+ const bar = document.querySelector(".t-tabs");
116
+ const pill = bar.querySelector(".t-tabs-pill");
117
+ const tabs = [...bar.querySelectorAll(".t-tab")];
118
+
119
+ function moveTo(tab, animate) {
120
+ if (!animate) {
121
+ const prev = pill.style.transition;
122
+ pill.style.transition = "none";
123
+ pill.style.transform = `translateX(${tab.offsetLeft}px)`;
124
+ pill.style.width = `${tab.offsetWidth}px`;
125
+ void pill.offsetWidth;
126
+ pill.style.transition = prev;
127
+ } else {
128
+ pill.style.transform = `translateX(${tab.offsetLeft}px)`;
129
+ pill.style.width = `${tab.offsetWidth}px`;
130
+ }
131
+ }
132
+ const active = () =>
133
+ tabs.find((t) => t.getAttribute("aria-selected") === "true") || tabs[0];
134
+
135
+ tabs.forEach((tab) => {
136
+ tab.addEventListener("click", () => {
137
+ tabs.forEach((t) =>
138
+ t.setAttribute("aria-selected", t === tab ? "true" : "false")
139
+ );
140
+ moveTo(tab, true);
141
+ });
142
+ });
143
+ requestAnimationFrame(() => moveTo(active(), false));
144
+ window.addEventListener("resize", () => moveTo(active(), false));
145
+ ```
146
+
@@ -0,0 +1,103 @@
1
+ # Tooltip open/close
2
+
3
+ ## When to use
4
+
5
+ A hover/focus tooltip that fades + scales in with a short appear-delay but disappears immediately on leave. Pure CSS — the wrap (not the trigger) is the hover target so the pointer can drift onto the tooltip without flicker.
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <span class="t-tt-wrap">
11
+ <button class="t-tt-trigger" aria-describedby="tt-1">…</button>
12
+ <span class="t-tt" id="tt-1" role="tooltip">Tooltip text</span>
13
+ </span>
14
+ ```
15
+
16
+ Pure CSS — no JS. The wrap (not the trigger) is the hover
17
+ target so the pointer can drift onto the tooltip without
18
+ flicker. transition-delay only applies in the hover/focus
19
+ rule, so leaving snaps it to 0 and the disappear plays
20
+ immediately. Trigger styling is yours; only the tooltip
21
+ positioning + its transition live here.
22
+
23
+ ## Tunable variables
24
+
25
+ | Variable | Default | Notes |
26
+ | --- | --- | --- |
27
+ | `--tt-in-dur` | `150ms` | sourced from `--p17-in-dur` |
28
+ | `--tt-out-dur` | `50ms` | sourced from `--p17-out-dur` |
29
+ | `--tt-scale` | `0.98` | sourced from `--p17-scale-from` |
30
+ | `--tt-delay` | `80ms` | sourced from `--p17-delay` |
31
+ | `--tt-in-ease` | `ease-out` | sourced from `--p17-in-ease` |
32
+ | `--tt-out-ease` | `ease-out` | sourced from `--p17-out-ease` |
33
+ | `--tt-bg` | `#ffffff` | sourced from `--p17-bg` |
34
+ | `--tt-fg` | `#2f2f2f` | sourced from `--p17-fg` |
35
+
36
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
37
+
38
+ ```css
39
+ :root {
40
+ --tt-in-dur: 150ms;
41
+ --tt-out-dur: 50ms;
42
+ --tt-scale: 0.98;
43
+ --tt-delay: 80ms;
44
+ --tt-in-ease: ease-out;
45
+ --tt-out-ease: ease-out;
46
+ --tt-bg: #ffffff;
47
+ --tt-fg: #2f2f2f;
48
+ }
49
+ ```
50
+
51
+ ## CSS
52
+
53
+ ```css
54
+ .t-tt-wrap {
55
+ position: relative;
56
+ display: inline-block;
57
+ }
58
+ .t-tt {
59
+ position: absolute;
60
+ bottom: calc(100% + 8px);
61
+ left: 50%;
62
+ transform: translate(-50%, 0) scale(var(--tt-scale));
63
+ transform-origin: 50% 100%;
64
+ padding: 8px 12px;
65
+ border-radius: 8px;
66
+ background: var(--tt-bg);
67
+ color: var(--tt-fg);
68
+ white-space: nowrap;
69
+ box-shadow:
70
+ 0 0 0 1px rgba(0, 0, 0, 0.06),
71
+ 0 2px 6px 0 rgba(0, 0, 0, 0.05),
72
+ 0 4px 42px 0 rgba(0, 0, 0, 0.06);
73
+ opacity: 0;
74
+ pointer-events: none;
75
+ /* Default rule controls the LEAVE state. transition-delay
76
+ stays unset so leaving plays without delay. */
77
+ transition:
78
+ opacity var(--tt-out-dur) var(--tt-out-ease),
79
+ transform var(--tt-out-dur) var(--tt-out-ease);
80
+ }
81
+ /* The 50ms delay belongs ONLY to the hover rule so leaving
82
+ the trigger snaps the delay back to 0 and the disappear
83
+ plays immediately. */
84
+ .t-tt-wrap:hover .t-tt,
85
+ .t-tt-trigger:focus-visible + .t-tt {
86
+ opacity: 1;
87
+ transform: translate(-50%, 0) scale(1);
88
+ transition-duration: var(--tt-in-dur);
89
+ transition-timing-function: var(--tt-in-ease);
90
+ transition-delay: var(--tt-delay);
91
+ }
92
+
93
+ @media (prefers-reduced-motion: reduce) {
94
+ .t-tt { transition: none !important; }
95
+ }
96
+ ```
97
+
98
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
99
+
100
+ ## JavaScript orchestration
101
+
102
+ None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app.
103
+
@@ -0,0 +1,110 @@
1
+ # Texts reveal
2
+
3
+ ## When to use
4
+
5
+ A headline + supporting line that rise into view with staggered blur — hero copy, empty states, onboarding steps. Exit is decoupled: a single quiet fade with no Y-return so dismissing doesn't replay the reveal in reverse.
6
+
7
+ ## HTML usage
8
+
9
+ ```html
10
+ <div class="t-stagger">
11
+ <strong class="t-stagger-line t-stagger-line--1">…</strong>
12
+ <span class="t-stagger-line t-stagger-line--2">…</span>
13
+ </div>
14
+ ```
15
+
16
+ State:
17
+ - Add `.is-shown` to play the staggered entrance.
18
+ - Add `.is-hiding` (and remove `.is-shown`) to fade
19
+ out in place over a short 200ms — independent of the
20
+ entrance timing so the exit doesn't replay the stagger.
21
+
22
+ Add more lines by adding `.t-stagger-line--N` with
23
+ `transition-delay: calc(var(--stagger-stagger) * (N - 1))`.
24
+
25
+ ## Tunable variables
26
+
27
+ | Variable | Default | Notes |
28
+ | --- | --- | --- |
29
+ | `--stagger-dur` | `500ms` | sourced from `--p18-dur` |
30
+ | `--stagger-distance` | `12px` | sourced from `--p18-distance` |
31
+ | `--stagger-stagger` | `40ms` | sourced from `--p18-stagger` |
32
+ | `--stagger-blur` | `3px` | sourced from `--p18-blur` |
33
+ | `--stagger-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p18-ease` |
34
+
35
+ The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
36
+
37
+ ```css
38
+ :root {
39
+ --stagger-dur: 500ms;
40
+ --stagger-distance: 12px;
41
+ --stagger-stagger: 40ms;
42
+ --stagger-blur: 3px;
43
+ --stagger-ease: cubic-bezier(0.22, 1, 0.36, 1);
44
+ }
45
+ ```
46
+
47
+ ## CSS
48
+
49
+ ```css
50
+ /* Lines start translated down + blurred + invisible; .is-shown
51
+ on the parent flips them to their resting state. The second
52
+ line's transition-delay holds it back by --stagger-stagger
53
+ so the eye lands on the headline first. */
54
+ .t-stagger-line {
55
+ display: block;
56
+ opacity: 0;
57
+ transform: translateY(var(--stagger-distance));
58
+ filter: blur(var(--stagger-blur));
59
+ transition:
60
+ opacity var(--stagger-dur) var(--stagger-ease),
61
+ transform var(--stagger-dur) var(--stagger-ease),
62
+ filter var(--stagger-dur) var(--stagger-ease);
63
+ will-change: transform, opacity, filter;
64
+ }
65
+ .t-stagger-line--2 { transition-delay: var(--stagger-stagger); }
66
+
67
+ .t-stagger.is-shown .t-stagger-line {
68
+ opacity: 1;
69
+ transform: translateY(0);
70
+ filter: blur(0);
71
+ }
72
+ /* Exit decouples from the stagger: same fade for every line,
73
+ no Y return, no blur — so the disappearance reads as a
74
+ single quiet fade instead of a reverse reveal. */
75
+ .t-stagger.is-hiding .t-stagger-line {
76
+ opacity: 0;
77
+ transform: translateY(0);
78
+ filter: blur(0);
79
+ transition:
80
+ opacity 200ms ease,
81
+ transform 0s linear,
82
+ filter 0s linear;
83
+ transition-delay: 0s;
84
+ }
85
+
86
+ @media (prefers-reduced-motion: reduce) {
87
+ .t-stagger-line { transition: none !important; }
88
+ }
89
+ ```
90
+
91
+ The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
92
+
93
+ ## JavaScript orchestration
94
+
95
+ ```js
96
+ const block = document.querySelector(".t-stagger");
97
+
98
+ function showText() {
99
+ block.classList.remove("is-hiding");
100
+ block.classList.remove("is-shown");
101
+ void block.offsetHeight;
102
+ block.classList.add("is-shown");
103
+ }
104
+ function hideText() {
105
+ block.classList.add("is-hiding");
106
+ block.classList.remove("is-shown");
107
+ setTimeout(() => block.classList.remove("is-hiding"), 200);
108
+ }
109
+ ```
110
+