@fluid-theme/core 0.4.3 → 2.0.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 (80) hide show
  1. package/AGENTS.md +6 -29
  2. package/CHANGELOG.md +543 -2
  3. package/CLAUDE.md +193 -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.min.css +2 -0
  14. package/dist/fluid-legacy-1x.css +3 -0
  15. package/dist/fluid-nav.min.css +3 -0
  16. package/dist/fluid-toc.min.css +3 -0
  17. package/dist/fluid-zero.min.css +3 -0
  18. package/dist/fluid.min.css +3 -2
  19. package/dist/fonts/OFL-newsreader.txt +93 -0
  20. package/dist/fonts/OFL-schibsted-grotesk.txt +93 -0
  21. package/dist/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
  22. package/dist/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  23. package/dist/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
  24. package/dist/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
  25. package/fluid-agent-kit/README.md +1 -0
  26. package/fluid-agent-kit/skills/accessibility.md +3 -1
  27. package/fluid-agent-kit/skills/css-conventions.md +22 -5
  28. package/fluid-agent-kit/skills/image-optimization.md +2 -0
  29. package/fluid-agent-kit/skills/preset-composition.md +108 -0
  30. package/package.json +29 -3
  31. package/src/animations/scroll-reveal.css +75 -193
  32. package/src/components/accordion.css +6 -47
  33. package/src/components/alert.css +47 -88
  34. package/src/components/avatar.css +4 -20
  35. package/src/components/badge.css +25 -48
  36. package/src/components/button.css +69 -175
  37. package/src/components/card.css +146 -481
  38. package/src/components/chip.css +46 -12
  39. package/src/components/compare.css +234 -0
  40. package/src/components/dropdown.css +10 -67
  41. package/src/components/empty-state.css +4 -0
  42. package/src/components/form.css +113 -81
  43. package/src/components/gallery.css +252 -0
  44. package/src/components/hero.css +203 -88
  45. package/src/components/indicator.css +23 -117
  46. package/src/components/loading.css +7 -29
  47. package/src/components/modal.css +12 -65
  48. package/src/components/nav-modes.css +261 -0
  49. package/src/components/nav.css +324 -392
  50. package/src/components/search-input.css +4 -2
  51. package/src/components/step-indicator.css +26 -27
  52. package/src/components/table.css +64 -171
  53. package/src/components/toc.css +254 -0
  54. package/src/components/tooltip.css +5 -46
  55. package/src/core/base.css +393 -547
  56. package/src/core/reset.css +17 -5
  57. package/src/core/tokens.css +426 -2276
  58. package/src/fluid.css +258 -17
  59. package/src/fonts/OFL-newsreader.txt +93 -0
  60. package/src/fonts/OFL-schibsted-grotesk.txt +93 -0
  61. package/src/fonts/fluid-fonts.css +55 -0
  62. package/src/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
  63. package/src/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  64. package/src/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
  65. package/src/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
  66. package/src/layouts/positioning.css +6 -3
  67. package/src/legacy-1x.css +61 -0
  68. package/src/sections/cta.css +33 -101
  69. package/src/sections/features.css +42 -219
  70. package/src/sections/footer.css +53 -134
  71. package/src/sections/stats.css +40 -153
  72. package/src/sections/team.css +24 -113
  73. package/src/sections/testimonials.css +34 -148
  74. package/src/zero/base.css +166 -0
  75. package/src/zero/tokens.css +64 -0
  76. package/src/zero.css +20 -0
  77. package/src/components/scroll-progress.css +0 -104
  78. package/src/components/settings-panel.css +0 -603
  79. package/src/components/themed-images.css +0 -229
  80. package/src/components/version-badge.css +0 -70
