@fluid-theme/core 0.4.3 → 2.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 (107) hide show
  1. package/AGENTS.md +6 -29
  2. package/CHANGELOG.md +564 -2
  3. package/CLAUDE.md +201 -14
  4. package/GEMINI.md +6 -45
  5. package/README.md +17 -8
  6. package/assets/js/compare.js +77 -0
  7. package/assets/js/fit-text.js +103 -0
  8. package/assets/js/lightbox.js +153 -0
  9. package/assets/js/scroll-reveal.js +72 -0
  10. package/assets/js/toc-build.mjs +86 -0
  11. package/assets/js/toc.js +384 -0
  12. package/dist/fluid-1x.min.css +4 -0
  13. package/dist/fluid-fonts-archivo.min.css +2 -0
  14. package/dist/fluid-fonts-bodoni.min.css +2 -0
  15. package/dist/fluid-fonts.min.css +2 -0
  16. package/dist/fluid-legacy-1x.css +3 -0
  17. package/dist/fluid-nav.min.css +3 -0
  18. package/dist/fluid-toc.min.css +3 -0
  19. package/dist/fluid-zero.min.css +3 -0
  20. package/dist/fluid.min.css +3 -2
  21. package/dist/fonts/OFL-archivo.txt +93 -0
  22. package/dist/fonts/OFL-bodoni-moda.txt +93 -0
  23. package/dist/fonts/OFL-hanken-grotesk.txt +93 -0
  24. package/dist/fonts/OFL-newsreader.txt +93 -0
  25. package/dist/fonts/OFL-schibsted-grotesk.txt +93 -0
  26. package/dist/fonts/archivo-latin-wdth-italic.woff2 +0 -0
  27. package/dist/fonts/archivo-latin-wdth-normal.woff2 +0 -0
  28. package/dist/fonts/bodoni-moda-latin-opsz-italic.woff2 +0 -0
  29. package/dist/fonts/bodoni-moda-latin-opsz-normal.woff2 +0 -0
  30. package/dist/fonts/hanken-grotesk-latin-wght-italic.woff2 +0 -0
  31. package/dist/fonts/hanken-grotesk-latin-wght-normal.woff2 +0 -0
  32. package/dist/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
  33. package/dist/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  34. package/dist/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
  35. package/dist/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
  36. package/fluid-agent-kit/README.md +1 -0
  37. package/fluid-agent-kit/skills/accessibility.md +3 -1
  38. package/fluid-agent-kit/skills/css-conventions.md +22 -5
  39. package/fluid-agent-kit/skills/image-optimization.md +2 -0
  40. package/fluid-agent-kit/skills/preset-composition.md +108 -0
  41. package/package.json +36 -11
  42. package/src/animations/scroll-reveal.css +75 -193
  43. package/src/components/accordion.css +6 -47
  44. package/src/components/alert.css +47 -88
  45. package/src/components/avatar.css +4 -20
  46. package/src/components/badge.css +25 -48
  47. package/src/components/button.css +69 -175
  48. package/src/components/card.css +146 -481
  49. package/src/components/chip.css +46 -12
  50. package/src/components/compare.css +234 -0
  51. package/src/components/dropdown.css +10 -67
  52. package/src/components/empty-state.css +4 -0
  53. package/src/components/form.css +113 -81
  54. package/src/components/gallery.css +252 -0
  55. package/src/components/hero.css +203 -88
  56. package/src/components/indicator.css +23 -117
  57. package/src/components/loading.css +7 -29
  58. package/src/components/modal.css +12 -65
  59. package/src/components/nav-modes.css +261 -0
  60. package/src/components/nav.css +324 -392
  61. package/src/components/search-input.css +4 -2
  62. package/src/components/step-indicator.css +26 -27
  63. package/src/components/table.css +64 -171
  64. package/src/components/toc.css +254 -0
  65. package/src/components/tooltip.css +5 -46
  66. package/src/core/base.css +393 -547
  67. package/src/core/reset.css +17 -5
  68. package/src/core/tokens.css +426 -2276
  69. package/src/fluid.css +258 -17
  70. package/src/fonts/OFL-newsreader.txt +93 -0
  71. package/src/fonts/OFL-schibsted-grotesk.txt +93 -0
  72. package/src/fonts/fluid-fonts.css +55 -0
  73. package/src/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
  74. package/src/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  75. package/src/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
  76. package/src/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
  77. package/src/layouts/positioning.css +6 -3
  78. package/src/legacy-1x.css +61 -0
  79. package/src/sections/cta.css +33 -101
  80. package/src/sections/features.css +42 -219
  81. package/src/sections/footer.css +53 -134
  82. package/src/sections/stats.css +40 -153
  83. package/src/sections/team.css +24 -113
  84. package/src/sections/testimonials.css +34 -148
  85. package/src/zero/base.css +166 -0
  86. package/src/zero/tokens.css +64 -0
  87. package/src/zero.css +20 -0
  88. package/starters/workflows/README.md +40 -0
  89. package/starters/workflows/dependabot.yml +21 -0
  90. package/starters/workflows/update-fluid-theme.yml +296 -0
  91. package/themes/fonts/OFL-archivo.txt +93 -0
  92. package/themes/fonts/OFL-bodoni-moda.txt +93 -0
  93. package/themes/fonts/OFL-hanken-grotesk.txt +93 -0
  94. package/themes/fonts/archivo-latin-wdth-italic.woff2 +0 -0
  95. package/themes/fonts/archivo-latin-wdth-normal.woff2 +0 -0
  96. package/themes/fonts/archivo.css +20 -0
  97. package/themes/fonts/bodoni-moda-latin-opsz-italic.woff2 +0 -0
  98. package/themes/fonts/bodoni-moda-latin-opsz-normal.woff2 +0 -0
  99. package/themes/fonts/bodoni.css +35 -0
  100. package/themes/fonts/hanken-grotesk-latin-wght-italic.woff2 +0 -0
  101. package/themes/fonts/hanken-grotesk-latin-wght-normal.woff2 +0 -0
  102. package/themes/gallery.css +105 -0
  103. package/themes/swiss.css +53 -0
  104. package/src/components/scroll-progress.css +0 -104
  105. package/src/components/settings-panel.css +0 -603
  106. package/src/components/themed-images.css +0 -229
  107. package/src/components/version-badge.css +0 -70
