@supertype.ai/foundations 0.1.24

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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +369 -0
  3. package/bin/foundations.mjs +713 -0
  4. package/dist/blocks/accordion.d.ts +23 -0
  5. package/dist/blocks/accordion.js +59 -0
  6. package/dist/blocks/callout.d.ts +57 -0
  7. package/dist/blocks/callout.js +61 -0
  8. package/dist/blocks/card.d.ts +34 -0
  9. package/dist/blocks/card.js +56 -0
  10. package/dist/blocks/index.d.ts +7 -0
  11. package/dist/blocks/index.js +7 -0
  12. package/dist/blocks/interactive-accordion.d.ts +13 -0
  13. package/dist/blocks/interactive-accordion.js +27 -0
  14. package/dist/blocks/segment.d.ts +37 -0
  15. package/dist/blocks/segment.js +37 -0
  16. package/dist/blocks/steps.d.ts +10 -0
  17. package/dist/blocks/steps.js +13 -0
  18. package/dist/blocks/tabs.d.ts +32 -0
  19. package/dist/blocks/tabs.js +69 -0
  20. package/dist/cjs/eslint.js +146 -0
  21. package/dist/cjs/package.json +3 -0
  22. package/dist/cn.d.ts +2 -0
  23. package/dist/cn.js +5 -0
  24. package/dist/contrast.d.ts +47 -0
  25. package/dist/contrast.js +255 -0
  26. package/dist/eslint.d.ts +74 -0
  27. package/dist/eslint.js +138 -0
  28. package/dist/essay/contents.d.ts +10 -0
  29. package/dist/essay/contents.js +17 -0
  30. package/dist/essay/essay.d.ts +125 -0
  31. package/dist/essay/essay.js +92 -0
  32. package/dist/essay/index.d.ts +7 -0
  33. package/dist/essay/index.js +9 -0
  34. package/dist/essay/layout.d.ts +72 -0
  35. package/dist/essay/layout.js +77 -0
  36. package/dist/essay/rail.d.ts +15 -0
  37. package/dist/essay/rail.js +26 -0
  38. package/dist/essay/reading.d.ts +17 -0
  39. package/dist/essay/reading.js +31 -0
  40. package/dist/essay/scroll.d.ts +8 -0
  41. package/dist/essay/scroll.js +78 -0
  42. package/dist/essay/toc.d.ts +23 -0
  43. package/dist/essay/toc.js +50 -0
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.js +33 -0
  46. package/dist/injection.d.ts +8 -0
  47. package/dist/injection.js +1 -0
  48. package/dist/mdx.d.ts +47 -0
  49. package/dist/mdx.js +68 -0
  50. package/dist/og.d.ts +18 -0
  51. package/dist/og.js +50 -0
  52. package/dist/rehype.d.ts +18 -0
  53. package/dist/rehype.js +41 -0
  54. package/dist/seo.d.ts +174 -0
  55. package/dist/seo.js +152 -0
  56. package/dist/typography/as.d.ts +15 -0
  57. package/dist/typography/as.js +8 -0
  58. package/dist/typography/header.d.ts +44 -0
  59. package/dist/typography/header.js +119 -0
  60. package/dist/typography/highlight.d.ts +33 -0
  61. package/dist/typography/highlight.js +98 -0
  62. package/dist/typography/index.d.ts +4 -0
  63. package/dist/typography/index.js +3 -0
  64. package/dist/typography/paragraph.d.ts +157 -0
  65. package/dist/typography/paragraph.js +229 -0
  66. package/llms.txt +125 -0
  67. package/package.json +140 -0
  68. package/src/prose.css +12 -0
  69. package/src/shiki.css +23 -0
  70. package/src/theme.css +272 -0
  71. package/src/tokens.css +43 -0
  72. package/src/type.css +73 -0