@@ -0,0 +1,254 @@
1
+ /**
2
+ * TOC: a table of contents with scroll spy (spec _docs/plans/base-v2.md §13)
3
+ * @layer components
4
+ *
5
+ * The server writes the list from the article's own headings (H2 top level,
6
+ * H3 nested; assets/js/toc-build.mjs), so crawlers, AI fetchers and no-JS
7
+ * readers get plain links. assets/js/toc.js adds the spy and the dock.
8
+ * Built to its own file, dist/fluid-toc.min.css (package export "./toc"):
9
+ * link it after core on article pages. Gate: scripts/toc-check.mjs.
10
+ *
11
+ * <div class="article__body">
12
+ * <nav class="toc" data-toc="rail dock" aria-labelledby="toc-label">
13
+ * <details class="toc__fold" open>
14
+ * <summary class="toc__label" id="toc-label">On this page</summary>
15
+ * <ol><li><a href="#cost">What it costs</a>
16
+ * <ol><li><a href="#labour">Labour</a></li></ol></li></ol>
17
+ * </details>
18
+ * </nav>
19
+ * <div class="prose content-grid">…</div>
20
+ * </div>
21
+ *
22
+ * data-toc words, one per layer:
23
+ * placement inline (in the flow) · rail (sticky side column at 70rem and
24
+ * up, inline below) · dock (pinned two-row bar; needs toc.js) ·
25
+ * "rail dock" (rail when wide, dock when narrow)
26
+ * side end puts the rail after the article (default: inline start)
27
+ * data-toc-depth="2" lists H2s only. toc.js sets data-toc-mode
28
+ * (inline | rail | dock) and aria-current="location" on the active link.
29
+ *
30
+ * Look hooks, read with fallbacks and never declared (set them on :root in
31
+ * a child theme): --toc-font, --toc-dock-font, --toc-dock-bg, --toc-size,
32
+ * --toc-sub-size (H3 entries, default 0.93em), --toc-color (resting entries; the active
33
+ * entry and its section's H2 rise to the text colour), --toc-active,
34
+ * --toc-rule, --toc-marker (a counter style for H2 entries, e.g.
35
+ * decimal-leading-zero).
36
+ *
37
+ * TOC PRESETS: each is markup words + a :root snippet. Layer 1 placement
38
+ * (vertical rail / inline list / horizontal dock), layer 2 behaviour
39
+ * (static: no toc.js; dynamic: toc.js spy, centring, the dock), layer 3
40
+ * motion (glide; a jump under reduced motion or data-motion="reduced"),
41
+ * layer 4 the look (the hooks).
42
+ * editorial (the default) data-toc="rail dock" no snippet
43
+ * numbered data-toc="rail dock" :root { --toc-marker: decimal-leading-zero; }
44
+ * serif data-toc="rail dock" :root { --toc-font: var(--font-heading); --toc-size: var(--step-0); }
45
+ * swiss data-toc="rail dock" :root { --toc-marker: decimal-leading-zero; --toc-dock-font: var(--font-body); --toc-rule: 3px; }
46
+ * gallery data-toc="dock" :root { --toc-font: var(--font-body); --toc-dock-font: var(--font-display); }
47
+ * quiet (static list) data-toc="inline" no toc.js; :root { --toc-sub-size: 0.875em; }
48
+ * end rail data-toc="rail end" the rail after the article
49
+ *
50
+ * The dock shows one section's subsections at a time (row two): the current
51
+ * section's, or those of the section that holds keyboard focus. The others
52
+ * stay in the accessibility tree and the tab order, clipped until reached.
53
+ */
54
+
55
+ @layer components {
56
+ .toc { font-variant-numeric: lining-nums proportional-nums; }
57
+
58
+ /* The type sits on the fold, not the nav: the nav's own ch (its --measure
59
+ width below) then resolves at body size and lines up with the prose. */
60
+ .toc__fold {
61
+ margin: 0;
62
+ font: var(--toc-size, var(--step--1)) / 1.3 var(--toc-font, var(--font-body));
63
+ }
64
+
65
+ .toc ol { list-style: none; margin: 0; padding: 0; counter-reset: toc; }
66
+ .toc ol ol { margin-inline-start: 1em; }
67
+ .toc li { margin: 0; counter-increment: toc; }
68
+
69
+ /* Rows 44px under a coarse pointer, 32px with a mouse. The rule sits on
70
+ the link, so the active mark moves with its (indented) text. */
71
+ .toc { --_toc-row: 2.75rem; }
72
+ @media (pointer: fine) { .toc { --_toc-row: 2rem; } }
73
+
74
+ .toc a {
75
+ display: block;
76
+ padding-block: calc((var(--_toc-row) - 1lh) / 2);
77
+ padding-inline: var(--space-s) 0;
78
+ border-inline-start: var(--toc-rule, var(--border-width-thick)) solid transparent;
79
+ color: var(--toc-color, var(--color-text-muted));
80
+ text-decoration-line: none;
81
+ }
82
+
83
+ /* The outline reads at a glance: sections in a heavier weight,
84
+ subsections smaller. At rest both are muted, so the current place
85
+ (the active entry and its section) stands out in the text colour. */
86
+ .toc__fold > ol > li > a { font-weight: calc(var(--weight-medium) + var(--_dark-weight)); }
87
+ .toc__fold > ol > li + li { margin-block-start: var(--space-1); }
88
+ .toc li li a { font-size: var(--toc-sub-size, 0.93em); }
89
+
90
+ .toc a:hover { color: var(--color-text); text-decoration-line: underline; }
91
+ .toc a:focus-visible { outline-offset: -2px; }
92
+
93
+ /* The H2 that holds the active H3 is lit without the rule. */
94
+ .toc li:has(a[aria-current]) > a,
95
+ .toc a[aria-current] { color: var(--color-text); }
96
+ .toc a[aria-current] { border-inline-start-color: var(--toc-active, var(--accent-ink)); }
97
+
98
+ /* Forced colours turn the transparent resting rule into a LinkText bar
99
+ on every entry; only the active one keeps a mark. */
100
+ @media (forced-colors: active) {
101
+ .toc a { border-inline-start-color: Canvas; }
102
+ .toc a[aria-current] { border-inline-start-color: Highlight; text-decoration-line: underline; }
103
+ }
104
+
105
+ /* Numbering: a leading space collapses when the counter style is none. */
106
+ .toc__fold > ol > li > a::before {
107
+ content: counter(toc, var(--toc-marker, none)) " ";
108
+ word-spacing: 0.5em;
109
+ }
110
+
111
+ .article__body > .toc {
112
+ inline-size: min(100% - 2 * var(--page-edge), var(--measure));
113
+ margin: 0 auto var(--space-l);
114
+ }
115
+
116
+ /* RAIL: two columns, the nav sticky in the first (or the second, "end").
117
+ Full bleed stops at the article column on these pages. toc.js keeps the
118
+ active entry in view when the list is taller than the rail. */
119
+ @media (width >= 70rem) {
120
+ .article__body:has(> .toc[data-toc~="rail"]) {
121
+ display: grid;
122
+ grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
123
+ column-gap: var(--space-l);
124
+ padding-inline: max(var(--page-edge), (100% - var(--container-content)) / 2);
125
+ }
126
+
127
+ .article__body:has(> .toc[data-toc~="rail"][data-toc~="end"]) { grid-template-columns: minmax(0, 1fr) minmax(12rem, 16rem); }
128
+ .article__body:has(> .toc[data-toc~="rail"]) > .content-grid { --_edge: 0rem; }
129
+
130
+ .article__body > .toc[data-toc~="rail"] {
131
+ position: sticky;
132
+ inset-block-start: var(--space-xl);
133
+ align-self: start;
134
+ inline-size: auto;
135
+ max-block-size: calc(100dvh - 2 * var(--space-xl));
136
+ margin: 0;
137
+ overflow-y: auto;
138
+ }
139
+
140
+ .article__body > .toc[data-toc~="end"] { grid-area: 1 / 2; }
141
+ }
142
+
143
+ /* DOCK: a bar pinned to the bottom. Row one is the sections, the current
144
+ one centred with its neighbours either side; row two is the current
145
+ section's subsections. toc.js centres both. Off screen it is only
146
+ translated away: it stays in the accessibility tree and the tab order,
147
+ and slides in when a link inside it takes focus. */
148
+ .toc[data-toc-mode="dock"] {
149
+ position: fixed;
150
+ inset: auto 0 0;
151
+ z-index: var(--z-sticky);
152
+ inline-size: auto;
153
+ margin: 0;
154
+ padding-block-end: env(safe-area-inset-bottom, 0px);
155
+ /* A hint off the page, so it reads as part of the content: the surface
156
+ tint, one step stronger in dark (the plain surface is 1.07:1 there). */
157
+ --_dock-bg: var(--toc-dock-bg, light-dark(var(--color-surface), var(--color-surface-raised)));
158
+ background-color: var(--_dock-bg);
159
+ border-block-start: var(--border-width) solid var(--color-border);
160
+ translate: 0 100%;
161
+ transition: translate var(--duration-normal) var(--ease-settle);
162
+ }
163
+
164
+ .toc[data-toc-mode="dock"]:is([data-toc-visible], :has(:focus-visible)) { translate: none; }
165
+ .toc[data-toc-instant] { transition: none; }
166
+ .toc[data-toc-mode="dock"] .toc__fold { border: 0; }
167
+ .toc[data-toc-mode="dock"] .toc__label { display: none; }
168
+
169
+ .toc[data-toc-mode="dock"] ol {
170
+ display: flex;
171
+ margin: 0;
172
+ overflow-x: auto;
173
+ scrollbar-width: none;
174
+ overscroll-behavior-inline: contain;
175
+ padding-inline: 50%;
176
+ }
177
+
178
+ /* The neighbours fade only at the very edge (1.5rem), so a neighbour's
179
+ label keeps its contrast; an overlay, not a mask, because a mask on row
180
+ one would also hide row two, which sits inside it. */
181
+ .toc[data-toc-mode="dock"]::after {
182
+ content: "";
183
+ position: absolute;
184
+ inset: 0;
185
+ pointer-events: none;
186
+ background: linear-gradient(90deg, var(--_dock-bg), transparent 1.5rem calc(100% - 1.5rem), var(--_dock-bg));
187
+ }
188
+
189
+ /* No scroll snap: toc.js centres the rows, and snapping rows cancelled
190
+ the page's own scroll to a #fragment while it loaded (Chromium). */
191
+ .toc[data-toc-mode="dock"] li { flex: none; }
192
+ .toc[data-toc-mode="dock"] li + li { margin: 0; }
193
+ .toc[data-toc-mode="dock"] a {
194
+ --_toc-row: 2.75rem;
195
+ white-space: nowrap;
196
+ padding-inline: var(--space-s);
197
+ border: 0;
198
+ }
199
+
200
+ .toc[data-toc-mode="dock"] .toc__fold > ol > li > a {
201
+ font: calc(var(--weight-display) + var(--_dark-weight)) var(--step-0) / 1.2 var(--toc-dock-font, var(--font-heading));
202
+ }
203
+
204
+ /* On a phone a neighbour is cut short with an ellipsis, so both read as
205
+ words either side of the current section (which is never cut).
206
+ toc.js writes --_toc-side, the room left beside the centred entry,
207
+ so a neighbour ends with its ellipsis on screen, never cut at the edge. */
208
+ @media (width < 48rem) {
209
+ .toc[data-toc-mode="dock"] .toc__fold > ol > li:not(:has(a[aria-current])) > a {
210
+ max-inline-size: var(--_toc-side, 34vw);
211
+ overflow: hidden;
212
+ text-overflow: ellipsis;
213
+ }
214
+ }
215
+
216
+ .toc[data-toc-mode="dock"] li > ol {
217
+ position: absolute;
218
+ inset: auto 0 env(safe-area-inset-bottom, 0px);
219
+ }
220
+
221
+ /* Row two: the current section's subsections, or those of the section
222
+ that holds keyboard focus (a link a pointer pressed keeps its focus
223
+ but does not count). The others are clipped, not removed. */
224
+ .toc[data-toc-mode="dock"] li:not(:has(a[aria-current])) > ol,
225
+ .toc[data-toc-mode="dock"] .toc__fold:has(a:focus-visible) > ol > li:not(:has(a:focus-visible)) > ol {
226
+ clip-path: inset(50%);
227
+ opacity: 0;
228
+ pointer-events: none;
229
+ }
230
+
231
+ .toc[data-toc-mode="dock"] .toc__fold > ol > li:has(a:focus-visible) > ol {
232
+ clip-path: none;
233
+ opacity: 1;
234
+ pointer-events: auto;
235
+ }
236
+
237
+ /* Row two's space is kept whenever any section has subsections, so the
238
+ bar never grows and shrinks between sections. */
239
+ .toc[data-toc-mode="dock"]:has(li > ol) { padding-block-end: calc(2.75rem + env(safe-area-inset-bottom, 0px)); }
240
+
241
+ .toc[data-toc-mode="dock"] :is(li:has(a[aria-current]) > a, a[aria-current]) {
242
+ text-decoration: underline var(--toc-rule, var(--border-width-thick)) var(--toc-active, var(--accent-ink));
243
+ text-underline-offset: 0.35em;
244
+ }
245
+
246
+ /* The article ends with the section rhythm in every mode (the rail left
247
+ its last line 18px above the next band). */
248
+ .article__body { padding-block-end: var(--space-section); }
249
+ .article__body:has(> .toc[data-toc-mode="dock"]) { padding-block-end: max(var(--space-section), var(--_toc-dock-size, 6rem) + var(--space-m)); }
250
+ /* toc.js writes the dock's measured height; 6rem until it does. */
251
+ :root:has(.toc[data-toc-mode="dock"]) { scroll-padding-block-end: calc(var(--_toc-dock-size, 6rem) + var(--space-s)); }
252
+
253
+ @media print { .toc[data-toc-mode="dock"] { display: none; } }
254
+ }
@@ -50,14 +50,11 @@
50
50
  max-inline-size: min(20rem, 90vw);