@@ -0,0 +1,108 @@
1
+ # Preset Composition — Per-Preset Layout DNA
2
+
3
+ > **Superseded (2026-10-23, Pass 0).** This describes the preset/palette system, which was removed. What the theme is now, and its knobs: [`_docs/STATUS.md`](../../_docs/STATUS.md). Kept as history.
4
+
5
+ When building a page with Fluid, the active `data-preset` is a COMPLETE design decision: it drives which layouts and arrangements you compose, not just colors and fonts. This skill maps each preset to the compositions that suit it.
6
+
7
+ Full preset reference: `_docs/theming-guide.md` ("Visual Presets in Depth").
8
+
9
+ ## The Two Mechanisms
10
+
11
+ **1. Runtime arrangement — `data-arrange="auto"`** (opt-in, on `.hero` and `.features`):
12
+
13
+ - `.hero` — requires flat markup: `hero__headline`, `hero__subheadline`, `hero__actions`, optional `hero__media` as **direct children** (no `hero__content` wrapper). The preset then remaps the arrangement via `grid-template-areas`. `data-arrange` wins over `data-layout`.
14
+ - `.features` — the preset remaps the grid's shape and density.
15
+
16
+ | Preset | Hero signature | Features signature |
17
+ |--------|----------------|--------------------|
18
+ | **still** | Swiss left rail + asymmetric void | Ruled hairline grid |
19
+ | **serene** | Editorial offset: full-width headline, copy hangs right | Single literary column |
20
+ | **rapids** | Poster split: giant headline left, caption column right | Dense 4-up |
21
+ | **tsunami** | Cinema poster: bottom-anchored massive headline, subheadline as kicker | Monumental 2-up |
22
+ | **flowing / stream / cascade** | Centered classic stack | Default responsive card grid |
23
+
24
+ **2. Composition-time choices (the important one)**: when generating page markup for a given preset, CHOOSE section variants, hero `data-layout` (`center` / `split` / `split-reverse`), grid densities, and content order that match the preset's layout DNA below. This applies to ALL sections — not just the two with `data-arrange`.
25
+
26
+ ## Layout DNA
27
+
28
+ | Preset | Canvas | Section rhythm | Hero to prefer | Grid density | Alignment bias |
29
+ |--------|--------|----------------|----------------|--------------|----------------|
30
+ | **still** | Focused 768px column | 32px, tight | Left-rail split, restrained size | Compact, ruled | Flush left |
31
+ | **serene** | 42rem literary column | 144px intervals | Full-width serif headline + offset deck | Single column | Centered, unboxed |
32
+ | **stream** | 1536px wide canvas | 64px | Wide split | 3–4 col cards | Left, horizontal |
33
+ | **flowing** | 1280px | 80px | Balanced centered | 3-up | Centered |
34
+ | **cascade** | 1280px | 64px | Friendly centered | Generous cards | Centered |
35
+ | **rapids** | 1536px | 32px, dense | Poster split, oversized type | Dense 4-up | Hard left |
36
+ | **tsunami** | Full width | 96px cinematic | Full-bleed, bottom-anchored | Monumental 2-up | Left, bottom-anchored |
37
+
38
+ ### still — Swiss precision
39
+
40
+ Mono grayscale default palette, system fonts, restrained display sizes, left-aligned Swiss grid, hairline rules.
41
+
42
+ - DO: compact tables and ruled grids; flush-left headlines; 2-col asymmetric splits with deliberate void
43
+ - DON'T: centered hero copy; decorative gradients; pill shapes
44
+
45
+ ### serene — literary calm
46
+
47
+ Large graceful serif display, morandi default palette (dusty lavender accent), extravagant whitespace.
48
+
49
+ - DO: single-column flows; generous stacked sections; full-width serif headline with offset deck; centered unboxed feature lists
50
+ - DON'T: packed multi-column grids; dense dashboards
51
+
52
+ ### stream — tech-forward
53
+
54
+ Plus Jakarta Sans, fluid deep-teal accent, professional and balanced.
55
+
56
+ - DO: wide split heroes; 3–4 col card grids; horizontal nav emphasis; dashboard patterns
57
+ - DON'T: narrow editorial columns
58
+
59
+ ### flowing — the versatile default
60
+
61
+ Outfit/DM Sans, brand medium-blue accent. Balanced centered heroes, 3-up grids — the reference point when no other DNA applies.
62
+
63
+ ### cascade — warm and bubbly
64
+
65
+ Nunito, bold vivid-purple accent, maximum roundness.
66
+
67
+ - DO: friendly centered layouts; generous card grids; pill buttons
68
+ - DON'T: sharp-edged dense tables
69
+
70
+ ### rapids — bold poster energy
71
+
72
+ Syne/Space Grotesk at 800–900 weight, electric coral accent, oversized display type.
73
+
74
+ - DO: poster splits; dense 4-up packs; tight rhythm; hard offset shadows
75
+ - DON'T: airy whitespace; serif elegance
76
+
77
+ ### tsunami — cinematic maximum
78
+
79
+ Bebas Neue massive display (hero headlines up to 6.5rem), storm-teal accent, asymmetric corners.
80
+
81
+ - DO: full-bleed bottom-anchored heroes; monumental 2-up grids; kicker-above-headline patterns
82
+ - DON'T: small centered cards; subtle anything
83
+
84
+ ### Identity presets
85
+
86
+ - **editorial** — extreme-whitespace serif essay (38rem column, 8rem sections): single-column prose only
87
+ - **brutalist** — monospace ruled rawness: hard borders, raw grids, zero decoration
88
+ - **neon** — dark glowing cyberpunk: dark surfaces, glow shadows, high-tech density
89
+
90
+ ## Default Palettes
91
+
92
+ Each water preset ships a default palette family + accent, so switching presets changes the color mood too:
93
+
94
+ | Preset | Family | Default accent |
95
+ |--------|--------|----------------|
96
+ | still | mono | grayscale slate |
97
+ | serene | morandi | dusty lavender |
98
+ | stream | fluid | deep teal |
99
+ | flowing | fluid | brand medium blue |
100
+ | cascade | bold | vivid purple |
101
+ | rapids | bold | electric coral |
102
+ | tsunami | tsunami | storm teal |
103
+
104
+ Explicit `data-palette` / `data-accent` always override. Do NOT add a `data-palette` unless the user asks for a specific color mood — the default family is part of the preset's design.
105
+
106
+ ## Never Mix DNA
107
+
108
+ Never mix arrangement DNA across presets on one page. If the user's content fights the preset's DNA (e.g., a dense data table on serene), keep the preset's rhythm and adapt the section — or suggest a better-suited preset. Do not import another preset's arrangement to solve it.
package/package.json CHANGED
@@ -1,15 +1,30 @@
1
1
  {
2
2
  "name": "@fluid-theme/core",
3
- "version": "0.4.3",
3
+ "version": "2.1.0",
4
4
  "description": "A modern, semantic CSS design system built on physics principles — not utility classes",
5
5
  "main": "dist/fluid.min.css",
6
6
  "style": "dist/fluid.min.css",
7
7
  "exports": {
8
8
  ".": "./dist/fluid.min.css",
9
+ "./zero": "./dist/fluid-zero.min.css",
10
+ "./fonts": "./dist/fluid-fonts.min.css",
11
+ "./fonts-archivo": "./dist/fluid-fonts-archivo.min.css",
12
+ "./fonts-bodoni": "./dist/fluid-fonts-bodoni.min.css",
13
+ "./legacy-1x": "./dist/fluid-legacy-1x.css",
14
+ "./1x": "./dist/fluid-1x.min.css",
15
+ "./toc": "./dist/fluid-toc.min.css",
16
+ "./nav": "./dist/fluid-nav.min.css",
17
+ "./toc-build": "./assets/js/toc-build.mjs",
18
+ "./themes/swiss": "./themes/swiss.css",
19
+ "./themes/gallery": "./themes/gallery.css",
20
+ "./themes/*": "./themes/*",
9
21
  "./src": "./src/fluid.css"
10
22
  },
11
23
  "files": [
12
24
  "dist/",
25
+ "themes/",
26
+ "starters/workflows/",
27
+ "assets/js/",
13
28
  "src/",
14
29
  "fluid-agent-kit/",
15
30
  "CLAUDE.md",
@@ -24,7 +39,14 @@
24
39
  "build": "node scripts/build-css.mjs",
25
40
  "prepublishOnly": "npm run build",
26
41
  "optimize-images": "node scripts/optimize-images.js",
27
- "test": "echo \"Visual testing only - open examples/foundation.html in browser\""
42
+ "test:reveal": "node scripts/reveal-check.mjs",
43
+ "test:forms": "node scripts/form-check.mjs",
44
+ "test:axe": "node scripts/axe-check.mjs",
45
+ "test:focus": "node scripts/focus-check.mjs",
46
+ "test:toc": "node scripts/toc-check.mjs",
47
+ "test:nav": "node scripts/nav-check.mjs",
48
+ "test": "echo \"Visual testing only - open examples/foundation.html in browser\"",
49
+ "test:zero": "node scripts/visual-scan.mjs scripts/fixtures/zero.html && node scripts/axe-check.mjs --pages scripts/fixtures/zero.html && node scripts/focus-check.mjs --page scripts/fixtures/zero.html"
28
50
  },
29
51
  "keywords": [
30
52
  "css",
@@ -48,21 +70,24 @@
48
70
  "url": "https://github.com/Agile-Media-Agency/the-fluid-theme/issues"
49
71
  },
50
72
  "homepage": "https://thefluidtheme.com",
51
- "engines": {
52
- "browsers": [
53
- "last 2 Chrome versions",
54
- "last 2 Firefox versions",
55
- "last 2 Safari versions",
56
- "last 2 Edge versions"
57
- ]
58
- },
59
73
  "browserslist": [
60
74
  "supports css-cascade-layers",
61
75
  "supports css-container-queries",
62
76
  "supports css-logical-properties"
63
77
  ],
64
78
  "devDependencies": {
79
+ "@capsizecss/core": "^4.1.3",
80
+ "@capsizecss/metrics": "^4.4.0",
81
+ "@fontsource-variable/newsreader": "^5.3.0",
82
+ "@fontsource-variable/schibsted-grotesk": "^5.3.0",
83
+ "axe-core": "^4.11.1",
84
+ "lighthouse": "^13.0.3",
65
85
  "lightningcss": "^1.32.0",
66
- "sharp": "^0.34.5"
86
+ "playwright": "^1.64.1",
87
+ "puppeteer": "^24.40.0",
88
+ "sharp": "^0.35.5"
89
+ },
90
+ "publishConfig": {
91
+ "access": "public"
67
92
  }
68
93
  }