package/src/shiki.css ADDED
@@ -0,0 +1,23 @@
1
+ /* Picks which of `--shiki-light` / `--shiki-dark` wins. Keyed off the inline
2
+ custom properties, never a class: a host is free to swap in its own <pre> or
3
+ <code> and drop `.shiki` with it, but the token spans always survive.
4
+ Import after tokens.css and after any host's own code CSS. */
5
+ [style*="--shiki-light"] {
6
+ color: var(--shiki-light);
7
+ font-style: var(--shiki-light-font-style, inherit);
8
+ font-weight: var(--shiki-light-font-weight, inherit);
9
+ }
10
+
11
+ .dark [style*="--shiki-dark"] {
12
+ color: var(--shiki-dark);
13
+ font-style: var(--shiki-dark-font-style, inherit);
14
+ font-weight: var(--shiki-dark-font-weight, inherit);
15
+ }
16
+
17
+ [style*="--shiki-light-bg"] {
18
+ background-color: var(--shiki-light-bg);
19
+ }
20
+
21
+ .dark [style*="--shiki-dark-bg"] {
22
+ background-color: var(--shiki-dark-bg);
23
+ }
package/src/theme.css ADDED
@@ -0,0 +1,272 @@
1
+ /* The latte/espresso palette. tokens.css names these roles; this is the only
2
+ place they get values. Unlayered on purpose: inside a layer, this file's :root
3
+ would tie with a .dark on specificity and light would win in both themes. */
4
+
5
+ @theme inline {
6
+ /* Beyond what tokens.css already registers. */
7
+ --color-tint-foreground: var(--tint-foreground);
8
+
9
+ /* Tertiary ink: placeholders, disabled labels, the "…" in a truncation. Held
10
+ to 3:1 rather than 4.5:1, so nothing load-bearing may be written in it. */
11
+ --color-subtle-foreground: var(--subtle-foreground);
12
+
13
+ /* The accent as text; --secondary is the fill and --secondary-foreground the
14
+ label printed on it. */
15
+ --color-secondary-ink: var(--secondary-ink);
16
+
17
+ /* Identity and kind, never state. A pale wash of one hue is identical to any
18
+ other's, so the ink is what tells two swatches apart. checkSignals holds the
19
+ fill to 3:1 and the ink to 4.5:1. */
20
+ --color-terracotta: var(--terracotta);
21
+ --color-terracotta-foreground: var(--terracotta-foreground);
22
+ --color-ochre: var(--ochre);
23
+ --color-ochre-foreground: var(--ochre-foreground);
24
+ --color-moss: var(--moss);
25
+ --color-moss-foreground: var(--moss-foreground);
26
+ --color-fern: var(--fern);
27
+ --color-fern-foreground: var(--fern-foreground);
28
+ --color-sage: var(--sage);
29
+ --color-sage-foreground: var(--sage-foreground);
30
+ --color-stone: var(--stone);
31
+ --color-stone-foreground: var(--stone-foreground);
32
+ --color-fig: var(--fig);
33
+ --color-fig-foreground: var(--fig-foreground);
34
+ --color-cocoa: var(--cocoa);
35
+ --color-cocoa-foreground: var(--cocoa-foreground);
36
+
37
+ --color-chart-1: var(--chart-1);
38
+ --color-chart-2: var(--chart-2);
39
+ --color-chart-3: var(--chart-3);
40
+ --color-chart-4: var(--chart-4);
41
+ --color-chart-5: var(--chart-5);
42
+ --color-chart-6: var(--chart-6);
43
+
44
+ --color-sidebar: var(--sidebar);
45
+ --color-sidebar-foreground: var(--sidebar-foreground);
46
+ --color-sidebar-primary: var(--sidebar-primary);
47
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
48
+ --color-sidebar-accent: var(--sidebar-accent);
49
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
50
+ --color-sidebar-border: var(--sidebar-border);
51
+ --color-sidebar-ring: var(--sidebar-ring);
52
+
53
+ /* Only layers that leave the page plane cast a shadow. `recessed` is the one
54
+ inward shadow: a surface things sit on, not one floating above the page. */
55
+ --shadow-raised: var(--elevation-raised);
56
+ --shadow-overlay: var(--elevation-overlay);
57
+ --shadow-recessed: var(--elevation-recessed);
58
+
59
+ /* Additive scale off a 10px `lg` — fixed pixel offsets, so no oddball values a
60
+ multiplicative scale would produce. `rounded-full` stays a pill. */
61
+ --radius-sm: calc(var(--radius) - 4px); /* 6px */
62
+ --radius-md: calc(var(--radius) - 2px); /* 8px — inputs, menus, small controls */
63
+ --radius-lg: var(--radius); /* 10px — buttons, default control */
64
+ --radius-xl: calc(var(--radius) + 4px); /* 14px — cards */
65
+ --radius-2xl: calc(var(--radius) + 8px); /* 18px — marketing feature cards */
66
+ --radius-3xl: calc(var(--radius) + 12px); /* 22px */
67
+ --radius-4xl: calc(var(--radius) + 16px); /* 26px */
68
+ }
69
+
70
+ :root {
71
+ /* Ink for any tinted surface. Does NOT flip with the theme: a tint is tinted in
72
+ both, so its label stays light in both. */
73
+ --tint-foreground: hsl(0, 0%, 100%);
74
+
75
+
76
+ /* Status, on the same two-token rule as the categorical set below: the fill is
77
+ a mark (3:1), the ink is the same hue as words (4.5:1). Nothing prints a
78
+ label on a status fill — orange cannot carry white text at any lightness. */
79
+ --success: hsl(142 75% 31%);
80
+ --success-ink: hsl(142 68% 28%);
81
+ /* Was 52%, which measured 2.27:1 on latte — a dot you could not see. */
82
+ --warn: hsl(30 100% 43%);
83
+ --warn-ink: hsl(22 85% 36%);
84
+ /* Deprecated: --warn-foreground was always this ink, under shadcn's name for
85
+ the opposite thing. Use --warn-ink; the ESLint rules flag the old name. */
86
+ --warn-foreground: var(--warn-ink);
87
+ --info: hsl(211 80% 43%);
88
+ --info-ink: hsl(211 80% 40%);
89
+ /* Danger ships as an ink only: a vivid brick red harmonised toward terracotta. */
90
+ --danger: hsl(5 72% 43%);
91
+
92
+ /* Latte-warm greens, clays and stone. Both cuts of each hue, light theme. */
93
+ --terracotta: hsl(10 54% 60%);
94
+ --terracotta-foreground: hsl(10 72% 40%);
95
+ --ochre: hsl(44 60% 42%);
96
+ --ochre-foreground: hsl(44 78% 27%);
97
+ --moss: hsl(68 42% 41%);
98
+ --moss-foreground: hsl(68 60% 26%);
99
+ --fern: hsl(133 30% 47%);
100
+ --fern-foreground: hsl(133 48% 30%);
101
+ --sage: hsl(168 28% 47%);
102
+ --sage-foreground: hsl(168 46% 29%);
103
+ --stone: hsl(202 24% 55%);
104
+ --stone-foreground: hsl(202 42% 36%);
105
+ --fig: hsl(332 32% 61%);
106
+ --fig-foreground: hsl(332 50% 43%);
107
+ --cocoa: hsl(20 16% 56%);
108
+ --cocoa-foreground: hsl(20 34% 39%);
109
+
110
+ --background: hsl(43 33% 96%); /* #FAF8F3 */
111
+ /* Near-black keyed to the latte hue, not a neutral #333: grey reads cold over
112
+ surfaces hued at 40-43°, and the APCA headroom keeps three ink tiers apart. */
113
+ --foreground: hsl(30 14% 12%);
114
+ --card: hsl(0 0% 100%);
115
+ --card-foreground: hsl(30 14% 12%);
116
+ --popover: hsl(0 0% 100%);
117
+ --popover-foreground: hsl(30 14% 12%);
118
+ --primary: hsl(133 11% 33%); /* #4A5D4E */
119
+ --primary-foreground: hsl(0 0% 100%);
120
+ /* Three tokens, because the accent pulls opposite ways: fill, the label on it,
121
+ and the accent as text. Tuning any one for another sinks the third. */
122
+ --secondary: hsl(14, 67%, 72%);
123
+ --secondary-foreground: hsl(14, 69%, 14%);
124
+ --secondary-ink: hsl(14 67% 38%);
125
+ --muted: hsl(40 20% 92%); /* #EBE7E0 */
126
+ /* Warm-keyed, and set by APCA rather than WCAG: 41% cleared AA at 72.5 Lc, and
127
+ the dominant pairing is a 12px rung where the 75 floor must be beaten. */
128
+ --muted-foreground: hsl(30 8% 34%);
129
+ --subtle-foreground: hsl(0 0% 52%);
130
+ --accent: hsl(133 15% 90%); /* the hover tint */
131
+ --accent-foreground: hsl(133 11% 33%);
132
+ --destructive: hsl(0 50% 36%);
133
+ --destructive-foreground: hsl(0 0% 98%);
134
+ /* Soft beige-grey, to keep a rule un-intrusive. */
135
+ --border: hsl(40 15% 88%); /* #E3DFD8 */
136
+ --input: var(--border);
137
+ --ring: hsl(133 11% 33%); /* the brand green */
138
+ --chart-1: hsl(133 18% 45%);
139
+ --chart-2: hsl(14 47% 59%);
140
+ --chart-3: hsl(43 33% 60%);
141
+ --chart-4: hsl(200 20% 50%);
142
+ --chart-5: hsl(30 12% 40%);
143
+ /* A muted teal: the one cool hue the warm set leaves open, and far enough from
144
+ the slate at 200 to read as its own colour. */
145
+ --chart-6: hsl(178 35% 40%);
146
+ --radius: 0.625rem; /* 10px */
147
+ /* Keyed off the espresso foreground hue, not pure black, so a shadow sits
148
+ naturally on a latte surface. */
149
+ --elevation-raised:
150
+ 0 1px 2px -1px hsl(30 18% 12% / 0.1), 0 4px 12px -2px hsl(30 18% 12% / 0.1);
151
+ --elevation-overlay:
152
+ 0 6px 16px -4px hsl(30 18% 12% / 0.14),
153
+ 0 16px 40px -12px hsl(30 18% 12% / 0.22);
154
+ --elevation-recessed:
155
+ inset 0 1px 3px hsl(30 18% 12% / 0.07),
156
+ inset 0 0 0 1px hsl(30 18% 12% / 0.03);
157
+ --sidebar: hsl(40 25% 94%); /* #F5F2EC */
158
+ --sidebar-foreground: hsl(0 0% 25%);
159
+ --sidebar-primary: hsl(133 11% 33%);
160
+ --sidebar-primary-foreground: hsl(0 0% 100%);
161
+ --sidebar-accent: hsl(133 15% 88%);
162
+ --sidebar-accent-foreground: hsl(133 11% 25%);
163
+ --sidebar-border: hsl(40 15% 88%);
164
+ --sidebar-ring: hsl(133 11% 33%);
165
+ }
166
+
167
+ .dark {
168
+ /* --success does not flip: the same green clears 3:1 on espresso as on latte. */
169
+ --success-ink: hsl(142 55% 58%);
170
+ --warn: hsl(30 90% 55%);
171
+ --warn-ink: hsl(33 60% 72%);
172
+ --warn-foreground: var(--warn-ink);
173
+ --info: hsl(211 75% 60%);
174
+ --info-ink: hsl(211 75% 64%);
175
+ /* Soft terracotta-red. The light cut measured ~2.9:1 on espresso and read muddy. */
176
+ --danger: hsl(8 60% 64%);
177
+
178
+ /* Espresso twins: fills darken and inks lift to hold the same two bars. */
179
+ --terracotta: hsl(10 54% 47%);
180
+ --terracotta-foreground: hsl(10 66% 66%);
181
+ --ochre: hsl(44 60% 33%);
182
+ --ochre-foreground: hsl(44 72% 45%);
183
+ --moss: hsl(68 42% 32%);
184
+ --moss-foreground: hsl(68 54% 43%);
185
+ --fern: hsl(133 30% 37%);
186
+ --fern-foreground: hsl(133 42% 49%);
187
+ --sage: hsl(168 28% 37%);
188
+ --sage-foreground: hsl(168 40% 49%);
189
+ --stone: hsl(202 24% 42%);
190
+ --stone-foreground: hsl(202 36% 60%);
191
+ --fig: hsl(332 32% 49%);
192
+ --fig-foreground: hsl(332 44% 67%);
193
+ --cocoa: hsl(20 16% 44%);
194
+ --cocoa-foreground: hsl(20 28% 62%);
195
+
196
+ /* 4 points of lightness at L10 reads flat where the same 4 at L96 reads raised;
197
+ ssite wrote `bg-card dark:bg-muted` at 109 sites before the ground dropped.
198
+ The lift goes here: --card cannot rise without sinking terracotta's fill. */
199
+ --background: hsl(30 10% 7%);
200
+ --foreground: hsl(43 20% 92%);
201
+ /* Unchanged, and now 7 points above the ground rather than 4 — what those 109
202
+ sites were reaching for, without moving anything measured against --card. */
203
+ --card: hsl(30 8% 14%);
204
+ --card-foreground: hsl(43 20% 92%);
205
+ --popover: hsl(30 8% 14%);
206
+ --popover-foreground: hsl(43 20% 92%);
207
+ --primary: hsl(133 18% 58%); /* #7A9E7E */
208
+ --primary-foreground: hsl(30 10% 13%);
209
+ /* Espresso twin: the fill darkens under a white label, the ink lifts as text. */
210
+ --secondary: hsl(14, 64%, 37%);
211
+ --secondary-foreground: hsl(0 0% 100%);
212
+ --secondary-ink: hsl(14 60% 66%);
213
+ --muted: hsl(30 8% 18%);
214
+ /* The clearest case here of APCA and WCAG disagreeing: 62% passed WCAG at 7.06
215
+ and measured 52.1 Lc. Stops at 80, or the step below --foreground closes. */
216
+ --muted-foreground: hsl(43 16% 80%);
217
+ /* Raised alongside the secondary so the gap survives; 50% sat a hair over
218
+ where text stops resolving. */
219
+ --subtle-foreground: hsl(43 10% 59%);
220
+ --accent: hsl(133 12% 20%);
221
+ --accent-foreground: hsl(133 18% 78%);
222
+ /* Overwhelmingly an ink: sixty `text-destructive` sites, no fill carrying text.
223
+ The light value measured 2.08:1 here — a colour, not words. */
224
+ --destructive: hsl(0 65% 67%);
225
+ /* The ink is light here, so anything printed on it must be dark. Nothing does
226
+ today; white would arm a 1.3:1 trap for whoever tries first. */
227
+ --destructive-foreground: hsl(30 10% 10%);
228
+ /* L22 gave a 1.34:1 hairline on --card, which is where the
229
+ `dark:border-white/[0.2]` patches came from. L26 stops short of a rule. */
230
+ --border: hsl(30 8% 26%);
231
+ --input: var(--border);
232
+ --ring: hsl(133 18% 58%);
233
+ --chart-1: hsl(133 18% 58%);
234
+ --chart-2: hsl(14 45% 55%);
235
+ --chart-3: hsl(43 20% 70%);
236
+ --chart-4: hsl(200 15% 55%);
237
+ --chart-5: hsl(30 8% 35%);
238
+ --chart-6: hsl(178 30% 52%);
239
+ /* A warm tint reads as haze on espresso: deep black, more spread. */
240
+ --elevation-raised:
241
+ 0 1px 2px -1px hsl(0 0% 0% / 0.4), 0 4px 12px -2px hsl(0 0% 0% / 0.45);
242
+ --elevation-overlay:
243
+ 0 6px 16px -4px hsl(0 0% 0% / 0.5), 0 16px 40px -12px hsl(0 0% 0% / 0.6);
244
+ /* A recess in the dark is a well, not a stain: a hairline of light catches
245
+ the bottom lip to sell the depth. */
246
+ --elevation-recessed:
247
+ inset 0 2px 5px hsl(0 0% 0% / 0.4), inset 0 -1px 0 hsl(43 20% 92% / 0.04);
248
+ --sidebar: hsl(30 10% 13%);
249
+ --sidebar-foreground: hsl(43 20% 90%);
250
+ --sidebar-primary: hsl(133 18% 58%);
251
+ --sidebar-primary-foreground: hsl(133 20% 10%);
252
+ --sidebar-accent: hsl(30 8% 19%);
253
+ --sidebar-accent-foreground: hsl(43 20% 85%);
254
+ --sidebar-border: hsl(30 8% 20%);
255
+ --sidebar-ring: hsl(133 18% 58%);
256
+ }
257
+
258
+ /* The accordion panel's open/close, so the component does not depend on a
259
+ consumer having imported tw-animate-css. Base UI publishes the measured
260
+ height as --accordion-panel-height on the panel. */
261
+ @theme {
262
+ --animate-accordion-down: accordion-down 0.2s ease-out;
263
+ --animate-accordion-up: accordion-up 0.2s ease-out;
264
+ @keyframes accordion-down {
265
+ from { height: 0; }
266
+ to { height: var(--accordion-panel-height, auto); }
267
+ }
268
+ @keyframes accordion-up {
269
+ from { height: var(--accordion-panel-height, auto); }
270
+ to { height: 0; }
271
+ }
272
+ }
package/src/tokens.css ADDED
@@ -0,0 +1,43 @@
1
+ /* The structural roles, and nothing else. This file names them and binds the
2
+ dark variant; theme.css assigns every one a value. An app that skips theme.css
3
+ must declare all of them itself — `foundations doctor` fails if neither holds. */
4
+
5
+ /* Binds `dark:` to the `.dark` class. Without it Tailwind v4 follows the OS
6
+ setting and ignores the app's toggle, silently. */
7
+ @custom-variant dark (&:where(.dark, .dark *));
8
+
9
+ @theme inline {
10
+ --color-background: var(--background);
11
+ --color-foreground: var(--foreground);
12
+ --color-card: var(--card);
13
+ --color-card-foreground: var(--card-foreground);
14
+ --color-popover: var(--popover);
15
+ --color-popover-foreground: var(--popover-foreground);
16
+ --color-primary: var(--primary);
17
+ --color-primary-foreground: var(--primary-foreground);
18
+ --color-secondary: var(--secondary);
19
+ --color-secondary-foreground: var(--secondary-foreground);
20
+ --color-muted: var(--muted);
21
+ --color-muted-foreground: var(--muted-foreground);
22
+ --color-accent: var(--accent);
23
+ --color-accent-foreground: var(--accent-foreground);
24
+ --color-destructive: var(--destructive);
25
+ --color-destructive-foreground: var(--destructive-foreground);
26
+
27
+ /* Named for meaning, not hue: a `success` a project renders blue still reads
28
+ right. Each status hue ships twice — the fill is a mark, the ink is words. */
29
+ --color-success: var(--success);
30
+ --color-success-ink: var(--success-ink);
31
+ --color-warn: var(--warn);
32
+ --color-warn-ink: var(--warn-ink);
33
+ --color-info: var(--info);
34
+ --color-info-ink: var(--info-ink);
35
+ --color-danger: var(--danger);
36
+
37
+ /* Deprecated: this was always an ink. Use `warn-ink`. */
38
+ --color-warn-foreground: var(--warn-foreground);
39
+
40
+ --color-border: var(--border);
41
+ --color-input: var(--input);
42
+ --color-ring: var(--ring);
43
+ }
package/src/type.css ADDED
@@ -0,0 +1,73 @@
1
+ /* Editorial ramp: Tailwind's default sizes, untouched. Density is a product
2
+ concern — viably runs its own dense ramp and opts marketing back out to these. */
3
+
4
+ @theme {
5
+ /* Below Tailwind's 12px floor. Without them these get written as `text-[11px]`. */
6
+ --text-3xs: 0.625rem;
7
+ --text-3xs--line-height: 1.3;
8
+ --text-2xs: 0.6875rem;
9
+ --text-2xs--line-height: 1.35;
10
+
11
+ /* The heading ladder, and the reason it is four rungs of its own rather than
12
+ four picks off the body ramp: a heading's size is a RATIO to the body under
13
+ it, and the two surfaces set body at different rungs — 13px in the product,
14
+ 18px on `.editorial`. A heading pinned to `--text-base` therefore reads one
15
+ step above body on one surface and two steps BELOW it on the other, which is
16
+ how h3 and h4 ended up smaller than the paragraphs they introduce and why
17
+ `larger` and `entry` were added to climb back out. No shared rung can be
18
+ both 14px and 20px; a rung of the heading's own can.
19
+
20
+ Dense values here, editorial below — the same shape `--heading-weight` uses.
21
+ The ladder off 13px body: 1.08 / 1.23 / 1.38 / 1.69. */
22
+ --text-h4: 0.875rem; /* 14 — the card / panel title */
23
+ --text-h4--line-height: 1.35;
24
+ --text-h3: 1rem; /* 16 */
25
+ --text-h3--line-height: 1.3;
26
+ --text-h2: 1.125rem; /* 18 — the section heading */
27
+ --text-h2--line-height: 1.25;
28
+ --text-h1: 1.375rem; /* 22 — the page title */
29
+ --text-h1--line-height: 1.2;
30
+
31
+ /* Plain @theme, never `inline`: inline bakes the family into the utility and
32
+ `.editorial` could no longer redirect it per subtree.
33
+ Consumers bind these with `${font.variable}`, never `${font.className}`. */
34
+ --font-sans: var(--font-ubuntu-sans, ui-sans-serif), system-ui, sans-serif;
35
+ --font-mono:
36
+ var(--font-ubuntu-sans-mono, ui-monospace), SFMono-Regular, monospace;
37
+ --font-heading: var(--font-sans);
38
+ }
39
+
40
+ @layer base {
41
+ :root {
42
+ /* 680, not 600: Ubuntu Sans steps 500→600 by 9.3% where Geist stepped 11.9%,
43
+ and panel titles stopped separating from the labels inside them. */
44
+ --heading-weight: 680;
45
+ }
46
+
47
+ /* Headline serif over sans body copy. Body stays sans — Average's 0.456 x-height
48
+ reads classical at 40px and cramped at reading size. */
49
+ .editorial {
50
+ --font-heading: var(--font-average, ui-serif), Georgia, serif;
51
+ --heading-weight: 400;
52
+
53
+ /* The same ladder off an 18px reading rung: 1.11 / 1.33 / 1.67 / 2. Every
54
+ rung clears the body it sits over — the contract the product values above
55
+ keep too, and the one the shared rungs could not. */
56
+ --text-h4: 1.25rem; /* 20 */
57
+ --text-h4--line-height: 1.4;
58
+ --text-h3: 1.5rem; /* 24 */
59
+ --text-h3--line-height: 1.3;
60
+ --text-h2: 1.875rem; /* 30 */
61
+ --text-h2--line-height: 1.2;
62
+ --text-h1: 2.25rem; /* 36 */
63
+ --text-h1--line-height: 1.1;
64
+ }
65
+ }
66
+
67
+ /* Unlayered on purpose: `font-medium` sits in @layer utilities, which outranks
68
+ any specificity reachable from @layer base. Average has one weight, so a
69
+ literal 500 anywhere under `.editorial` would be synthesised. */
70
+ .editorial .font-heading,
71
+ .editorial.font-heading {
72
+ font-weight: var(--heading-weight);
73
+ }