51
51
  padding: var(--space-1-5) var(--space-3);
52
52
  border-radius: var(--radius-md);
53
- box-shadow: var(--shadow-md);
54
53
 
55
54
  /* Hidden state */
56
55
  opacity: 0;
57
- transform: scale(0.95);
58
56
  transition:
59
- opacity var(--duration-fast) var(--ease-fluid),
60
- transform var(--duration-fast) var(--ease-settle);
57
+ opacity var(--duration-fast) var(--ease-fluid);
61
58
  }
62
59
 
63
60
  /* Dark mode tooltip — automatically inverts via semantic tokens above */
@@ -66,7 +63,6 @@
66
63
  [data-tooltip]:hover::after,
67
64
  [data-tooltip]:focus-visible::after {
68
65
  opacity: 1;
69
- transform: scale(1);
70
66
  }
71
67
 
72
68
 
@@ -153,10 +149,8 @@
153
149
 
154
150
  /* Hidden by default */
155
151
  opacity: 0;
156
- transform: scale(0.95) translateY(-4px);
157
152
  transition:
158
153
  opacity var(--duration-fast) var(--ease-fluid),
159
- transform var(--duration-fast) var(--ease-settle),
160
154
  display var(--duration-fast) allow-discrete,
161
155
  overlay var(--duration-fast) allow-discrete;