@@ -1,8 +1,11 @@
1
1
  /**
2
- * Scroll Reveal Animations
2
+ * Scroll Reveal Animations (opt-in)
3
3
  *
4
- * Add [data-reveal] attribute to elements you want to animate in on scroll.
5
- * Works with the scroll-reveal.js script or CSS-only with animation-timeline.
4
+ * Only elements that carry [data-reveal] ever animate, and only once
5
+ * assets/js/scroll-reveal.js has set data-reveal-ready on <html>. Without
6
+ * the script, with scripting off, or with reduced motion asked for, nothing
7
+ * is hidden: the hidden start state exists only under all three conditions.
8
+ * A bare .section is never hidden (that blanked the stack's pages, #61).
6
9
  *
7
10
  * Variants:
8
11
  * data-reveal="fade" - Fade in
@@ -16,209 +19,88 @@
16
19
  * Timing:
17
20
  * data-reveal-delay="100" - Delay in ms (100, 200, 300, 400, 500)
18
21
  * data-reveal-duration="slow" - Duration (fast, normal, slow)
22
+ * data-reveal-stagger on a parent staggers its [data-reveal] children
19
23
  */
20
24
 
21
25
  @layer components {
22
- /* Base reveal state - hidden by default */
23
- [data-reveal] {
24
- opacity: 0;
25
- transition:
26
- opacity var(--reveal-duration, var(--duration-slow)) var(--reveal-easing, cubic-bezier(0.16, 1, 0.3, 1)),
27
- transform var(--reveal-duration, var(--duration-slow)) var(--reveal-easing, cubic-bezier(0.16, 1, 0.3, 1)),
28
- filter var(--reveal-duration, var(--duration-slow)) var(--reveal-easing, cubic-bezier(0.16, 1, 0.3, 1));
29
- will-change: opacity, transform;
30
- }
31
-
32
- /* Revealed state */
33
- [data-reveal].revealed,
34
- [data-reveal][data-revealed] {
35
- opacity: 1;
36
- transform: none;
37
- filter: none;
38
- }
39
-
40
- /* Fade variants - starting positions */
41
- [data-reveal="fade-up"] {
42
- transform: translateY(2rem);
43
- }
44
-
45
- [data-reveal="fade-down"] {
46
- transform: translateY(-2rem);
47
- }
48
-
49
- [data-reveal="fade-left"] {
50
- transform: translateX(2rem);
51
- }
52
-
53
- [data-reveal="fade-right"] {
54
- transform: translateX(-2rem);
55
- }
56
-
57
- [data-reveal="scale"] {
58
- transform: scale(0.95);
59
- }
60
-
61
- [data-reveal="blur"] {
62
- filter: blur(8px);
63
- }
64
-
65
- /* Delays */
66
- [data-reveal-delay="100"] { transition-delay: 100ms; }
67
- [data-reveal-delay="200"] { transition-delay: 200ms; }
68
- [data-reveal-delay="300"] { transition-delay: 300ms; }
69
- [data-reveal-delay="400"] { transition-delay: 400ms; }
70
- [data-reveal-delay="500"] { transition-delay: 500ms; }
71
-
72
- /* Durations */
73
- [data-reveal-duration="fast"] { --reveal-duration: var(--duration-fast); }
74
- [data-reveal-duration="normal"] { --reveal-duration: var(--duration-slow); }
75
- [data-reveal-duration="slow"] { --reveal-duration: var(--duration-slower); }
76
-
77
- /* Stagger children - useful for grids and lists */
78
- [data-reveal-stagger] > [data-reveal]:nth-child(1) { transition-delay: calc(var(--reveal-stagger, 100ms) * 0); }
79
- [data-reveal-stagger] > [data-reveal]:nth-child(2) { transition-delay: calc(var(--reveal-stagger, 100ms) * 1); }
80
- [data-reveal-stagger] > [data-reveal]:nth-child(3) { transition-delay: calc(var(--reveal-stagger, 100ms) * 2); }
81
- [data-reveal-stagger] > [data-reveal]:nth-child(4) { transition-delay: calc(var(--reveal-stagger, 100ms) * 3); }
82
- [data-reveal-stagger] > [data-reveal]:nth-child(5) { transition-delay: calc(var(--reveal-stagger, 100ms) * 4); }
83
- [data-reveal-stagger] > [data-reveal]:nth-child(6) { transition-delay: calc(var(--reveal-stagger, 100ms) * 5); }
84
- [data-reveal-stagger] > [data-reveal]:nth-child(7) { transition-delay: calc(var(--reveal-stagger, 100ms) * 6); }
85
- [data-reveal-stagger] > [data-reveal]:nth-child(8) { transition-delay: calc(var(--reveal-stagger, 100ms) * 7); }
86
-
87
- /* ========================================
88
- AUTO-REVEAL BY MOTION STYLE
89
- Sections auto-animate based on theme motion setting
90
- ======================================== */
91
-
92
- /* Still: No animations */
93
- [data-motion="still"] [data-reveal] {
94
- opacity: 1;
95
- transform: none;
96
- filter: none;
97
- transition: none;
98
- }
99
-
100
- /* Serene: Very slow, gentle fade */
101
- [data-motion="serene"] [data-reveal] {
102
- --reveal-duration: var(--duration-slower);
103
- --reveal-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
104
- }
105
-
106
- [data-motion="serene"] .section:not([data-reveal]) {
107
- opacity: 0;
108
- transform: scale(1.02);
109
- transition:
110
- opacity var(--duration-slowest) cubic-bezier(0.25, 0.1, 0.25, 1),
111
- transform var(--duration-slowest) cubic-bezier(0.25, 0.1, 0.25, 1);
112
- }
113
-
114
- [data-motion="serene"] .section:not([data-reveal]).revealed {
115
- opacity: 1;
116
- transform: none;
117
- }
118
-
119
- /* Stream: Gentle horizontal flow */
120
- [data-motion="stream"] [data-reveal] {
121
- --reveal-duration: var(--duration-slow);
122
- --reveal-easing: cubic-bezier(0.22, 1, 0.36, 1);
123
- }
124
-
125
- [data-motion="stream"] .section:not([data-reveal]) {
126
- opacity: 0;
127
- transform: translateX(-1rem);
128
- transition:
129
- opacity var(--duration-slow) cubic-bezier(0.22, 1, 0.36, 1),
130
- transform var(--duration-slow) cubic-bezier(0.22, 1, 0.36, 1);
131
- }
132
-
133
- [data-motion="stream"] .section:not([data-reveal]).revealed {
134
- opacity: 1;
135
- transform: none;
136
- }
137
-
138
- /* Flowing: Smooth ease-in-out, organic */
139
- [data-motion="flowing"] [data-reveal] {
140
- --reveal-duration: var(--duration-slow);
141
- --reveal-easing: cubic-bezier(0.16, 1, 0.3, 1);
142
- }
143
-
144
- [data-motion="flowing"] .section:not([data-reveal]) {
145
- opacity: 0;
146
- transform: translateY(1.5rem);
147
- transition:
148
- opacity var(--duration-slow) cubic-bezier(0.16, 1, 0.3, 1),
149
- transform var(--duration-slow) cubic-bezier(0.16, 1, 0.3, 1);
150
- }
151
-
152
- [data-motion="flowing"] .section:not([data-reveal]).revealed {
153
- opacity: 1;
154
- transform: none;
155
- }
26
+ @media (prefers-reduced-motion: no-preference) {
27
+ /* Hidden start state: the script is running and the element has not
28
+ been revealed yet. */
29
+ /* Headlines never animate in. A bare data-reveal rises 12px and fades. */
30
+ :root[data-reveal-ready] [data-reveal]:not(.revealed, [data-revealed], h1, h2, h3, .display, .hero__headline) {
31
+ opacity: 0;
32
+ }
156
33
 
157
- /* Cascade: Medium energy, waterfall effect */
158
- [data-motion="cascade"] [data-reveal] {
159
- --reveal-duration: var(--duration-normal);
160
- --reveal-easing: cubic-bezier(0.34, 1.56, 0.64, 1);
161
- }
34
+ :root[data-reveal-ready] [data-reveal=""]:not(.revealed, [data-revealed], h1, h2, h3, .display, .hero__headline) {
35
+ translate: 0 12px;
36
+ }
162
37
 
163
- [data-motion="cascade"] .section:not([data-reveal]) {
164
- opacity: 0;
165
- transform: translateY(2rem);
166
- transition:
167
- opacity var(--duration-normal) cubic-bezier(0.34, 1.56, 0.64, 1),
168
- transform var(--duration-normal) cubic-bezier(0.34, 1.56, 0.64, 1);
169
- }
38
+ :root[data-reveal-ready] [data-reveal="fade-up"]:not(.revealed, [data-revealed]) {
39
+ transform: translateY(2rem);
40
+ }
170
41
 
171
- [data-motion="cascade"] .section:not([data-reveal]).revealed {
172
- opacity: 1;
173
- transform: none;
174
- }
42
+ :root[data-reveal-ready] [data-reveal="fade-down"]:not(.revealed, [data-revealed]) {
43
+ transform: translateY(-2rem);
44
+ }
175
45
 
176
- /* Rapids: Fast, direct */
177
- [data-motion="rapids"] [data-reveal] {
178
- --reveal-duration: var(--duration-fast);
179
- --reveal-easing: cubic-bezier(0.25, 0.46, 0.45, 0.94);
180
- }
46
+ :root[data-reveal-ready] [data-reveal="fade-left"]:not(.revealed, [data-revealed]) {
47
+ transform: translateX(2rem);
48
+ }
181
49
 
182
- [data-motion="rapids"] .section:not([data-reveal]) {
183
- opacity: 0;
184
- transform: scale(0.98);
185
- transition:
186
- opacity var(--duration-fast) cubic-bezier(0.25, 0.46, 0.45, 0.94),
187
- transform var(--duration-fast) cubic-bezier(0.25, 0.46, 0.45, 0.94);
188
- }
50
+ :root[data-reveal-ready] [data-reveal="fade-right"]:not(.revealed, [data-revealed]) {
51
+ transform: translateX(-2rem);
52
+ }
189
53
 
190
- [data-motion="rapids"] .section:not([data-reveal]).revealed {
191
- opacity: 1;
192
- transform: none;
193
- }
54
+ :root[data-reveal-ready] [data-reveal="scale"]:not(.revealed, [data-revealed]) {
55
+ transform: scale(0.95);
56
+ }
194
57
 
195
- /* Tsunami: Dramatic, bold sweep */
196
- [data-motion="tsunami"] [data-reveal] {
197
- --reveal-duration: var(--duration-normal);
198
- --reveal-easing: cubic-bezier(0.68, -0.2, 0.32, 1.2);
199
- }
58
+ :root[data-reveal-ready] [data-reveal="blur"]:not(.revealed, [data-revealed]) {
59
+ filter: blur(8px);
60
+ }
200
61
 
201
- [data-motion="tsunami"] .section:not([data-reveal]) {
202
- opacity: 0;
203
- transform: translateY(3rem) scale(0.97);
204
- transition:
205
- opacity var(--duration-normal) cubic-bezier(0.68, -0.2, 0.32, 1.2),
206
- transform var(--duration-normal) cubic-bezier(0.68, -0.2, 0.32, 1.2);
207
- }
62
+ [data-reveal] {
63
+ transition:
64
+ opacity var(--reveal-duration, var(--duration-slow)) var(--reveal-easing, var(--ease-settle)),
65
+ translate var(--reveal-duration, var(--duration-slow)) var(--reveal-easing, var(--ease-settle)),
66
+ transform var(--reveal-duration, var(--duration-slow)) var(--reveal-easing, var(--ease-settle)),
67
+ filter var(--reveal-duration, var(--duration-slow)) var(--reveal-easing, var(--ease-settle));
68
+ }
208
69
 
209
- [data-motion="tsunami"] .section:not([data-reveal]).revealed {
210
- opacity: 1;
211
- transform: none;
212
- }
70
+ /* While the script switches the page to ready, below-the-fold content
71
+ goes to its hidden start state at once instead of fading out. */
72
+ [data-reveal-init] [data-reveal] {
73
+ transition: none;
74
+ }
213
75
 
214
- /* Reduced motion preference */
215
- @media (prefers-reduced-motion: reduce) {
216
- [data-reveal],
217
- .section {
218
- opacity: 1 !important;
219
- transform: none !important;
220
- filter: none !important;
221
- transition: none !important;
76
+ /* A sideways start position adds 2rem of horizontal overflow until the
77
+ element is revealed, so a phone could scroll sideways (services
78
+ template: 32px at every width up to 1024px). Sections holding one clip
79
+ it; clip, unlike hidden, leaves vertical overflow and sticky alone. */
80
+ .section:has([data-reveal="fade-left"], [data-reveal="fade-right"]) {
81
+ overflow-x: clip;
222
82
  }
83
+
84
+ /* Delays */
85
+ [data-reveal-delay="100"] { transition-delay: 100ms; }
86
+ [data-reveal-delay="200"] { transition-delay: 200ms; }
87
+ [data-reveal-delay="300"] { transition-delay: 300ms; }
88
+ [data-reveal-delay="400"] { transition-delay: 400ms; }
89
+ [data-reveal-delay="500"] { transition-delay: 500ms; }
90
+
91
+ /* Durations */
92
+ [data-reveal-duration="fast"] { --reveal-duration: var(--duration-fast); }
93
+ [data-reveal-duration="normal"] { --reveal-duration: var(--duration-slow); }
94
+ [data-reveal-duration="slow"] { --reveal-duration: var(--duration-slower); }
95
+
96
+ /* Stagger children - useful for grids and lists */
97
+ [data-reveal-stagger] > [data-reveal]:nth-child(1) { transition-delay: calc(var(--reveal-stagger, 100ms) * 0); }
98
+ [data-reveal-stagger] > [data-reveal]:nth-child(2) { transition-delay: calc(var(--reveal-stagger, 100ms) * 1); }
99
+ [data-reveal-stagger] > [data-reveal]:nth-child(3) { transition-delay: calc(var(--reveal-stagger, 100ms) * 2); }
100
+ [data-reveal-stagger] > [data-reveal]:nth-child(4) { transition-delay: calc(var(--reveal-stagger, 100ms) * 3); }
101
+ [data-reveal-stagger] > [data-reveal]:nth-child(5) { transition-delay: calc(var(--reveal-stagger, 100ms) * 4); }
102
+ [data-reveal-stagger] > [data-reveal]:nth-child(6) { transition-delay: calc(var(--reveal-stagger, 100ms) * 5); }
103
+ [data-reveal-stagger] > [data-reveal]:nth-child(7) { transition-delay: calc(var(--reveal-stagger, 100ms) * 6); }
104
+ [data-reveal-stagger] > [data-reveal]:nth-child(8) { transition-delay: calc(var(--reveal-stagger, 100ms) * 7); }
223
105
  }
224
106
  }
@@ -57,7 +57,7 @@
57
57
  /* Bordered variant: each item is a card */
58
58
  .accordion[data-variant="bordered"] .accordion__item {
59
59
  border: var(--border-width) solid var(--color-border-subtle);
60
- border-radius: var(--radius-lg);
60
+ border-radius: var(--radius-card);
61
61
  overflow: hidden;
62
62
  }
63
63
 
@@ -68,9 +68,8 @@
68
68
  /* Separated variant: elevated card items */
69
69
  .accordion[data-variant="separated"] .accordion__item {
70
70
  border: var(--border-width) solid var(--color-border-subtle);
71
- border-radius: var(--radius-lg);
71
+ border-radius: var(--radius-card);
72
72
  background: var(--color-surface-raised);
73
- box-shadow: var(--shadow-xs);
74
73
  overflow: hidden;
75
74
  }
76
75
 
@@ -137,15 +136,15 @@
137
136
 
138
137
  /* Hover */
139
138
  .accordion__trigger:hover {
140
- color: var(--accent);
139
+ color: var(--accent-ink);
141
140
  background-color: color-mix(in oklch, var(--accent) 5%, transparent);
142
141
  }
143
142
 
144
143
  /* Focus */
145
144
  .accordion__trigger:focus-visible {
146
- outline: 2px solid var(--color-focus);
145
+ outline: var(--focus-ring-width) solid var(--color-focus);
147
146
  outline-offset: -2px;
148
- border-radius: var(--radius-sm);
147
+ border-radius: calc(var(--radius-control) * 0.6);
149
148
  }
150
149
 
151
150
 
@@ -201,50 +200,10 @@
201
200
  }
202
201
 
203
202
 
204
- /* ========================================
205
- THEME-SPECIFIC STYLES
206
- ======================================== */
207
-
208
- /* Still: no animation */
209
- [data-motion="still"] .accordion__trigger::after {
210
- transition: none;
211
- }
212
-
213
- /* Serene: rounded, soft */
214
- [data-motion="serene"] .accordion[data-variant="separated"] .accordion__item,
215
- [data-motion="serene"] .accordion[data-variant="bordered"] .accordion__item {
216
- border-radius: var(--radius-xl);
217
- }
218
-
219
- /* Cascade: bouncy, bubbly */
220
- [data-motion="cascade"] .accordion[data-variant="separated"] .accordion__item,
221
- [data-motion="cascade"] .accordion[data-variant="bordered"] .accordion__item {
222
- border-radius: var(--radius-xl);
223
- }
224
-
225
- /* Rapids: hard shadow on separated */
226
- [data-motion="rapids"] .accordion[data-variant="separated"] .accordion__item {
227
- box-shadow: var(--shadow-offset);
228
- }
229
-
230
- /* Tsunami: asymmetric corners */
231
- [data-motion="tsunami"] .accordion[data-variant="separated"] .accordion__item,
232
- [data-motion="tsunami"] .accordion[data-variant="bordered"] .accordion__item {
233
- border-radius: var(--radius-asymmetric, 0 24px 0 0);
234
- }
235
-
236
203
 
237
204
  /* ========================================
238
205
  REDUCED MOTION
239
206
  ======================================== */
240
207
 
241
- @media (prefers-reduced-motion: reduce) {
242
- .accordion__trigger {
243
- transition: none;
244
- }
245
-
246
- .accordion__trigger::after {
247
- transition: none;
248
- }
249
- }
208
+ /* Reduced motion: the reset shortens every transition. */
250
209
  }