@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
@@ -14,10 +14,67 @@
14
14
  * </ul>
15
15
  * </nav>
16
16
  *
17
+ * The bar is the page's other scheme by default (_docs/defaults.md): a dark
18
+ * bar on a light page, a light bar on a dark one; tokens.css flips every
19
+ * colour role on it. Content on the .container edges, no hairline unless
20
+ * sticky. Links: sans 500 in the text colour, underlined on hover; the
21
+ * current page gets a 2px underline. No pills, no blur, no lift.
22
+ *
23
+ * Small screens (a bar under 40rem), every nav without the 1.x .nav__toggle
24
+ * (that one folds its menu behind the button): the links take their own row
25
+ * under the brand; the space between them and their tracking tighten first,
26
+ * then they wrap in balanced rows (4 links: 2 + 2, never 3 + 1). Both only
27
+ * ever remove orphans. data-nav="fit" is accepted and does nothing extra.
28
+ *
17
29
  * Variants:
18
- * data-variant="transparent" - Transparent background
19
- * data-variant="elevated" - With shadow
20
- * data-sticky - Sticky positioning
30
+ * data-sticky - sticky, with a hairline under it
31
+ * data-variant="transparent" - no background, the page's colours
32
+ * data-bg="plain" - the page's colours on the page's background
33
+ * data-bg="inverse" - the default; forces the flipped bar on a transparent nav
34
+ *
35
+ * Alignment follows the page (_docs/defaults.md): wrapped link rows inherit
36
+ * text-align, so <body data-align="center"> centres them; data-align="center"
37
+ * on the .nav, on the body or header it is a direct child of, or on the body
38
+ * of that header, also centres the masthead (the brand on its own row, the rest under it). The
39
+ * nearest setting wins.
40
+ *
41
+ * Modes, in the opt-in module nav-modes.css (dist/fluid-nav.min.css; link it
42
+ * after core, as the TOC module is). Without it each falls back to the plain nav.
43
+ * data-sticky="links" - two rows: the brand row scrolls away, the links stick
44
+ * data-nav="icon" - the brand shrinks to its .nav__brand-mark; the links
45
+ * share its row: <a class="nav__brand"><span class="nav__brand-mark"
46
+ * aria-hidden="true">NE</span><span class="nav__brand-name">Name</span></a>
47
+ * data-nav="compact" - a "+" button opens the links in a panel that slides
48
+ * over from the end edge; the "+" turns into a close "×". No JS (popover):
49
+ * <button class="nav__more" type="button" popovertarget="nav-panel">
50
+ * <span class="sr-only">Menu</span></button>
51
+ * <div class="nav__panel" id="nav-panel" popover>
52
+ * <button class="nav__more" type="button" popovertarget="nav-panel"
53
+ * popovertargetaction="hide"><span class="sr-only">Close menu</span></button>
54
+ * <ul class="nav__menu" role="list">…</ul>
55
+ * </div>
56
+ * Without popover support, or on a wide bar, the panel is invisible to
57
+ * layout and the links show as usual, so the markup is safe on every nav.
58
+ * data-nav="compact drop" - the panel drops under the bar instead
59
+ *
60
+ * Tokens (a theme sets them on :root; the fallbacks are the defaults):
61
+ * --nav-gap link side padding, half the space between links (--space-3)
62
+ * --nav-gap-min the squeeze stops here (--space-2, 8px)
63
+ * --nav-tracking link letter-spacing (0em)
64
+ * --nav-tracking-min the squeeze stops here (= --nav-tracking)
65
+ * --nav-wrap-align start | center | end, for wrapped rows (unset: the
66
+ * page's alignment)
67
+ * --nav-bg, --nav-ink a bar colour and the colour of what is on it (text,
68
+ * links, toggles, marks, focus ring; not the CTA,
69
+ * button, badge or logo-mark fills); set both, on
70
+ * :root: --nav-bg: var(--accent-fill);
71
+ * --nav-ink: var(--on-accent-fill) (_docs/defaults.md)
72
+ * --nav-glyph the compact button's glyph ("+")
73
+ * --nav-glyph-turn how far it turns when open (45deg: + to ×)
74
+ * --nav-panel-size width of the compact side panel (min(20rem, 85vw))
75
+ * --nav-stick-offset how far a data-sticky="links" bar hides (set by the module)
76
+ * --nav-sticky-size jump-link clearance under a sticky bar (5rem; 13rem
77
+ * under 64rem, where the links may wrap)
21
78
  */
22
79
 
23
80
  @layer components {
@@ -26,152 +83,155 @@
26
83
  ======================================== */
27
84
 
28
85
  .nav {
29
- /* Layout */
30
86
  display: flex;
87
+ /* Wrap here, not in the container query below: an element cannot query
88
+ its own container. */
89
+ flex-wrap: wrap;
31
90
  align-items: center;
32
- gap: var(--space-6);
33
- padding-block: var(--space-4);
34
- padding-inline: var(--space-5);
35
-
36
- /* Visual */
37
- background-color: var(--_nav-bg, var(--color-background));
38
- border-block-end: 1px solid var(--_nav-border, var(--color-border-subtle));
39
-
40
- /* Container for responsive behavior */
91
+ gap: var(--space-2) var(--space-m);
92
+ padding-block: var(--space-s);
93
+ padding-inline: max(var(--page-edge), (100% - var(--container-content)) / 2);
94
+ background-color: var(--_nav-bg, var(--nav-bg, var(--color-background)));
95
+ border-block-end: var(--border-width) solid var(--_nav-border, transparent);
41
96
  container-type: inline-size;
42
97
  container-name: nav;
98
+ --color-focus: var(--nav-ink, var(--accent-ink));
99
+ }
100
+
101
+ /* A nav that is also a .container already sits on the content edges; the
102
+ edge padding (a % of the body, not of the nav) would take its width. */
103
+ .nav.container { padding-inline: 0; }
104
+
105
+ /* As the page's top bar, its paint still reaches the screen edges: a
106
+ border image outset paints past the box without adding scroll,
107
+ hairline included. Paint only, so content scrolled under a sticky
108
+ bar's sides stays clickable; forced colours keep the system Canvas. */
109
+ :is(body, body > header) > .nav.container {
110
+ --_p: var(--_nav-bg, var(--nav-bg, var(--color-background)));
111
+ border-image: linear-gradient(0deg, var(--_nav-border, var(--_p)) var(--border-width), var(--_p) 0) fill 0 / 0 / 0 100vmax;
112
+ }
113
+
114
+ @media (forced-colors: active) {
115
+ /* -source: lightningcss minifies the border-image shorthand "none" to
116
+ an empty, invalid declaration (CLAUDE.md Exceptions). */
117
+ :is(body, body > header) > .nav.container { border-image-source: none; }
43
118
  }
44
119
 
45
- /* Sticky navigation */
46
120
  .nav[data-sticky] {
47
121
  position: sticky;
48
122
  inset-block-start: 0;
49
123
  z-index: var(--z-sticky);
50
- backdrop-filter: blur(8px);
51
- background-color: oklch(from var(--color-background) l c h / 0.9);
124
+ --_nav-border: var(--color-border);
125
+ }
126
+
127
+ /* A jump to #section must land below a sticky bar (WCAG 2.4.11). CSS
128
+ cannot read the bar's height (72px on one row; up to about 210px when
129
+ the links wrap on a phone), so the clearance is generous: too much
130
+ leaves a gap, too little hides the heading. A site with a known bar
131
+ sets --nav-sticky-size. Targets add their own scroll margin on top. */
132
+ :root:has(.nav[data-sticky]) { scroll-padding-block-start: var(--nav-sticky-size, 5rem); }
133
+
134
+ @media (width < 64rem) {
135
+ :root:has(.nav[data-sticky]) { scroll-padding-block-start: var(--nav-sticky-size, 13rem); }
52
136
  }
53
137
 
54
- /* Transparent variant */
55
138
  .nav[data-variant="transparent"] {
56
139
  --_nav-bg: transparent;
57
140
  --_nav-border: transparent;
58
141
  }
59
142
 
60
- /* Elevated variant */
61
- .nav[data-variant="elevated"] {
62
- --_nav-border: transparent;
63
- box-shadow: var(--shadow-sm);
64
- }
143
+ /* Over a hero, everything on it takes the hero's text colour. */
144
+ .nav[data-variant="transparent"]:not([data-bg="inverse"]) { --nav-ink: currentColor; }
65
145
 
146
+ /* After transparent on purpose: a forced inverse bar needs its paint. */
147
+ .nav[data-bg="inverse"] { --_nav-bg: var(--nav-bg, var(--color-background)); }
66
148
 
67
- /* Brand / Logo - adopts active preset's font */
68
149
  .nav__brand {
69
- font-family: var(--font-heading, var(--font-body, var(--font-system)));
70
- font-size: var(--font-size-lg);
71
- font-weight: var(--weight-semibold);
72
- color: var(--color-text);
73
- text-decoration: none;
74
150
  display: flex;
75
151
  align-items: center;
76
152
  gap: var(--space-2);
77
153
  flex-shrink: 0;
78
- transition:
79
- opacity var(--duration-normal) var(--ease-fluid),
80
- font-family var(--duration-normal) var(--ease-fluid);
81
- }
82
-
83
- .nav__brand:hover {
84
- opacity: 0.8;
154
+ font-family: var(--font-heading);
155
+ font-size: var(--step-1);
156
+ font-weight: calc(500 + var(--_dark-weight));
157
+ letter-spacing: -0.01em;
158
+ color: var(--nav-ink, var(--color-text));
159
+ text-decoration: none;
85
160
  }
86
161
 
87
- .nav__brand img,
88
- .nav__brand svg {
162
+ .nav__brand :is(img, svg) {
89
163
  block-size: 2rem;
90
164
  inline-size: auto;
91
165
  }
92
166
 
93
- /* Active preset name indicator */
94
- .nav__preset-name {
95
- font-family: var(--font-heading, var(--font-body, var(--font-system)));
96
- font-size: var(--font-size-xs);
97
- font-weight: var(--weight-medium);
98
- color: var(--accent);
99
- letter-spacing: var(--tracking-wide);
100
- text-transform: uppercase;
101
- white-space: nowrap;
102
- flex-shrink: 0;
103
- transition:
104
- color var(--duration-normal) var(--ease-fluid),
105
- font-family var(--duration-normal) var(--ease-fluid);
106
- }
107
-
108
- .nav__preset-name:empty {
109
- display: none;
110
- }
111
-
112
-
113
- /* Menu container */
114
167
  .nav__menu {
115
168
  display: flex;
169
+ flex-wrap: wrap;
116
170
  align-items: center;
117
- gap: var(--space-1);
171
+ gap: 0 var(--space-1);
118
172
  margin: 0;
119
- margin-inline-start: auto; /* Default: push menu to end */
173
+ margin-inline-start: auto;
120
174
  padding: 0;
121
175
  list-style: none;
122
- overflow: visible; /* Allow dropdowns to overflow */
176
+ overflow: visible;
123
177
  }
124
178
 
125
- /* Menu items - ensure alignment and reset base styles */
126
179
  .nav__menu > li {
127
180
  display: flex;
128
181
  align-items: center;
129
- margin: 0; /* Override base li margin that can cause misalignment */
130
- position: relative; /* For dropdown positioning */
131
- }
132
-
133
- /* Keep menu at start (next to brand) */
134
- .nav__menu[data-align="start"] {
135
- margin-inline-start: 0;
182
+ margin: 0;
183
+ position: relative;
136
184
  }
137
185
 
138
- /* Center menu */
139
- .nav__menu[data-align="center"] {
140
- margin-inline: auto;
141
- }
186
+ .nav__menu[data-align="start"] { margin-inline-start: 0; --nav-wrap-align: start; }
187
+ .nav__menu[data-align="center"] { margin-inline: auto; --nav-wrap-align: center; }
142
188
 
189
+ /* Centred masthead: data-align="center" on the nav, its header or the
190
+ body (unless a nearer header or the nav sets its own), the brand on
191
+ its own row. */
192
+ :is(.nav[data-align="center"], :is(body, header)[data-align="center"] > .nav:not([data-align]), body[data-align="center"] > header:not([data-align]) > .nav:not([data-align])) { justify-content: center; }
193
+ :is(.nav[data-align="center"], :is(body, header)[data-align="center"] > .nav:not([data-align]), body[data-align="center"] > header:not([data-align]) > .nav:not([data-align])) > .nav__brand { flex-basis: 100%; justify-content: center; }
194
+ :is(.nav[data-align="center"], :is(body, header)[data-align="center"] > .nav:not([data-align]), body[data-align="center"] > header:not([data-align]) > .nav:not([data-align])) > .nav__actions { margin-inline: 0; }
195
+ /* :where(): the small-screen menu keeps its own (negative) margins. */
196
+ :where(.nav[data-align="center"], :is(body, header)[data-align="center"] > .nav:not([data-align]), body[data-align="center"] > header:not([data-align]) > .nav:not([data-align])) > :is(.nav__menu, .nav__panel) { margin-inline: 0; }
143
197
 
144
- /* Navigation links */
145
- .nav__link {
198
+ /* --_nav-pad and --_nav-track are the small-screen squeeze (below). */
199
+ :is(.nav__link, .nav__dropdown-trigger, .nav__dropdown-link) {
146
200
  display: flex;
147
201
  align-items: center;
148
- gap: var(--space-2);
149
- padding: var(--space-2) var(--space-3);
150
- font-size: var(--font-size-sm);
151
- font-weight: var(--weight-medium);
152
- line-height: 1; /* Prevent line-height variance affecting alignment */
153
- color: var(--color-text-muted);
154
- text-decoration: none;
155
- border-radius: var(--radius-md);
156
- white-space: nowrap; /* Prevent wrapping that can affect height */
157
- transition:
158
- color var(--duration-normal) var(--ease-fluid),
159
- background-color var(--duration-normal) var(--ease-fluid);
202
+ gap: var(--space-1);
203
+ min-block-size: 2.75rem;
204
+ padding-inline: var(--_nav-pad, var(--nav-gap, var(--space-3)));
205
+ font-family: var(--font-body);
206
+ font-size: 0.9375rem;
207
+ font-weight: calc(500 + var(--_dark-weight));
208
+ line-height: 1;
209
+ letter-spacing: var(--_nav-track, var(--nav-tracking, 0em));
210
+ color: var(--nav-ink, var(--color-text));
211
+ white-space: nowrap;
212
+ text-decoration-line: none;
213
+ text-decoration-thickness: 0.06em;
214
+ text-underline-offset: 0.4em;
215
+ background: none;
216
+ border: 0;
217
+ border-radius: var(--radius-control);
218
+ cursor: pointer;
160
219
  }
161
220
 
162
- .nav__link:hover {
163
- color: var(--color-text);
164
- background-color: var(--color-surface);
221
+ :is(.nav__link, .nav__dropdown-trigger, .nav__dropdown-link):hover {
222
+ text-decoration-line: underline;
223
+ text-decoration-color: var(--nav-ink, var(--accent-ink));
165
224
  }
166
225
 
167
- .nav__link[aria-current="page"],
168
- .nav__link[data-active] {
169
- color: var(--accent);
170
- background-color: var(--accent-subtle);
226
+ /* The accent as ink, not fill: it flips with the scheme, so the mark
227
+ clears 3:1 on a dark band or an inverse bar too. */
228
+ :is(.nav__link, .nav__dropdown-link):is([aria-current], [data-active]),
229
+ .nav__dropdown-trigger[data-active] {
230
+ text-decoration-line: underline;
231
+ text-decoration-thickness: 2px;
232
+ text-decoration-color: var(--nav-ink, var(--accent-ink));
171
233
  }
172
234
 
173
-
174
- /* Actions area (buttons, search, etc.) */
175
235
  .nav__actions {
176
236
  display: flex;
177
237
  align-items: center;
@@ -179,198 +239,172 @@
179
239
  margin-inline-start: auto;
180
240
  }
181
241
 
242
+ /* The links always hang at the end edge: with a CTA after them, the CTA
243
+ takes the bar's gap, not a second auto margin (the menu may sit in a
244
+ compact .nav__panel). */
245
+ :is(.nav__menu, .nav__panel) ~ .nav__actions { margin-inline-start: 0; }
246
+
182
247
 
183
248
  /* ========================================
184
249
  DROPDOWN MENUS
185
250
  ======================================== */
186
251
 
187
- .nav__dropdown {
188
- position: relative;
189
- }
190
-
191
- .nav__dropdown-trigger {
192
- display: flex;
193
- align-items: center;
194
- gap: var(--space-1);
195
- padding: var(--space-2) var(--space-3);
196
- font-size: var(--font-size-sm);
197
- font-weight: var(--weight-medium);
198
- color: var(--color-text-muted);
199
- text-decoration: none;
200
- background: none;
201
- border: none;
202
- border-radius: var(--radius-md);
203
- cursor: pointer;
204
- transition:
205
- color var(--duration-normal) var(--ease-fluid),
206
- background-color var(--duration-normal) var(--ease-fluid);
207
- }
208
-
209
- .nav__dropdown-trigger:hover,
210
- .nav__dropdown:hover .nav__dropdown-trigger,
211
- .nav__dropdown:focus-within .nav__dropdown-trigger {
212
- color: var(--color-text);
213
- background-color: var(--color-surface);
214
- }
215
-
216
- .nav__dropdown-trigger[data-active] {
217
- color: var(--accent);
218
- }
252
+ .nav__dropdown { position: relative; }
219
253
 
220
254
  .nav__dropdown-trigger svg {
221
255
  inline-size: 1em;
222
256
  block-size: 1em;
223
- transition: transform var(--duration-normal) var(--ease-fluid);
224
- }
225
-
226
- .nav__dropdown:hover .nav__dropdown-trigger svg,
227
- .nav__dropdown:focus-within .nav__dropdown-trigger svg {
228
- transform: rotate(180deg);
229
257
  }
230
258
 
231
259
  .nav__dropdown-menu {
232
260
  position: absolute;
233
261
  inset-block-start: 100%;
234
262
  inset-inline-start: 0;
263
+ z-index: var(--z-dropdown);
235
264
  min-inline-size: 12rem;
236
- padding: var(--space-2);
265
+ padding: var(--space-1) 0;
237
266
  margin: 0;
238
- margin-block-start: var(--space-1);
239
267
  list-style: none;
240
- background-color: var(--color-background);
241
- border: 1px solid var(--color-border-subtle);
242
- border-radius: var(--radius-lg);
243
- box-shadow: var(--shadow-lg);
268
+ background-color: var(--nav-bg, var(--color-background));
269
+ border: var(--border-width) solid var(--color-border-strong);
270
+ border-radius: var(--radius-card);
244
271
  opacity: 0;
245
272
  visibility: hidden;
246
- transform: translateY(-0.5rem);
247
273
  transition:
248
- opacity var(--duration-normal) var(--ease-fluid),
249
- visibility var(--duration-normal) var(--ease-fluid),
250
- transform var(--duration-normal) var(--ease-fluid);
251
- z-index: var(--z-dropdown);
274
+ opacity var(--duration-fast) var(--ease-fluid),
275
+ visibility var(--duration-fast) var(--ease-fluid);
252
276
  }
253
277
 
254
- .nav__dropdown:hover .nav__dropdown-menu,
255
- .nav__dropdown:focus-within .nav__dropdown-menu {
278
+ .nav__dropdown:is(:hover, :focus-within) .nav__dropdown-menu {
256
279
  opacity: 1;
257
280
  visibility: visible;
258
- transform: translateY(0);
259
281
  }
260
282
 
261
- .nav__dropdown-menu li {
262
- margin: 0;
263
- }
283
+ .nav__dropdown-menu li { margin: 0; }
264
284
 
265
- .nav__dropdown-link {
266
- display: block;
267
- padding: var(--space-2) var(--space-3);
268
- font-size: var(--font-size-sm);
269
- color: var(--color-text-muted);
270
- text-decoration: none;
271
- border-radius: var(--radius-md);
272
- transition:
273
- color var(--duration-normal) var(--ease-fluid),
274
- background-color var(--duration-normal) var(--ease-fluid);
275
- }
285
+ /* Inset, so the ring stays on the panel's colour, not the page's. */
286
+ .nav__dropdown-link:focus-visible { outline-offset: calc(-1 * var(--focus-ring-width)); }
276
287
 
277
- .nav__dropdown-link:hover {
278
- color: var(--color-text);
279
- background-color: var(--color-surface);
280
- }
281
288
 
282
- .nav__dropdown-link[aria-current="page"] {
283
- color: var(--accent);
284
- background-color: var(--accent-subtle);
285
- }
286
-
287
- /* CTA Button in nav */
289
+ /* CTA in the bar: a primary button, no lift. */
288
290
  .nav__cta {
289
291
  display: inline-flex;
290
292
  align-items: center;
291
293
  gap: var(--space-2);
292
- padding: var(--space-2) var(--space-4);
293
- font-size: var(--font-size-sm);
294
- font-weight: var(--weight-medium);
295
- color: var(--white);
294
+ min-block-size: 2.75rem;
295
+ padding-inline: 1.2em;
296
+ font-size: 0.9375rem;
297
+ font-weight: calc(var(--weight-semibold) + var(--_dark-weight));
298
+ color: var(--on-accent-fill);
296
299
  text-decoration: none;
297
- background-color: var(--accent);
298
- border-radius: var(--radius-md);
299
- transition:
300
- background-color var(--duration-normal) var(--ease-fluid),
301
- transform var(--duration-fast) var(--ease-settle);
300
+ background-color: var(--accent-fill);
301
+ border-radius: var(--radius-control);
302
+ transition: background-color var(--duration-fast) var(--ease-fluid);
302
303
  }
303
304
 
304
- .nav__cta:hover {
305
- background-color: var(--accent-hover);
306
- transform: translateY(-1px);
307
- }
305
+ .nav__cta:hover { background-color: var(--accent-fill-hover); }
308
306
 
309
307
 
310
308
  /* ========================================
311
- RESPONSIVE NAVIGATION
312
- Adapts to container width
309
+ TOGGLES
310
+ .nav__toggle is the 1.x JS menu button (aria-expanded, or data-open on
311
+ the menu), kept so existing markup works. data-nav="compact" (below) is
312
+ its no-JS successor: use that for new sites.
313
313
  ======================================== */
314
314
 
315
- /* Mobile toggle button */
316
- .nav__toggle {
317
- display: none;
318
- padding: var(--space-2);
319
- background: none;
320
- border: none;
321
- color: var(--color-text);
322
- cursor: pointer;
323
- }
324
-
325
- .nav__toggle svg {
326
- inline-size: 1.5rem;
327
- block-size: 1.5rem;
328
- }
329
-
330
- /* Theme Toggle Button */
331
- .nav__theme-toggle {
315
+ :is(.nav__toggle, .nav__theme-toggle) {
332
316
  display: flex;
333
317
  align-items: center;
334
318
  justify-content: center;
319
+ flex-shrink: 0;
320
+ min-inline-size: 2.75rem;
321
+ min-block-size: 2.75rem;
335
322
  padding: var(--space-2);
323
+ color: var(--nav-ink, var(--color-text));
336
324
  background: none;
337
- border: none;
338
- border-radius: var(--radius-md);
339
- color: var(--color-text-muted);
325
+ border: 0;
340
326
  cursor: pointer;
341
- transition:
342
- color var(--duration-normal) var(--ease-fluid),
343
- background-color var(--duration-normal) var(--ease-fluid);
344
327
  }
345
328
 
346
- .nav__theme-toggle:hover {
347
- color: var(--color-text);
348
- background-color: var(--color-surface);
349
- }
329
+ .nav__toggle { display: none; }
350
330
 
351
- .nav__theme-toggle svg {
352
- inline-size: 1.25rem;
353
- block-size: 1.25rem;
331
+ .nav__toggle svg { inline-size: 1.5rem; block-size: 1.5rem; }
332
+ .nav__theme-toggle svg { inline-size: 1.25rem; block-size: 1.25rem; }
333
+
334
+ /* Icon buttons in the bar stay quiet on hover and press (the generic
335
+ button fill is for calls to action). */
336
+ .nav :is(.nav__toggle, .nav__theme-toggle, .nav__more, .nav__dropdown-trigger):is(:hover, :active):not(:disabled) {
337
+ color: var(--nav-ink, var(--color-text));
338
+ background-color: transparent;
354
339
  }
355
340
 
356
- /* Show appropriate icon based on theme */
357
341
  .nav__theme-toggle .icon-sun { display: none; }
358
342
  .nav__theme-toggle .icon-moon { display: block; }
359
-
360
343
  [data-theme="dark"] .nav__theme-toggle .icon-sun { display: block; }
361
344
  [data-theme="dark"] .nav__theme-toggle .icon-moon { display: none; }
362
345
 
363
346
 
364
- /* Narrow container - stack navigation */
347
+ /* ========================================
348
+ NARROW: brand and CTA share the top row. With a .nav__toggle the menu
349
+ folds under it; without one the links wrap in a row under the brand,
350
+ so a bar with no toggle never loses its links.
351
+ ======================================== */
352
+
365
353
  @container nav (max-width: 40rem) {
366
- .nav {
367
- flex-wrap: wrap;
354
+ .nav__brand { min-inline-size: 0; }
355
+
356
+ .nav > .nav__actions {
357
+ order: 1;
358
+ margin-inline-start: auto;
359
+ }
360
+
361
+ .nav__cta {
362
+ min-block-size: 2.75rem;
363
+ padding-inline: 1em;
364
+ font-size: var(--step--1);
368
365
  }
369
366
 
367
+ /* The links' own row. First the squeeze: from full at a 40rem bar to the
368
+ minimum at 20rem, 1px of padding per 80px of bar and 1px of tracking
369
+ per 300px. Then the balance: inline items in a block, so text-wrap can
370
+ even the rows (4: 2 + 2, 3: 2 + 1, 5: 3 + 2). The link padding is the
371
+ gap, so the spaces between items are zeroed (font-size 0). Without
372
+ text-wrap: balance the links wrap as before. */
373
+ .nav:not(:has(.nav__toggle)) .nav__menu {
374
+ --_nav-pad: clamp(var(--nav-gap-min, var(--space-2)),
375
+ calc(var(--nav-gap-min, var(--space-2)) + (100cqi - 20rem) / 80),
376
+ var(--nav-gap, var(--space-3)));
377
+ --_nav-track: clamp(var(--nav-tracking-min, var(--nav-tracking, 0em)),
378
+ calc(var(--nav-tracking-min, var(--nav-tracking, 0em)) + (100cqi - 20rem) / 300),
379
+ var(--nav-tracking, 0em));
380
+ display: block;
381
+ flex-basis: 100%;
382
+ order: 3;
383
+ margin-inline: calc(-1 * var(--_nav-pad));
384
+ font-size: 0;
385
+ /* No fallback: unset, the declaration is invalid at computed-value
386
+ time and text-align inherits, so the rows follow the page. */
387
+ text-align: var(--nav-wrap-align);
388
+ text-wrap: balance;
389
+ }
390
+
391
+ .nav:not(:has(.nav__toggle)) .nav__menu > li {
392
+ display: inline-flex;
393
+ vertical-align: top;
394
+ }
395
+
396
+ /* With a toggle, the brand takes the row beside it and wraps. */
397
+ .nav__brand:has(~ .nav__toggle) {
398
+ flex: 1 1 0;
399
+ min-inline-size: 0;
400
+ }
401
+
402
+ /* Top row: brand, CTA (order 1), toggle (order 2); the menu is the
403
+ next row (order 3). */
370
404
  .nav__toggle {
371
405
  display: flex;
372
406
  margin-inline-start: auto;
373
- order: 1;
407
+ order: 2;
374
408
  }
375
409
 
376
410
  .nav__theme-toggle {
@@ -378,15 +412,11 @@
378
412
  margin-inline-start: auto;
379
413
  }
380
414
 
381
- /* Mobile menu with smooth reveal animation */
382
- .nav__menu {
415
+ .nav:has(.nav__toggle) .nav__menu {
383
416
  flex-basis: 100%;
384
417
  flex-direction: column;
385
418
  align-items: stretch;
386
- gap: var(--space-1);
387
- padding-block-start: var(--space-4);
388
- order: 2;
389
- /* Hidden state */
419
+ order: 3;
390
420
  display: flex;
391
421
  visibility: hidden;
392
422
  opacity: 0;
@@ -398,79 +428,39 @@
398
428
  max-block-size var(--duration-normal) var(--ease-fluid);
399
429
  }
400
430
 
401
- /* Show menu when toggle is open */
402
- .nav__menu[data-open],
431
+ .nav .nav__menu[data-open],
403
432
  .nav:has(.nav__toggle[aria-expanded="true"]) .nav__menu {
433
+ padding-block-start: var(--space-2);
404
434
  visibility: visible;
405
435
  opacity: 1;
406
- max-block-size: 100vh; /* Large enough for any menu */
436
+ max-block-size: 100vh;
407
437
  }
408
438
 
409
- .nav__menu > li {
410
- display: block;
411
- opacity: 0;
412
- transform: translateY(-0.5rem);
413
- transition:
414
- opacity var(--duration-fast) var(--ease-fluid),
415
- transform var(--duration-fast) var(--ease-settle);
416
- }
417
-
418
- /* Animate items in when menu opens */
419
- .nav__menu[data-open] > li,
420
- .nav:has(.nav__toggle[aria-expanded="true"]) .nav__menu > li {
421
- opacity: 1;
422
- transform: translateY(0);
423
- }
424
-
425
- /* Stagger the item animations */
426
- .nav__menu > li:nth-child(1) { transition-delay: 50ms; }
427
- .nav__menu > li:nth-child(2) { transition-delay: 100ms; }
428
- .nav__menu > li:nth-child(3) { transition-delay: 150ms; }
429
- .nav__menu > li:nth-child(4) { transition-delay: 200ms; }
430
- .nav__menu > li:nth-child(5) { transition-delay: 250ms; }
431
- .nav__menu > li:nth-child(6) { transition-delay: 300ms; }
432
-
433
- .nav__link {
439
+ .nav:has(.nav__toggle) .nav__menu > li {
434
440
  display: block;
435
- padding: var(--space-3);
441
+ border-block-start: var(--border-width) solid var(--color-border);
436
442
  }
437
443
 
438
- /* Mobile dropdowns - always visible, stacked */
439
444
  .nav__dropdown-menu {
440
445
  position: static;
441
446
  opacity: 1;
442
447
  visibility: visible;
443
- transform: none;
444
- box-shadow: none;
445
- border: none;
448
+ border: 0;
446
449
  padding-inline-start: var(--space-4);
447
- margin-block-start: 0;
448
450
  background-color: transparent;
449
451
  }
450
452
 
451
- .nav__dropdown-trigger svg {
452
- display: none;
453
- }
453
+ .nav__dropdown-trigger svg { display: none; }
454
454
 
455
- .nav__dropdown-trigger {
456
- padding: var(--space-3);
457
- font-weight: var(--weight-semibold);
458
- color: var(--color-text);
459
- }
455
+ .nav:has(.nav__actions) .nav__toggle { margin-inline-start: 0; }
456
+ }
460
457
 
461
- .nav__cta {
462
- margin-block-start: var(--space-3);
463
- justify-content: center;
464
- }
465
458
 
466
- .nav__actions {
467
- flex-basis: 100%;
468
- padding-block-start: var(--space-4);
469
- border-block-start: 1px solid var(--color-border-subtle);
470
- margin-block-start: var(--space-4);
471
- order: 3;
472
- }
473
- }
459
+ /* The data-nav="compact" markup (fluid-nav.min.css) is safe without that
460
+ file: the panel wrapper is invisible to layout and its buttons hidden,
461
+ so the links show as usual. */
462
+ .nav__panel { display: contents; }
463
+ .nav__more { display: none; }
474
464
 
475
465
 
476
466
  /* ========================================
@@ -485,13 +475,14 @@
485
475
  padding: 0;
486
476
  margin: 0;
487
477
  list-style: none;
488
- font-size: var(--font-size-sm);
478
+ font-size: var(--step--1);
489
479
  }
490
480
 
491
481
  .breadcrumb__item {
492
482
  display: inline-flex;
493
483
  align-items: center;
494
484
  gap: var(--space-2);
485
+ margin: 0;
495
486
  line-height: 1;
496
487
  }
497
488
 
@@ -500,21 +491,19 @@
500
491
  color: var(--color-text-subtle);
501
492
  }
502
493
 
503
- .breadcrumb__item:last-child::after {
504
- content: none;
505
- }
494
+ .breadcrumb__item:last-child::after { content: none; }
506
495
 
507
496
  .breadcrumb__link {
508
497
  display: inline-flex;
509
498
  align-items: center;
510
499
  color: var(--color-text-muted);
511
- text-decoration: none;
500
+ text-decoration-line: none;
512
501
  line-height: 1;
513
- transition: color var(--duration-normal) var(--ease-fluid);
514
502
  }
515
503
 
516
504
  .breadcrumb__link:hover {
517
- color: var(--accent);
505
+ color: var(--color-text);
506
+ text-decoration-line: underline;
518
507
  }
519
508
 
520
509
  .breadcrumb__item:last-child .breadcrumb__link,
@@ -525,32 +514,22 @@
525
514
 
526
515
  /* Mobile: collapse to "← Back to [Parent]" */
527
516
  @media (max-width: 40rem) {
528
- .breadcrumb .breadcrumb__item {
529
- display: none;
530
- }
531
-
532
- /* Show only the second-to-last item (parent) as a back link */
533
- .breadcrumb .breadcrumb__item:nth-last-child(2) {
534
- display: inline-flex;
535
- }
517
+ .breadcrumb .breadcrumb__item { display: none; }
518
+ .breadcrumb .breadcrumb__item:nth-last-child(2) { display: inline-flex; }
536
519
 
537
520
  .breadcrumb .breadcrumb__item:nth-last-child(2)::before {
538
521
  content: "← ";
539
522
  color: var(--color-text-muted);
540
523
  }
541
524
 
542
- .breadcrumb .breadcrumb__item:nth-last-child(2)::after {
543
- content: none;
544
- }
525
+ .breadcrumb .breadcrumb__item:nth-last-child(2)::after { content: none; }
545
526
 
546
527
  .breadcrumb .breadcrumb__item:nth-last-child(2) .breadcrumb__link {
547
528
  color: var(--color-text-muted);
548
529
  pointer-events: auto;
549
530
  }
550
531
 
551
- .breadcrumb .breadcrumb__item:nth-last-child(2) .breadcrumb__link::before {
552
- content: "Back to ";
553
- }
532
+ .breadcrumb .breadcrumb__item:nth-last-child(2) .breadcrumb__link::before { content: "Back to "; }
554
533
  }
555
534
 
556
535
 
@@ -567,32 +546,35 @@
567
546
  list-style: none;
568
547
  }
569
548
 
549
+ .pagination > li { margin: 0; }
550
+
570
551
  .pagination__link {
571
552
  display: inline-flex;
572
553
  align-items: center;
573
554
  justify-content: center;
574
- min-inline-size: 2.5rem;
575
- min-block-size: 2.5rem;
555
+ gap: var(--space-2);
556
+ min-inline-size: 2.75rem;
557
+ min-block-size: 2.75rem;
576
558
  padding: var(--space-2);
577
- font-size: var(--font-size-sm);
578
- font-weight: var(--weight-medium);
559
+ font-size: var(--step--1);
560
+ font-weight: calc(500 + var(--_dark-weight));
561
+ font-variant-numeric: lining-nums tabular-nums;
579
562
  color: var(--color-text-muted);
580
- text-decoration: none;
581
- border-radius: var(--radius-md);
582
- transition:
583
- color var(--duration-normal) var(--ease-fluid),
584
- background-color var(--duration-normal) var(--ease-fluid);
563
+ text-decoration-line: none;
564
+ text-underline-offset: 0.4em;
565
+ border-radius: var(--radius-control);
585
566
  }
586
567
 
587
568
  .pagination__link:hover {
588
569
  color: var(--color-text);
589
- background-color: var(--color-surface);
570
+ text-decoration-line: underline;
590
571
  }
591
572
 
592
- .pagination__link[aria-current="page"],
593
- .pagination__link[data-active] {
594
- color: var(--white);
595
- background-color: var(--accent);
573
+ .pagination__link:is([aria-current="page"], [data-active]) {
574
+ color: var(--color-text);
575
+ text-decoration-line: underline;
576
+ text-decoration-thickness: 2px;
577
+ text-decoration-color: var(--accent-ink);
596
578
  }
597
579
 
598
580
  .pagination__link[data-disabled] {
@@ -600,12 +582,6 @@
600
582
  pointer-events: none;
601
583
  }
602
584
 
603
- /* Prev/Next buttons */
604
- .pagination__link[data-prev],
605
- .pagination__link[data-next] {
606
- gap: var(--space-2);
607
- }
608
-
609
585
  .pagination__ellipsis {
610
586
  padding: var(--space-2);
611
587
  color: var(--color-text-subtle);
@@ -618,79 +594,59 @@
618
594
 
619
595
  .tabs {
620
596
  display: flex;
621
- gap: var(--space-1);
622
- border-block-end: 1px solid var(--color-border-subtle);
597
+ gap: var(--space-m);
598
+ border-block-end: var(--border-width) solid var(--color-border);
623
599
  }
624
600
 
625
601
  .tabs__tab {
626
602
  position: relative;
627
- padding: var(--space-3) var(--space-4);
628
- font-size: var(--font-size-sm);
629
- font-weight: var(--weight-medium);
603
+ min-block-size: 2.75rem;
604
+ padding: var(--space-2) 0;
605
+ font-family: var(--font-body);
606
+ font-size: var(--step--1);
607
+ font-weight: calc(500 + var(--_dark-weight));
630
608
  color: var(--color-text-muted);
631
609
  text-decoration: none;
632
610
  background: none;
633
- border: none;
634
- border-radius: var(--radius-md);
611
+ border: 0;
612
+ border-radius: 0;
635
613
  cursor: pointer;
636
- outline: 2px solid transparent;
637
- transition:
638
- color var(--duration-normal) var(--ease-fluid),
639
- background-color var(--duration-normal) var(--ease-fluid);
614
+ transition: color var(--duration-fast) var(--ease-fluid);
640
615
  }
641
616
 
642
- /* Focus state - subtle ring */
643
617
  .tabs__tab:focus-visible {
644
- background-color: var(--color-surface);
645
- box-shadow: 0 0 0 2px var(--accent-subtle);
618
+ outline: var(--focus-ring-width) solid var(--color-focus);
619
+ outline-offset: 2px;
646
620
  }
647
621
 
648
622
  .tabs__tab::after {
649
623
  content: "";
650
624
  position: absolute;
651
- inset-inline: var(--space-2); /* Inset from edges for rounded look */
625
+ inset-inline: 0;
652
626
  inset-block-end: -1px;
653
- block-size: 3px; /* Slightly thicker for visible rounded ends */
627
+ block-size: 2px;
654
628
  background-color: transparent;
655
- border-radius: var(--radius-full);
656
- transition: background-color var(--duration-normal) var(--ease-fluid);
657
629
  }
658
630
 
659
- .tabs__tab:hover {
660
- color: var(--color-text);
661
- background-color: var(--color-surface);
662
- }
631
+ .tabs__tab:hover { color: var(--color-text); background: none; }
663
632
 
664
- .tabs__tab[aria-selected="true"],
665
- .tabs__tab[data-active] {
666
- color: var(--accent);
667
- }
633
+ .tabs__tab:is([aria-selected="true"], [data-active]) { color: var(--color-text); }
668
634
 
669
- .tabs__tab[aria-selected="true"]::after,
670
- .tabs__tab[data-active]::after {
671
- background-color: var(--accent);
672
- }
635
+ .tabs__tab:is([aria-selected="true"], [data-active])::after { background-color: var(--accent-ink); }
673
636
 
674
- /* Contained tabs variant */
637
+ /* Contained: a segmented control on the surface tint. */
675
638
  .tabs[data-variant="contained"] {
639
+ gap: 0;
676
640
  background-color: var(--color-surface);
677
- border-radius: var(--radius-md);
678
- border: none;
641
+ border: 0;
679
642
  padding: var(--space-1);
680
643
  }
681
644
 
682
- .tabs[data-variant="contained"] .tabs__tab {
683
- border-radius: var(--radius-sm);
684
- }
685
-
686
- .tabs[data-variant="contained"] .tabs__tab::after {
687
- display: none;
688
- }
645
+ .tabs[data-variant="contained"] .tabs__tab { padding-inline: var(--space-4); }
646
+ .tabs[data-variant="contained"] .tabs__tab::after { display: none; }
689
647
 
690
- .tabs[data-variant="contained"] .tabs__tab[aria-selected="true"],
691
- .tabs[data-variant="contained"] .tabs__tab[data-active] {
648
+ .tabs[data-variant="contained"] .tabs__tab:is([aria-selected="true"], [data-active]) {
692
649
  background-color: var(--color-background);
693
- box-shadow: var(--shadow-xs);
694
650
  }
695
651
 
696
652
 
@@ -698,29 +654,5 @@
698
654
  REDUCED MOTION
699
655
  ======================================== */
700
656
 
701
- @media (prefers-reduced-motion: reduce) {
702
- .nav__menu > li {
703
- transition: none;
704
- opacity: 1;
705
- transform: none;
706
- }
707
-
708
- .nav__menu > li:nth-child(1),
709
- .nav__menu > li:nth-child(2),
710
- .nav__menu > li:nth-child(3),
711
- .nav__menu > li:nth-child(4),
712
- .nav__menu > li:nth-child(5),
713
- .nav__menu > li:nth-child(6) {
714
- transition-delay: 0ms;
715
- }
716
-
717
- .nav__link,
718
- .nav__dropdown-menu,
719
- .breadcrumb__link,
720
- .pagination__link,
721
- .tabs__tab,
722
- .tabs__tab::after {
723
- transition: none;
724
- }
725
- }
657
+ /* Reduced motion: the reset shortens every transition. */
726
658
  }