162
156
  }
@@ -165,22 +159,19 @@
165
159
  .popover:popover-open,
166
160
  .popover[data-open] {
167
161
  opacity: 1;
168
- transform: scale(1) translateY(0);
169
162
  }
170
163
 
171
164
  /* Starting style for entry animation */
172
165
  @starting-style {
173
166
  .popover:popover-open {
174
167
  opacity: 0;
175
- transform: scale(0.95) translateY(-4px);
176
168
  }
177
169
  }
178
170
 
179
171
  .popover__content {
180
- background: var(--color-surface-raised);
181
- border: var(--border-width) solid var(--color-border-subtle);
182
- border-radius: var(--radius-xl);
183
- box-shadow: var(--shadow-lg);
172
+ background: var(--color-background);
173
+ border: var(--border-width) solid var(--color-border-strong);
174
+ border-radius: var(--radius-card);
184
175
  padding: var(--space-4);
185
176
  min-inline-size: 12rem;
186
177
  max-inline-size: 24rem;
@@ -199,29 +190,6 @@
199
190
  }
200
191
 
201
192
 
202
- /* ========================================
203
- THEME-SPECIFIC STYLES
204
- ======================================== */
205
-
206
- /* Still: no animation */
207
- [data-motion="still"] [data-tooltip]::after {
208
- transition: none;
209
- }
210
-
211
- [data-motion="still"] .popover {
212
- transition: none;
213
- }
214
-
215
- /* Rapids: hard shadow */
216
- [data-motion="rapids"] .popover__content {
217
- box-shadow: var(--shadow-offset);
218
- }
219
-
220
- /* Tsunami: asymmetric */
221
- [data-motion="tsunami"] .popover__content {
222
- border-radius: var(--radius-asymmetric, 0 24px 0 0);
223
- }
224
-
225
193
 
226
194
  /* ========================================
227
195
  ACCESSIBILITY
@@ -234,14 +202,5 @@
234
202
  }
235
203
  }
236
204
 
237
- /* Reduced motion: instant show/hide */
238
- @media (prefers-reduced-motion: reduce) {
239
- [data-tooltip]::after {
240
- transition: none;
241
- }
242
-
243
- .popover {
244
- transition: none;
245
- }
246
- }
205
+ /* Reduced motion: the reset shortens every transition (instant show/hide). */
247
206
  }