ovellum 0.2.0 → 0.2.2

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.
@@ -1,9 +1,20 @@
1
1
  /*
2
- * Ovellum default site theme — Tier 1 + Tier 2 token snapshot from
3
- * docs/internal/STYLES.md. Hand-ported; resync if the source moves.
2
+ * Ovellum default site theme.
3
+ *
4
+ * The Tier-1 palette, font stacks, type scale, space scale, and a few
5
+ * radii live between the `@tokens:from-styles-md` markers below and are
6
+ * auto-synced from `docs/internal/STYLES.md` by
7
+ * `scripts/extract-style-tokens.mjs`. Edit STYLES.md, then run
8
+ * `pnpm extract-tokens` to refresh; `pnpm check-tokens` reports drift.
9
+ *
10
+ * Tokens *outside* the markers are site-specific or deliberate
11
+ * deviations from STYLES.md — leave them alone unless you know the
12
+ * reason for the deviation.
4
13
  */
5
14
 
6
15
  :root {
16
+ /* @tokens:from-styles-md:start */
17
+
7
18
  /* Tier 1 — neutrals (zinc) */
8
19
  --color-zinc-50: oklch(98.5% 0.001 286.38);
9
20
  --color-zinc-100: oklch(96.7% 0.001 286.38);
@@ -15,7 +26,7 @@
15
26
  --color-zinc-900: oklch(21% 0.006 285.89);
16
27
  --color-zinc-950: oklch(14.1% 0.005 285.82);
17
28
 
18
- /* Tier 1 — accents */
29
+ /* Tier 1 — accents (blue) */
19
30
  --color-blue-50: oklch(97% 0.014 254.6);
20
31
  --color-blue-200: oklch(88.2% 0.059 254.16);
21
32
  --color-blue-300: oklch(80.9% 0.105 251.81);
@@ -25,21 +36,23 @@
25
36
  --color-blue-700: oklch(48.8% 0.243 264.38);
26
37
  --color-blue-900: oklch(37.9% 0.146 265.52);
27
38
 
28
- /* Tier 2semantic (default light) */
29
- --color-bg: var(--color-zinc-50);
30
- --color-bg-subtle: var(--color-zinc-100);
31
- --color-bg-muted: var(--color-zinc-200);
32
- --color-fg: var(--color-zinc-900);
33
- --color-fg-muted: var(--color-zinc-700);
34
- --color-fg-subtle: var(--color-zinc-500);
35
- --color-border: var(--color-zinc-200);
36
- --color-border-strong: var(--color-zinc-300);
37
- --color-border-focus: var(--color-blue-500);
38
- --color-accent: var(--color-blue-600);
39
- --color-accent-hover: var(--color-blue-700);
40
- --color-link: var(--color-blue-600);
41
- --color-link-hover: var(--color-blue-700);
42
- --color-code-bg: var(--color-zinc-100);
39
+ /* Tier 1status accents (callouts) */
40
+ --color-green-50: oklch(98.2% 0.018 155.83);
41
+ --color-green-300: oklch(87.1% 0.15 154.45);
42
+ --color-green-700: oklch(52.7% 0.154 150.07);
43
+ --color-green-950: oklch(26.6% 0.065 152.93);
44
+ --color-amber-50: oklch(98.7% 0.022 95.28);
45
+ --color-amber-300: oklch(87.9% 0.169 91.61);
46
+ --color-amber-800: oklch(47.3% 0.137 46.2);
47
+ --color-amber-950: oklch(27.9% 0.077 45.64);
48
+ --color-red-50: oklch(97.1% 0.013 17.38);
49
+ --color-red-300: oklch(80.8% 0.114 19.57);
50
+ --color-red-700: oklch(50.5% 0.213 27.52);
51
+ --color-red-950: oklch(25.8% 0.092 26.05);
52
+ --color-violet-50: oklch(96.9% 0.016 293.76);
53
+ --color-violet-300: oklch(81.1% 0.111 293.57);
54
+ --color-violet-700: oklch(49.1% 0.27 292.58);
55
+ --color-violet-950: oklch(28.3% 0.141 291.09);
43
56
 
44
57
  /* Fonts */
45
58
  --font-sans:
@@ -49,10 +62,7 @@
49
62
  ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
50
63
  monospace;
51
64
 
52
- /* Type scale (Major Third → Perfect Fourth)
53
- Body deliberately runs 15→16px (tightened from the Utopia default
54
- of 16→19px) — closer to Stripe/Mintlify density. Other steps in
55
- the scale are unchanged. */
65
+ /* Type scale (Major Third → Perfect Fourth) — full Utopia values */
56
66
  --font-size-0: clamp(0.9375rem, 0.9158rem + 0.1087vw, 1rem);
57
67
  --font-size-1: clamp(1.25rem, 1.1341rem + 0.5793vw, 1.5831rem);
58
68
  --font-size-2: clamp(1.5625rem, 1.3721rem + 0.9522vw, 2.11rem);
@@ -67,29 +77,95 @@
67
77
  --space-m: clamp(1.5rem, 1.4022rem + 0.4892vw, 1.7813rem);
68
78
  --space-l: clamp(2rem, 1.8696rem + 0.6522vw, 2.375rem);
69
79
  --space-xl: clamp(3rem, 2.8043rem + 0.9783vw, 3.5625rem);
80
+ --space-2xl: clamp(4rem, 3.7391rem + 1.3043vw, 4.75rem);
81
+ --space-3xl: clamp(6rem, 5.6087rem + 1.9565vw, 7.125rem);
70
82
 
71
- /* Misc */
83
+ /* Radii (subset shipped) */
72
84
  --radius-md: 0.375rem;
73
85
  --radius-lg: 0.5rem;
86
+
87
+ /* @tokens:from-styles-md:end */
88
+
89
+ /* Tier 2 — semantic (default light) — site-specific remappings */
90
+ /* Body + footer-chrome are pure-neutral grays (chroma 0 — no hue) for
91
+ now; a blended background image is planned to replace this surface
92
+ later. Chrome (footer) sits ~4% L below body in light mode — the
93
+ smallest gap that reads as real tonal separation rather than
94
+ rendering noise. */
95
+ --color-bg: oklch(97% 0 0);
96
+ --color-bg-chrome: oklch(93% 0 0);
97
+ --color-bg-subtle: var(--color-zinc-100);
98
+ --color-bg-muted: var(--color-zinc-200);
99
+ /* Card surface — a hair lighter than body so a card lifts off the page
100
+ on a hairline border alone, no heavy shadow. */
101
+ --color-surface: oklch(100% 0 0);
102
+ --color-fg: var(--color-zinc-900);
103
+ --color-fg-muted: var(--color-zinc-700);
104
+ --color-fg-subtle: var(--color-zinc-500);
105
+ --color-border: var(--color-zinc-200);
106
+ --color-border-strong: var(--color-zinc-300);
107
+ --color-border-focus: var(--color-blue-500);
108
+ --color-accent: var(--color-blue-600);
109
+ --color-accent-hover: var(--color-blue-700);
110
+ --color-link: var(--color-blue-600);
111
+ --color-link-hover: var(--color-blue-700);
112
+ --color-code-bg: var(--color-zinc-100);
113
+
114
+ /* Callout types — GitHub alert vocabulary, editorial-calm palette */
115
+ --callout-note-fg: var(--color-blue-700);
116
+ --callout-note-bg: var(--color-blue-50);
117
+ --callout-tip-fg: var(--color-green-700);
118
+ --callout-tip-bg: var(--color-green-50);
119
+ --callout-important-fg: var(--color-violet-700);
120
+ --callout-important-bg: var(--color-violet-50);
121
+ --callout-warning-fg: var(--color-amber-800);
122
+ --callout-warning-bg: var(--color-amber-50);
123
+ --callout-caution-fg: var(--color-red-700);
124
+ --callout-caution-bg: var(--color-red-50);
125
+
126
+ /* Deliberate deviations from STYLES.md:
127
+ - shadows inlined (no shared --shadow-color token in this build)
128
+ - body line-height 1.55 (vs spec 1.5) — closer to Stripe density at our
129
+ tightened 15→16px body size
130
+ - tight 1.2 (vs spec 1.15) — large headings need a hair more air */
74
131
  --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.08);
75
132
  --shadow-md: 0 4px 6px oklch(0% 0 0 / 0.08), 0 2px 4px oklch(0% 0 0 / 0.08);
76
-
77
133
  --leading-normal: 1.55;
78
134
  --leading-tight: 1.2;
79
135
  --leading-mono: 1.55;
80
136
 
81
- /* Layout widths */
82
- --sidebar-w: 240px;
83
- --toc-w: 200px;
84
- --content-max: 70ch;
137
+ /* Layout widths (site-specific, not in STYLES.md) */
138
+ --sidebar-w: 260px;
139
+ --toc-w: 220px;
140
+ --content-max: 76ch;
141
+ /* Width tokens.
142
+ --chrome-max stays constant across every page — topbar and footer
143
+ read this so the chrome never shifts width between landing and docs
144
+ (avoiding the jarring "small header → wide header" jump on click).
145
+ --page-max governs the main content boundary: docs run wide
146
+ (Mintlify-style) so long code blocks and reference tables have room;
147
+ landing tightens via the body class for editorial centrepiece feel. */
148
+ --chrome-max: 1600px;
149
+ --page-max: 1600px;
150
+ }
151
+
152
+ body.ov-body-landing {
153
+ --page-max: 1100px;
85
154
  }
86
155
 
87
156
  /* Dark theme — auto + explicit */
88
157
  @media (prefers-color-scheme: dark) {
89
158
  :root[data-theme='auto'] {
90
- --color-bg: var(--color-zinc-950);
159
+ --color-bg: oklch(14.1% 0 0);
160
+ /* Chrome lifts ~6% L above body in dark mode (elevation inversion:
161
+ you can't go darker than near-black without reading as a void).
162
+ The gap is bigger than light mode's 4% because the human eye
163
+ discriminates lightness deltas worse in dark regions. */
164
+ --color-bg-chrome: oklch(20% 0 0);
91
165
  --color-bg-subtle: var(--color-zinc-900);
92
166
  --color-bg-muted: var(--color-zinc-800);
167
+ /* Card sits between body (14.1%) and chrome (20%) — a gentle lift. */
168
+ --color-surface: oklch(17.5% 0 0);
93
169
  --color-fg: var(--color-zinc-100);
94
170
  --color-fg-muted: var(--color-zinc-300);
95
171
  --color-fg-subtle: var(--color-zinc-500);
@@ -101,13 +177,25 @@
101
177
  --color-link: var(--color-blue-400);
102
178
  --color-link-hover: var(--color-blue-300);
103
179
  --color-code-bg: var(--color-zinc-900);
180
+ --callout-note-fg: var(--color-blue-300);
181
+ --callout-note-bg: var(--color-blue-950);
182
+ --callout-tip-fg: var(--color-green-300);
183
+ --callout-tip-bg: var(--color-green-950);
184
+ --callout-important-fg: var(--color-violet-300);
185
+ --callout-important-bg: var(--color-violet-950);
186
+ --callout-warning-fg: var(--color-amber-300);
187
+ --callout-warning-bg: var(--color-amber-950);
188
+ --callout-caution-fg: var(--color-red-300);
189
+ --callout-caution-bg: var(--color-red-950);
104
190
  }
105
191
  }
106
192
 
107
193
  :root[data-theme='dark'] {
108
- --color-bg: var(--color-zinc-950);
194
+ --color-bg: oklch(14.1% 0 0);
195
+ --color-bg-chrome: oklch(20% 0 0);
109
196
  --color-bg-subtle: var(--color-zinc-900);
110
197
  --color-bg-muted: var(--color-zinc-800);
198
+ --color-surface: oklch(17.5% 0 0);
111
199
  --color-fg: var(--color-zinc-100);
112
200
  --color-fg-muted: var(--color-zinc-300);
113
201
  --color-fg-subtle: var(--color-zinc-500);
@@ -119,6 +207,16 @@
119
207
  --color-link: var(--color-blue-400);
120
208
  --color-link-hover: var(--color-blue-300);
121
209
  --color-code-bg: var(--color-zinc-900);
210
+ --callout-note-fg: var(--color-blue-300);
211
+ --callout-note-bg: var(--color-blue-950);
212
+ --callout-tip-fg: var(--color-green-300);
213
+ --callout-tip-bg: var(--color-green-950);
214
+ --callout-important-fg: var(--color-violet-300);
215
+ --callout-important-bg: var(--color-violet-950);
216
+ --callout-warning-fg: var(--color-amber-300);
217
+ --callout-warning-bg: var(--color-amber-950);
218
+ --callout-caution-fg: var(--color-red-300);
219
+ --callout-caution-bg: var(--color-red-950);
122
220
  }
123
221
 
124
222
  /* Reset + base */
@@ -134,6 +232,15 @@ body {
134
232
  padding: 0;
135
233
  }
136
234
 
235
+ /* html bg matches the body (not the chrome) so Safari's rubber-band
236
+ overscroll at the top continues the topbar's body color cleanly.
237
+ The footer keeps its chrome tint, so bottom overscroll will reveal
238
+ body instead of footer — accepted tradeoff: top overscroll is the
239
+ one users actually notice. */
240
+ html {
241
+ background: var(--color-bg);
242
+ }
243
+
137
244
  body {
138
245
  font-family: var(--font-sans);
139
246
  font-size: var(--font-size-0);
@@ -159,30 +266,77 @@ a:hover {
159
266
  border-radius: 2px;
160
267
  }
161
268
 
162
- /* Top bar */
269
+ /* Top bar
270
+ ===
271
+ Outer element is full-bleed so the chrome tint and bottom border span
272
+ the viewport — keeps the header visually identical between landing
273
+ (narrow body) and docs (wide body). Inner element holds the grid and
274
+ constrains content to --chrome-max. */
163
275
  .ov-topbar {
164
276
  position: sticky;
165
277
  top: 0;
166
278
  z-index: 10;
279
+ inline-size: 100%;
280
+ /* Topbar reads as a continuation of the body, separated only by a
281
+ thin hairline border-block-end. Footer keeps the chrome tint —
282
+ the asymmetry is intentional: a tinted footer reads as a closing
283
+ baseline, a tinted topbar fought every other surface on the page. */
284
+ background: var(--color-bg);
285
+ border-block-end: 1px solid var(--color-border);
286
+ }
287
+ .ov-topbar-inner {
167
288
  display: grid;
289
+ /* brand | search (fills the middle) | icon cluster */
168
290
  grid-template-columns: auto 1fr auto;
169
291
  align-items: center;
170
292
  gap: var(--space-m);
171
293
  padding: var(--space-2xs) var(--space-m);
172
- max-inline-size: 1320px;
294
+ max-inline-size: var(--chrome-max);
173
295
  margin-inline: auto;
174
296
  inline-size: 100%;
175
- border-block-end: 1px solid var(--color-border);
176
- background: color-mix(in oklch, var(--color-bg) 88%, transparent);
177
- backdrop-filter: saturate(180%) blur(8px);
297
+ }
298
+ .ov-brand-row {
299
+ display: inline-flex;
300
+ align-items: center;
301
+ gap: var(--space-2xs);
302
+ min-inline-size: 0;
178
303
  }
179
304
  .ov-brand {
180
- font-weight: 600;
305
+ display: inline-flex;
306
+ align-items: center;
307
+ gap: var(--space-2xs);
308
+ font-weight: 700;
181
309
  text-decoration: none;
182
310
  color: var(--color-fg);
183
- font-size: var(--font-size-1);
311
+ font-size: 1.125rem;
184
312
  letter-spacing: -0.01em;
185
313
  }
314
+ .ov-brand-mark {
315
+ flex: none;
316
+ inline-size: 1.5rem;
317
+ block-size: 1.5rem;
318
+ line-height: 0;
319
+ }
320
+ /* Center zone — the search box stretches to fill it. */
321
+ .ov-topbar-search {
322
+ display: flex;
323
+ min-inline-size: 0;
324
+ }
325
+ .ov-brand-version {
326
+ display: inline-flex;
327
+ align-items: center;
328
+ padding: 1px 0.45em;
329
+ font-family: var(--font-mono);
330
+ font-size: 0.7em;
331
+ font-weight: 500;
332
+ line-height: 1.4;
333
+ color: var(--color-fg-muted);
334
+ background: var(--color-bg-subtle);
335
+ border: 1px solid var(--color-border);
336
+ border-radius: var(--radius-sm);
337
+ letter-spacing: 0;
338
+ user-select: all;
339
+ }
186
340
  .ov-topbar-nav {
187
341
  display: flex;
188
342
  gap: var(--space-xs);
@@ -206,13 +360,39 @@ a:hover {
206
360
  color: var(--color-fg);
207
361
  background: var(--color-bg-subtle);
208
362
  }
363
+ /* Icon-only links (e.g. GitHub, npm) and the theme toggle share one look:
364
+ rounded-corner square buttons with a hairline border, spaced apart. */
365
+ .ov-topbar-link--icon,
366
+ .ov-theme-toggle {
367
+ display: inline-flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+ inline-size: 2.25rem;
371
+ block-size: 2.25rem;
372
+ padding: 0;
373
+ border: 1px solid var(--color-border);
374
+ background: var(--color-bg-subtle);
375
+ color: var(--color-fg-muted);
376
+ border-radius: var(--radius-md);
377
+ transition: color 120ms ease, background-color 120ms ease, border-color 120ms ease;
378
+ }
379
+ .ov-topbar-link--icon:hover,
380
+ .ov-theme-toggle:hover {
381
+ color: var(--color-fg);
382
+ background: var(--color-bg-muted);
383
+ border-color: var(--color-border-strong);
384
+ }
385
+ .ov-topbar-glyph {
386
+ line-height: 0;
387
+ }
209
388
  .ov-topbar-icon {
210
389
  opacity: 0.7;
211
390
  }
212
391
  .ov-topbar-right {
213
392
  display: flex;
214
393
  align-items: center;
215
- gap: var(--space-2xs);
394
+ gap: var(--space-xs);
395
+ min-inline-size: 0;
216
396
  }
217
397
 
218
398
  /* Search box (Pagefind UI as a floating dropdown)
@@ -228,8 +408,11 @@ a:hover {
228
408
  .ov-search {
229
409
  position: relative;
230
410
  inline-size: 100%;
231
- min-inline-size: 220px;
232
- max-inline-size: 320px;
411
+ min-inline-size: 0;
412
+ /* Fills the center grid column; capped so it stays comfortable on very
413
+ wide viewports without hugging the icon cluster. */
414
+ max-inline-size: 720px;
415
+ margin-inline: auto;
233
416
  }
234
417
  .ov-search .pagefind-ui {
235
418
  --pagefind-ui-scale: 0.6;
@@ -242,6 +425,13 @@ a:hover {
242
425
  --pagefind-ui-border-radius: var(--radius-md);
243
426
  --pagefind-ui-font: var(--font-sans);
244
427
  }
428
+ /* Let the search collapse with its grid column — an <input>'s intrinsic
429
+ min-width would otherwise refuse to shrink and overflow the topbar
430
+ (pushing the hamburger off-screen) on narrow viewports. */
431
+ .ov-search .pagefind-ui__form,
432
+ .ov-search .pagefind-ui__search-input {
433
+ min-inline-size: 0;
434
+ }
245
435
  .ov-search .pagefind-ui__search-input {
246
436
  font-weight: 500;
247
437
  color: var(--color-fg);
@@ -317,30 +507,10 @@ a:hover {
317
507
  display: none;
318
508
  }
319
509
 
320
- @media (max-width: 720px) {
321
- .ov-search {
322
- display: none;
323
- }
324
- }
325
-
510
+ /* Shared icon-button appearance (square, bordered) lives with
511
+ `.ov-topbar-link--icon` above; here we only add the toggle's cursor. */
326
512
  .ov-theme-toggle {
327
- display: inline-flex;
328
- align-items: center;
329
- justify-content: center;
330
- inline-size: 2.25rem;
331
- block-size: 2.25rem;
332
- padding: 0;
333
- border: 1px solid transparent;
334
- background: transparent;
335
- color: var(--color-fg-muted);
336
- border-radius: var(--radius-md);
337
513
  cursor: pointer;
338
- transition: color 120ms ease, background-color 120ms ease, border-color 120ms ease;
339
- }
340
- .ov-theme-toggle:hover {
341
- color: var(--color-fg);
342
- background: var(--color-bg-subtle);
343
- border-color: var(--color-border);
344
514
  }
345
515
  .ov-theme-icon {
346
516
  display: none;
@@ -406,15 +576,39 @@ a:hover {
406
576
  .ov-mobile-nav .ov-topbar-link {
407
577
  padding: var(--space-xs) var(--space-s);
408
578
  font-size: 1.05em;
579
+ gap: 0.6em;
580
+ }
581
+ /* Theme toggle lives in the sheet on mobile (kept out of the top row). */
582
+ .ov-mobile-theme {
583
+ display: flex;
584
+ align-items: center;
585
+ justify-content: space-between;
586
+ padding: var(--space-2xs) var(--space-s);
587
+ margin-block-start: var(--space-2xs);
588
+ border-block-start: 1px solid var(--color-border);
589
+ }
590
+ .ov-mobile-theme-label {
591
+ font-size: 1.05em;
592
+ font-weight: 500;
593
+ color: var(--color-fg-muted);
409
594
  }
410
595
  body.ov-menu-open {
411
596
  overflow: hidden;
412
597
  }
413
598
 
414
599
  @media (max-width: 720px) {
600
+ /* Tighter inner gap so the centered search keeps room next to the
601
+ larger brand and the hamburger. */
602
+ .ov-topbar-inner {
603
+ gap: var(--space-xs);
604
+ }
415
605
  .ov-topbar-nav {
416
606
  display: none;
417
607
  }
608
+ /* The desktop theme toggle hides on mobile; the sheet carries its own. */
609
+ .ov-topbar-right > .ov-theme-toggle {
610
+ display: none;
611
+ }
418
612
  .ov-topbar-menu {
419
613
  display: inline-flex;
420
614
  }
@@ -425,7 +619,7 @@ body.ov-menu-open {
425
619
  display: grid;
426
620
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr) var(--toc-w);
427
621
  gap: var(--space-l);
428
- max-inline-size: 1320px;
622
+ max-inline-size: var(--page-max);
429
623
  margin-inline: auto;
430
624
  padding: var(--space-m);
431
625
  }
@@ -446,23 +640,35 @@ body.ov-menu-open {
446
640
  padding: 0;
447
641
  }
448
642
  .ov-sidebar-nav > ul > li {
449
- margin-block-end: var(--space-2xs);
643
+ margin-block-end: var(--space-s);
644
+ padding-block-end: var(--space-s);
645
+ border-block-end: 1px solid var(--color-border);
646
+ }
647
+ .ov-sidebar-nav > ul > li:last-child {
648
+ border-block-end: 0;
649
+ padding-block-end: 0;
650
+ }
651
+ .ov-sidebar-nav li {
652
+ margin-block-end: var(--space-3xs);
450
653
  }
451
654
  .ov-nav-link {
452
655
  display: block;
453
656
  padding: var(--space-3xs) var(--space-2xs);
454
- border-radius: var(--radius-md);
657
+ border-radius: var(--radius-sm);
455
658
  text-decoration: none;
456
659
  color: var(--color-fg-muted);
457
660
  font-size: 0.95em;
661
+ border-inline-start: 2px solid transparent;
662
+ margin-inline-start: -2px;
663
+ transition: color 120ms ease, border-color 120ms ease, background-color 120ms ease;
458
664
  }
459
665
  .ov-nav-link:hover {
460
- background: var(--color-bg-subtle);
461
666
  color: var(--color-fg);
462
667
  }
463
668
  .ov-nav-link.is-active {
464
- background: var(--color-accent);
465
- color: var(--color-bg);
669
+ color: var(--color-accent-fg, var(--color-accent));
670
+ font-weight: 600;
671
+ border-inline-start-color: var(--color-accent);
466
672
  }
467
673
  .ov-nav-group {
468
674
  display: block;
@@ -489,25 +695,31 @@ body.ov-menu-open {
489
695
  .ov-prose h1 {
490
696
  font-size: var(--font-size-4);
491
697
  line-height: var(--leading-tight);
492
- letter-spacing: -0.025em;
698
+ letter-spacing: -0.03em;
493
699
  margin-block: 0 var(--space-m);
700
+ font-weight: 700;
494
701
  }
495
702
  .ov-prose h2 {
496
703
  font-size: var(--font-size-3);
497
704
  line-height: var(--leading-tight);
498
- letter-spacing: -0.025em;
499
- margin-block: var(--space-l) var(--space-s);
500
- padding-block-start: var(--space-s);
501
- border-block-start: 1px solid var(--color-border);
705
+ letter-spacing: -0.02em;
706
+ margin-block: var(--space-xl) var(--space-s);
707
+ font-weight: 700;
502
708
  }
503
709
  .ov-prose h3 {
504
710
  font-size: var(--font-size-2);
505
711
  line-height: 1.25;
506
- margin-block: var(--space-m) var(--space-s);
712
+ letter-spacing: -0.015em;
713
+ margin-block: var(--space-l) var(--space-2xs);
714
+ font-weight: 600;
507
715
  }
508
716
  .ov-prose h4 {
509
717
  font-size: var(--font-size-1);
510
- margin-block: var(--space-m) var(--space-s);
718
+ margin-block: var(--space-m) var(--space-2xs);
719
+ font-weight: 600;
720
+ }
721
+ .ov-prose :is(h1, h2, h3, h4) + p {
722
+ margin-block-start: 0;
511
723
  }
512
724
  .ov-prose p,
513
725
  .ov-prose ul,
@@ -522,37 +734,163 @@ body.ov-menu-open {
522
734
  border-inline-start: 3px solid var(--color-border-strong);
523
735
  color: var(--color-fg-muted);
524
736
  }
737
+
738
+ /* Callouts (GitHub alert syntax: > [!NOTE] / [!TIP] / [!IMPORTANT] /
739
+ [!WARNING] / [!CAUTION]). Rendered as a labelled panel with a 3px
740
+ left rule in the type color, a faint type-color background, and an
741
+ uppercase eyebrow label. Editorial-calm: thin rule, restrained tint,
742
+ no icon — the label carries the meaning. */
743
+ .ov-prose .ov-callout {
744
+ --_fg: var(--color-fg);
745
+ --_bg: var(--color-bg-subtle);
746
+ margin-block: 0 var(--space-s);
747
+ padding: var(--space-s) var(--space-m);
748
+ border-inline-start: 3px solid var(--_fg);
749
+ background: var(--_bg);
750
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
751
+ color: var(--color-fg);
752
+ }
753
+ .ov-prose .ov-callout--note {
754
+ --_fg: var(--callout-note-fg);
755
+ --_bg: var(--callout-note-bg);
756
+ }
757
+ .ov-prose .ov-callout--tip {
758
+ --_fg: var(--callout-tip-fg);
759
+ --_bg: var(--callout-tip-bg);
760
+ }
761
+ .ov-prose .ov-callout--important {
762
+ --_fg: var(--callout-important-fg);
763
+ --_bg: var(--callout-important-bg);
764
+ }
765
+ .ov-prose .ov-callout--warning {
766
+ --_fg: var(--callout-warning-fg);
767
+ --_bg: var(--callout-warning-bg);
768
+ }
769
+ .ov-prose .ov-callout--caution {
770
+ --_fg: var(--callout-caution-fg);
771
+ --_bg: var(--callout-caution-bg);
772
+ }
773
+ .ov-prose .ov-callout-label {
774
+ font-size: 0.74em;
775
+ font-weight: 700;
776
+ letter-spacing: 0.08em;
777
+ text-transform: uppercase;
778
+ color: var(--_fg);
779
+ margin-block-end: var(--space-2xs);
780
+ }
781
+ .ov-prose .ov-callout > p {
782
+ margin-block: 0;
783
+ }
784
+ .ov-prose .ov-callout > p + p,
785
+ .ov-prose .ov-callout > * + * {
786
+ margin-block-start: var(--space-2xs);
787
+ }
525
788
  .ov-prose ul,
526
789
  .ov-prose ol {
527
790
  padding-inline-start: var(--space-m);
528
791
  }
792
+ .ov-prose li {
793
+ margin-block-end: 0.25em;
794
+ }
795
+ .ov-prose li:last-child {
796
+ margin-block-end: 0;
797
+ }
798
+ .ov-prose li > p {
799
+ margin-block: 0;
800
+ }
801
+ .ov-prose li > p + p {
802
+ margin-block-start: var(--space-2xs);
803
+ }
804
+ .ov-prose ul ul,
805
+ .ov-prose ul ol,
806
+ .ov-prose ol ol,
807
+ .ov-prose ol ul {
808
+ margin-block: 0.25em 0;
809
+ }
810
+ .ov-prose ul li::marker {
811
+ color: var(--color-fg-subtle);
812
+ }
813
+ .ov-prose ol li::marker {
814
+ color: var(--color-fg-subtle);
815
+ font-variant-numeric: tabular-nums;
816
+ }
817
+
818
+ /* GFM task lists: kill the bullet, keep the checkbox tight to the text. */
819
+ .ov-prose ul.contains-task-list {
820
+ list-style: none;
821
+ padding-inline-start: 0;
822
+ }
823
+ .ov-prose li.task-list-item {
824
+ display: flex;
825
+ align-items: baseline;
826
+ gap: 0.5em;
827
+ }
828
+ .ov-prose li.task-list-item input[type='checkbox'] {
829
+ margin: 0;
830
+ accent-color: var(--color-accent);
831
+ transform: translateY(2px);
832
+ }
529
833
  .ov-prose code:not(pre code) {
530
834
  font-family: var(--font-mono);
531
835
  background: var(--color-code-bg);
532
- padding: 0.1em 0.4em;
533
- border-radius: 4px;
534
- font-size: 0.92em;
836
+ padding: 0.1em 0.38em;
837
+ border-radius: var(--radius-sm);
838
+ font-size: 0.88em;
839
+ color: var(--color-fg);
840
+ white-space: nowrap;
841
+ }
842
+ .ov-prose a > code:not(pre code) {
843
+ color: var(--color-link);
535
844
  }
536
845
  .ov-prose img {
537
846
  max-inline-size: 100%;
538
847
  height: auto;
539
848
  border-radius: var(--radius-md);
540
849
  }
850
+ /* Tables — editorial-calm: horizontal rules only, no grid, no header
851
+ fill. Header reads via weight + a thicker bottom rule.
852
+ Each table is wrapped by `.ov-table-wrap` (rehypeTableWrap) so a
853
+ table wider than the prose column scrolls horizontally inside the
854
+ wrap instead of forcing the column to grow. */
855
+ .ov-prose .ov-table-wrap {
856
+ margin-block: 0 var(--space-s);
857
+ inline-size: 100%;
858
+ max-inline-size: 100%;
859
+ overflow-x: auto;
860
+ }
861
+ .ov-prose .ov-table-wrap > table {
862
+ margin-block: 0;
863
+ }
541
864
  .ov-prose table {
542
865
  border-collapse: collapse;
543
866
  inline-size: 100%;
544
- margin-block: 0 var(--space-s);
545
- font-size: 0.95em;
867
+ font-size: 0.92em;
868
+ font-variant-numeric: tabular-nums;
546
869
  }
547
870
  .ov-prose th,
548
871
  .ov-prose td {
549
- padding: var(--space-2xs) var(--space-s);
550
- border: 1px solid var(--color-border);
872
+ padding: var(--space-2xs) var(--space-s) var(--space-2xs) 0;
551
873
  text-align: start;
874
+ vertical-align: top;
875
+ border-block-end: 1px solid var(--color-border);
552
876
  }
553
877
  .ov-prose th {
554
- background: var(--color-bg-subtle);
555
878
  font-weight: 600;
879
+ font-size: 0.92em;
880
+ text-transform: uppercase;
881
+ letter-spacing: 0.04em;
882
+ color: var(--color-fg-muted);
883
+ border-block-end: 1px solid var(--color-border-strong);
884
+ padding-block: var(--space-xs);
885
+ }
886
+ .ov-prose tbody tr:last-child td {
887
+ border-block-end: 0;
888
+ }
889
+ .ov-prose tbody tr:hover td {
890
+ background: var(--color-bg-subtle);
891
+ }
892
+ .ov-prose table code:not(pre code) {
893
+ font-size: 0.9em;
556
894
  }
557
895
 
558
896
  /* Heading anchors
@@ -578,14 +916,19 @@ body.ov-menu-open {
578
916
  color: var(--color-accent);
579
917
  }
580
918
 
581
- /* Code blocks (shiki dual-theme) */
919
+ /* Code blocks (shiki dual-theme)
920
+ ===
921
+ Padding bumped on top to clear the language eyebrow injected via
922
+ `pre[data-language]::before`. On hover the eyebrow fades and the
923
+ copy button (added by script.js) fades in at the same corner —
924
+ they trade places rather than compete for space. */
582
925
  .ov-prose pre {
583
926
  position: relative;
584
- padding: var(--space-s);
927
+ padding: var(--space-m) var(--space-s) var(--space-s);
585
928
  border-radius: var(--radius-md);
586
929
  overflow-x: auto;
587
930
  font-family: var(--font-mono);
588
- font-size: 0.92em;
931
+ font-size: 0.9em;
589
932
  line-height: var(--leading-mono);
590
933
  }
591
934
  .ov-prose pre.shiki {
@@ -612,38 +955,106 @@ body.ov-menu-open {
612
955
  color: var(--shiki-dark);
613
956
  }
614
957
 
615
- /* Copy button (injected by script.js) */
616
- .ov-copy-btn {
958
+ /* Language eyebrow (top-right of every highlighted fenced block) */
959
+ .ov-prose pre[data-language]::before {
960
+ content: attr(data-language);
617
961
  position: absolute;
618
962
  top: var(--space-2xs);
963
+ right: var(--space-s);
964
+ font-family: var(--font-mono);
965
+ font-size: 0.66em;
966
+ font-weight: 600;
967
+ text-transform: uppercase;
968
+ letter-spacing: 0.08em;
969
+ color: var(--color-fg-subtle);
970
+ opacity: 0.85;
971
+ pointer-events: none;
972
+ transition: opacity 120ms ease;
973
+ }
974
+ .ov-prose pre:hover[data-language]::before {
975
+ opacity: 0;
976
+ }
977
+
978
+ /* Copy button (injected by script.js) — shares the top-right corner
979
+ with the language eyebrow, swaps in on hover. */
980
+ .ov-copy-btn {
981
+ position: absolute;
982
+ top: var(--space-3xs);
619
983
  right: var(--space-2xs);
620
- padding: var(--space-3xs) var(--space-2xs);
621
- font-size: 0.78em;
984
+ padding: 2px var(--space-2xs);
985
+ font-size: 0.72em;
622
986
  font-family: var(--font-sans);
623
- background: var(--color-bg-subtle);
987
+ font-weight: 600;
988
+ letter-spacing: 0.02em;
989
+ background: transparent;
624
990
  color: var(--color-fg-muted);
625
- border: 1px solid var(--color-border-strong);
626
- border-radius: 4px;
991
+ border: 1px solid transparent;
992
+ border-radius: var(--radius-sm);
627
993
  cursor: pointer;
628
994
  opacity: 0;
629
- transition: opacity 120ms ease;
995
+ transition: opacity 120ms ease, color 120ms ease, background-color 120ms ease, border-color 120ms ease;
630
996
  }
631
997
  .ov-prose pre:hover .ov-copy-btn,
632
998
  .ov-copy-btn:focus-visible {
633
999
  opacity: 1;
634
1000
  }
1001
+ .ov-copy-btn:hover {
1002
+ color: var(--color-fg);
1003
+ background: var(--color-bg-subtle);
1004
+ border-color: var(--color-border);
1005
+ }
1006
+ /* Breadcrumbs — sits above the page-meta when the page is nested 2+
1007
+ levels deep. Slash separator inserted via ::after on each crumb so
1008
+ it scales with text and doesn't render as an extra DOM element. */
1009
+ .ov-breadcrumbs {
1010
+ margin: 0 0 var(--space-2xs);
1011
+ }
1012
+ .ov-breadcrumbs ol {
1013
+ list-style: none;
1014
+ margin: 0;
1015
+ padding: 0;
1016
+ display: flex;
1017
+ flex-wrap: wrap;
1018
+ gap: 0;
1019
+ font-size: 0.82em;
1020
+ color: var(--color-fg-muted);
1021
+ }
1022
+ .ov-crumb {
1023
+ display: inline-flex;
1024
+ align-items: baseline;
1025
+ }
1026
+ .ov-crumb a {
1027
+ color: var(--color-fg-muted);
1028
+ text-decoration: none;
1029
+ transition: color 120ms ease;
1030
+ }
1031
+ .ov-crumb a:hover {
1032
+ color: var(--color-fg);
1033
+ }
1034
+ .ov-crumb:not(:last-child)::after {
1035
+ content: '/';
1036
+ color: var(--color-fg-subtle);
1037
+ opacity: 0.45;
1038
+ padding-inline: 0.5em;
1039
+ }
1040
+ .ov-crumb.is-current {
1041
+ color: var(--color-fg);
1042
+ }
1043
+
635
1044
  /* Page meta line (reading time + last-modified) above the article */
636
1045
  .ov-page-meta {
637
1046
  margin: 0 0 var(--space-s);
638
- font-size: 0.88em;
639
- color: var(--color-fg-muted);
1047
+ font-size: 0.82em;
1048
+ color: var(--color-fg-subtle);
640
1049
  display: flex;
641
1050
  flex-wrap: wrap;
642
- gap: 0.25em;
1051
+ gap: 0;
643
1052
  align-items: baseline;
1053
+ font-variant-numeric: tabular-nums;
644
1054
  }
645
1055
  .ov-page-meta-sep {
646
- opacity: 0.6;
1056
+ opacity: 0.4;
1057
+ padding-inline: 0.5em;
647
1058
  }
648
1059
 
649
1060
  /* Edit-this-page link */
@@ -664,12 +1075,15 @@ body.ov-menu-open {
664
1075
  }
665
1076
 
666
1077
  /* Prev / next page navigation */
1078
+ /* Prev / next — Retype-style bordered button pair. The whole card is
1079
+ the click target; an arrow inline with the eyebrow label shows
1080
+ direction. Hover lifts the border + bg-subtle to confirm clickability. */
667
1081
  .ov-prevnext {
668
1082
  display: grid;
669
1083
  grid-template-columns: 1fr 1fr;
670
1084
  gap: var(--space-m);
671
- margin-block: var(--space-xl) var(--space-m);
672
- padding-block-start: var(--space-m);
1085
+ margin-block: var(--space-2xl) var(--space-m);
1086
+ padding-block-start: var(--space-l);
673
1087
  border-block-start: 1px solid var(--color-border);
674
1088
  }
675
1089
  .ov-prevnext-link {
@@ -681,25 +1095,62 @@ body.ov-menu-open {
681
1095
  border-radius: var(--radius-md);
682
1096
  text-decoration: none;
683
1097
  color: var(--color-fg);
684
- transition: border-color 120ms ease;
1098
+ background: var(--color-bg);
1099
+ transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
1100
+ min-inline-size: 0;
685
1101
  }
686
1102
  .ov-prevnext-link:hover {
687
- border-color: var(--color-fg-subtle);
688
- color: var(--color-fg);
1103
+ border-color: var(--color-border-strong);
1104
+ background: var(--color-bg-subtle);
1105
+ color: var(--color-accent);
689
1106
  }
690
1107
  .ov-prevnext-next {
691
1108
  text-align: end;
692
1109
  align-items: flex-end;
693
1110
  }
694
1111
  .ov-prevnext-label {
695
- font-size: 0.78em;
1112
+ display: inline-flex;
1113
+ align-items: center;
1114
+ gap: 0.4em;
1115
+ font-size: 0.74em;
696
1116
  text-transform: uppercase;
697
- letter-spacing: 0.05em;
1117
+ letter-spacing: 0.08em;
698
1118
  color: var(--color-fg-subtle);
699
1119
  font-weight: 600;
700
1120
  }
1121
+ .ov-prevnext-prev .ov-prevnext-label::before {
1122
+ content: '←';
1123
+ font-size: 1.1em;
1124
+ line-height: 1;
1125
+ letter-spacing: 0;
1126
+ transition: transform 120ms ease;
1127
+ }
1128
+ .ov-prevnext-next .ov-prevnext-label::after {
1129
+ content: '→';
1130
+ font-size: 1.1em;
1131
+ line-height: 1;
1132
+ letter-spacing: 0;
1133
+ transition: transform 120ms ease;
1134
+ }
1135
+ .ov-prevnext-link:hover .ov-prevnext-label {
1136
+ color: var(--color-accent);
1137
+ }
1138
+ .ov-prevnext-prev:hover .ov-prevnext-label::before {
1139
+ transform: translateX(-2px);
1140
+ }
1141
+ .ov-prevnext-next:hover .ov-prevnext-label::after {
1142
+ transform: translateX(2px);
1143
+ }
701
1144
  .ov-prevnext-title {
702
- font-weight: 500;
1145
+ font-weight: 600;
1146
+ font-size: var(--font-size-1);
1147
+ letter-spacing: -0.01em;
1148
+ line-height: 1.3;
1149
+ /* Truncate insanely long titles rather than wrapping the button shape. */
1150
+ overflow: hidden;
1151
+ text-overflow: ellipsis;
1152
+ white-space: nowrap;
1153
+ inline-size: 100%;
703
1154
  }
704
1155
  .ov-prevnext-spacer {
705
1156
  display: block;
@@ -715,7 +1166,8 @@ body.ov-menu-open {
715
1166
  }
716
1167
 
717
1168
  .ov-copy-btn.is-copied {
718
- color: var(--color-accent);
1169
+ color: var(--callout-tip-fg);
1170
+ opacity: 1;
719
1171
  }
720
1172
 
721
1173
  /* Right ToC */
@@ -728,44 +1180,146 @@ body.ov-menu-open {
728
1180
  font-size: 0.9em;
729
1181
  }
730
1182
  .ov-toc-title {
731
- font-size: 0.78em;
1183
+ font-size: 0.72em;
732
1184
  text-transform: uppercase;
733
- letter-spacing: 0.05em;
1185
+ letter-spacing: 0.08em;
734
1186
  color: var(--color-fg-subtle);
735
- margin-block: 0 var(--space-2xs);
1187
+ margin-block: 0 var(--space-xs);
736
1188
  font-weight: 600;
737
1189
  }
738
1190
  .ov-toc ul {
739
1191
  list-style: none;
740
1192
  margin: 0;
741
1193
  padding: 0;
1194
+ position: relative;
1195
+ border-inline-start: 1px solid var(--color-border);
742
1196
  }
743
1197
  .ov-toc li {
744
- margin-block-end: var(--space-3xs);
1198
+ margin: 0;
745
1199
  }
746
1200
  .ov-toc li a {
1201
+ position: relative;
1202
+ display: block;
1203
+ padding: 0.3em 0;
1204
+ padding-inline-start: var(--space-s);
747
1205
  color: var(--color-fg-muted);
748
1206
  text-decoration: none;
1207
+ transition: color 120ms ease;
1208
+ line-height: 1.4;
749
1209
  }
750
1210
  .ov-toc li a:hover {
751
1211
  color: var(--color-fg);
752
1212
  }
753
- .ov-toc-h3 {
754
- padding-inline-start: var(--space-s);
1213
+ .ov-toc li a.is-current {
1214
+ color: var(--color-accent);
1215
+ font-weight: 500;
1216
+ }
1217
+ /* The active indicator: 2px accent strip overlaying the 1px ul border.
1218
+ Pulled 1px outside so it covers the track cleanly. */
1219
+ .ov-toc li a.is-current::before {
1220
+ content: '';
1221
+ position: absolute;
1222
+ inset-block: 0;
1223
+ inset-inline-start: -1px;
1224
+ inline-size: 2px;
1225
+ background: var(--color-accent);
1226
+ }
1227
+ .ov-toc-h3 a {
1228
+ padding-inline-start: calc(var(--space-s) + var(--space-s));
1229
+ font-size: 0.95em;
755
1230
  }
756
1231
 
757
- /* Footer */
1232
+ /* Footer
1233
+ ===
1234
+ Outer is full-bleed (matches .ov-topbar) so the chrome tint and top
1235
+ border span the viewport; inner holds the grid and constrains to
1236
+ --chrome-max. Result: the topbar, content area, and footer rails
1237
+ share identical vertical gutters at every viewport, and the chrome
1238
+ width never jumps between landing and docs. */
758
1239
  .ov-footer {
759
- max-inline-size: 1320px;
760
- margin: var(--space-l) auto 0;
761
- padding: var(--space-s) var(--space-m);
1240
+ inline-size: 100%;
1241
+ margin-block-start: var(--space-2xl);
762
1242
  border-block-start: 1px solid var(--color-border);
1243
+ background: var(--color-bg-chrome);
763
1244
  color: var(--color-fg-subtle);
764
- font-size: 0.9em;
765
- text-align: center;
1245
+ font-size: 0.84em;
1246
+ }
1247
+ .ov-footer-inner {
1248
+ display: grid;
1249
+ grid-template-columns: auto 1fr auto;
1250
+ align-items: center;
1251
+ gap: var(--space-m);
1252
+ padding: var(--space-m);
1253
+ max-inline-size: var(--chrome-max);
1254
+ margin-inline: auto;
1255
+ inline-size: 100%;
1256
+ }
1257
+ .ov-footer-left {
1258
+ grid-column: 1;
1259
+ display: inline-flex;
1260
+ align-items: baseline;
1261
+ flex-wrap: wrap;
1262
+ gap: 0.45em;
1263
+ min-inline-size: 0;
1264
+ }
1265
+ .ov-footer-right {
1266
+ grid-column: 3;
1267
+ display: inline-flex;
1268
+ align-items: center;
1269
+ flex-wrap: wrap;
1270
+ gap: var(--space-xs);
1271
+ }
1272
+ .ov-footer-link {
1273
+ color: var(--color-fg-muted);
1274
+ text-decoration: none;
1275
+ transition: color 120ms ease;
1276
+ }
1277
+ .ov-footer-link:hover {
1278
+ color: var(--color-fg);
1279
+ }
1280
+ .ov-footer-link--icon {
1281
+ display: inline-flex;
1282
+ align-items: center;
1283
+ justify-content: center;
1284
+ inline-size: 1.85rem;
1285
+ block-size: 1.85rem;
1286
+ border-radius: var(--radius-md);
1287
+ color: var(--color-fg-muted);
1288
+ }
1289
+ .ov-footer-link--icon:hover {
1290
+ color: var(--color-fg);
1291
+ background: var(--color-bg-subtle);
1292
+ }
1293
+ .ov-footer-icon {
1294
+ display: block;
766
1295
  }
767
1296
  .ov-footer-sep {
768
- margin-inline: var(--space-2xs);
1297
+ margin-inline: 0.05em;
1298
+ opacity: 0.5;
1299
+ }
1300
+ .ov-sr-only {
1301
+ position: absolute;
1302
+ inline-size: 1px;
1303
+ block-size: 1px;
1304
+ padding: 0;
1305
+ margin: -1px;
1306
+ overflow: hidden;
1307
+ clip: rect(0, 0, 0, 0);
1308
+ white-space: nowrap;
1309
+ border: 0;
1310
+ }
1311
+ @media (max-width: 720px) {
1312
+ .ov-footer {
1313
+ grid-template-columns: 1fr;
1314
+ gap: var(--space-2xs);
1315
+ }
1316
+ .ov-footer-left {
1317
+ grid-column: 1;
1318
+ }
1319
+ .ov-footer-right {
1320
+ grid-column: 1;
1321
+ justify-content: flex-start;
1322
+ }
769
1323
  }
770
1324
 
771
1325
  /* Narrow viewports: drop the right ToC, then collapse the sidebar */
@@ -888,61 +1442,74 @@ body.ov-body-landing {
888
1442
  }
889
1443
 
890
1444
  .ov-landing {
891
- max-inline-size: 1100px;
1445
+ max-inline-size: var(--page-max);
892
1446
  margin-inline: auto;
893
1447
  padding: 0 var(--space-m) var(--space-l);
894
1448
  }
895
1449
 
896
1450
  /* Hero
897
1451
  ===
898
- Two stacked background layers behind the centred prose:
899
- ::before dotted noise pattern (faint, neutral, theme-aware)
900
- ::after radial spotlight gradient (accent-tinted, low alpha)
901
- Both use `pointer-events: none` and live above body bg but below content.
902
- The dotted pattern is a data-URL SVG so there's no extra HTTP request and
903
- no images to ship. */
1452
+ Centred prose (title / subtitle / CTAs) on a flat background. The earlier
1453
+ dotted-noise + accent-spotlight pseudo-layers were removed — a blended
1454
+ background image is planned to take this surface later. The vertical
1455
+ padding alone gives the hero its presence and the breathing room between
1456
+ the topbar above and the body content below. The `[data-media]` variant
1457
+ (site.landing.hero.media) still renders its own full-bleed <img> layer
1458
+ (see .ov-hero-art below). */
904
1459
  .ov-hero {
1460
+ /* position + isolation are dormant here (flat background) but kept so the
1461
+ [data-media] variant's absolutely-positioned .ov-hero-art layer still
1462
+ anchors to the hero and stays in its own stacking context. */
905
1463
  position: relative;
906
1464
  isolation: isolate;
907
1465
  text-align: center;
908
- padding-block: clamp(var(--space-2xl), 12vw, var(--space-3xl))
909
- clamp(var(--space-xl), 8vw, var(--space-2xl));
910
- margin-inline: calc(-1 * var(--space-m));
911
- overflow: hidden;
1466
+ padding-block: clamp(var(--space-2xl), 11vw, var(--space-3xl))
1467
+ clamp(var(--space-l), 7vw, var(--space-2xl));
912
1468
  }
913
- .ov-hero::before,
914
- .ov-hero::after {
915
- content: '';
1469
+
1470
+ /* Imagery hero variant — when `site.landing.hero.media` is configured the
1471
+ template adds `data-media`, switching the hero to a taller full-bleed
1472
+ layout sized to hold the supplied <img> (.ov-hero-art). */
1473
+ .ov-hero[data-media] {
1474
+ padding-block: clamp(var(--space-3xl), 14vw, calc(var(--space-3xl) * 1.4))
1475
+ clamp(var(--space-xl), 9vw, var(--space-3xl));
1476
+ min-block-size: clamp(420px, 56vw, 640px);
1477
+ display: flex;
1478
+ flex-direction: column;
1479
+ justify-content: center;
1480
+ }
1481
+ .ov-hero-art {
916
1482
  position: absolute;
917
1483
  inset: 0;
918
1484
  pointer-events: none;
919
1485
  z-index: -1;
920
- }
921
- .ov-hero::before {
922
- /* 24×24 grid of 1px dots in fg-subtle (currentColor would require
923
- `color`, but we'd rather not inherit). Mask to fade at the edges. */
924
- background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><circle cx='12' cy='12' r='1' fill='%23000' fill-opacity='0.18'/></svg>");
925
- background-size: 24px 24px;
926
- background-repeat: repeat;
927
- -webkit-mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, black 0%, transparent 80%);
928
- mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, black 0%, transparent 80%);
929
- opacity: 0.9;
930
- }
931
- :root[data-theme='dark'] .ov-hero::before {
932
- background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><circle cx='12' cy='12' r='1' fill='%23fff' fill-opacity='0.18'/></svg>");
933
- }
1486
+ overflow: hidden;
1487
+ /* Soft fade-out so the art recedes into the page before the feature grid
1488
+ begins keeps the seam editorial-calm rather than hard-edged. */
1489
+ -webkit-mask-image: linear-gradient(to bottom,
1490
+ black 0%, black 78%, transparent 100%);
1491
+ mask-image: linear-gradient(to bottom,
1492
+ black 0%, black 78%, transparent 100%);
1493
+ }
1494
+ .ov-hero-art-img {
1495
+ position: absolute;
1496
+ inset: 0;
1497
+ inline-size: 100%;
1498
+ block-size: 100%;
1499
+ object-fit: cover;
1500
+ object-position: center;
1501
+ }
1502
+ /* Default: light shown, dark hidden. Theme + auto-with-OS-preference flip. */
1503
+ .ov-hero-art-img--dark { opacity: 0; }
1504
+ :root[data-theme='dark'] .ov-hero-art-img--light { opacity: 0; }
1505
+ :root[data-theme='dark'] .ov-hero-art-img--dark { opacity: 1; }
934
1506
  @media (prefers-color-scheme: dark) {
935
- :root[data-theme='auto'] .ov-hero::before {
936
- background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><circle cx='12' cy='12' r='1' fill='%23fff' fill-opacity='0.18'/></svg>");
937
- }
1507
+ :root[data-theme='auto'] .ov-hero-art-img--light { opacity: 0; }
1508
+ :root[data-theme='auto'] .ov-hero-art-img--dark { opacity: 1; }
938
1509
  }
939
- .ov-hero::after {
940
- /* Spotlight: a soft accent-tinted ellipse, low alpha, no hard edges. */
941
- background: radial-gradient(
942
- ellipse 50% 38% at 50% 35%,
943
- color-mix(in oklch, var(--color-accent) 22%, transparent) 0%,
944
- transparent 70%
945
- );
1510
+ .ov-hero-inner {
1511
+ position: relative;
1512
+ z-index: 1;
946
1513
  }
947
1514
  .ov-hero-title {
948
1515
  position: relative;
@@ -996,17 +1563,28 @@ body.ov-body-landing {
996
1563
  .ov-cta--secondary {
997
1564
  background: transparent;
998
1565
  color: var(--color-fg);
999
- border: 1px solid var(--color-border-strong);
1566
+ border: 1px solid var(--color-border);
1000
1567
  }
1001
1568
  .ov-cta--secondary:hover {
1002
- background: var(--color-bg-subtle);
1003
- border-color: var(--color-fg-subtle);
1569
+ background: transparent;
1570
+ border-color: var(--color-fg);
1004
1571
  color: var(--color-fg);
1005
1572
  }
1006
1573
 
1007
- /* Feature grid */
1574
+ /* Card a clean, subtle surface primitive. A surface a hair lighter than
1575
+ the page, a hairline border, a modest radius, and a whisper of shadow.
1576
+ Reused across the site; the landing feature grid is the first adopter. */
1577
+ .ov-card {
1578
+ background: var(--color-surface);
1579
+ border: 1px solid var(--color-border);
1580
+ border-radius: var(--radius-lg);
1581
+ padding: var(--space-m);
1582
+ box-shadow: 0 1px 2px oklch(0% 0 0 / 0.04);
1583
+ }
1584
+
1585
+ /* Feature grid — three subtle cards beneath the hero. */
1008
1586
  .ov-feature-grid-wrap {
1009
- margin-block: var(--space-xl) var(--space-l);
1587
+ margin-block: var(--space-2xl) var(--space-xl);
1010
1588
  }
1011
1589
  .ov-feature-grid {
1012
1590
  display: grid;
@@ -1014,33 +1592,27 @@ body.ov-body-landing {
1014
1592
  gap: var(--space-m);
1015
1593
  }
1016
1594
  .ov-feature-card {
1017
- background: var(--color-bg-subtle);
1018
- border: 1px solid var(--color-border);
1019
- border-radius: var(--radius-lg);
1020
- padding: var(--space-m);
1021
1595
  display: flex;
1022
1596
  flex-direction: column;
1023
1597
  gap: var(--space-2xs);
1024
- transition: border-color 120ms ease;
1025
- }
1026
- .ov-feature-card:hover {
1027
- border-color: var(--color-border-strong);
1028
1598
  }
1029
1599
  .ov-feature-icon {
1030
1600
  font-size: var(--font-size-2);
1031
1601
  line-height: 1;
1602
+ color: var(--color-fg-subtle);
1032
1603
  }
1033
1604
  .ov-feature-title {
1034
1605
  margin: 0;
1035
1606
  font-size: var(--font-size-1);
1036
- line-height: 1.2;
1607
+ line-height: 1.25;
1037
1608
  font-weight: 600;
1609
+ letter-spacing: -0.01em;
1038
1610
  }
1039
1611
  .ov-feature-description {
1040
1612
  margin: 0;
1041
1613
  color: var(--color-fg-muted);
1042
1614
  font-size: var(--font-size-0);
1043
- line-height: 1.5;
1615
+ line-height: 1.6;
1044
1616
  }
1045
1617
 
1046
1618
  /* Pitch section (from _landing.md prose body) */
@@ -1052,7 +1624,7 @@ body.ov-body-landing {
1052
1624
  .ov-pitch-inner {
1053
1625
  max-inline-size: 60ch;
1054
1626
  margin-inline: auto;
1055
- font-size: var(--font-size-0);
1627
+ font-size: var(--font-size-1);
1056
1628
  line-height: 1.65;
1057
1629
  color: var(--color-fg-muted);
1058
1630
  }
@@ -1084,11 +1656,11 @@ body.ov-body-landing {
1084
1656
  text-align: center;
1085
1657
  }
1086
1658
  .ov-trust-label {
1087
- font-size: 0.78em;
1659
+ font-size: 0.72em;
1088
1660
  text-transform: uppercase;
1089
- letter-spacing: 0.08em;
1661
+ letter-spacing: 0.1em;
1090
1662
  color: var(--color-fg-subtle);
1091
- margin-block: 0 var(--space-s);
1663
+ margin-block: 0 var(--space-m);
1092
1664
  font-weight: 600;
1093
1665
  }
1094
1666
  .ov-trust-items {
@@ -1096,12 +1668,14 @@ body.ov-body-landing {
1096
1668
  flex-wrap: wrap;
1097
1669
  justify-content: center;
1098
1670
  align-items: center;
1099
- gap: var(--space-l);
1671
+ gap: var(--space-xl);
1100
1672
  }
1101
1673
  .ov-trust-item {
1102
- color: var(--color-fg-muted);
1674
+ color: var(--color-fg-subtle);
1103
1675
  text-decoration: none;
1104
1676
  font-weight: 500;
1677
+ font-size: 0.95em;
1678
+ transition: color 120ms ease;
1105
1679
  }
1106
1680
  .ov-trust-item:hover {
1107
1681
  color: var(--color-fg);
@@ -1116,6 +1690,60 @@ body.ov-body-landing {
1116
1690
  opacity: 1;
1117
1691
  }
1118
1692
 
1693
+ /* Ambient "scenes" — centered visual figures between landing sections.
1694
+ * Editorial-calm: no chrome, top/bottom mask fades so the visual recedes
1695
+ * into the surrounding content. SVG-driven animation lives inside each
1696
+ * asset; this wrapper is intentionally still. */
1697
+ .ov-scene {
1698
+ margin-block: var(--space-xl);
1699
+ inline-size: 100%;
1700
+ /* No full-bleed: scenes sit centered inside the landing column. The
1701
+ * landing's --page-max (1100px on body.ov-body-landing) is the
1702
+ * authoritative width; .ov-landing already centers itself. */
1703
+ margin-inline: auto;
1704
+ }
1705
+ .ov-scene-art {
1706
+ margin: 0;
1707
+ position: relative;
1708
+ aspect-ratio: 16 / 9;
1709
+ display: flex;
1710
+ justify-content: center;
1711
+ align-items: center;
1712
+ -webkit-mask-image: linear-gradient(
1713
+ to bottom,
1714
+ transparent 0%,
1715
+ black 14%,
1716
+ black 86%,
1717
+ transparent 100%
1718
+ );
1719
+ mask-image: linear-gradient(
1720
+ to bottom,
1721
+ transparent 0%,
1722
+ black 14%,
1723
+ black 86%,
1724
+ transparent 100%
1725
+ );
1726
+ }
1727
+ .ov-scene-img {
1728
+ position: absolute;
1729
+ inset: 0;
1730
+ inline-size: 100%;
1731
+ block-size: 100%;
1732
+ /* `contain` keeps the SVG's intrinsic aspect ratio visible; the figure
1733
+ * sits centered (justify/align on the parent) inside the 16:9 frame. */
1734
+ object-fit: contain;
1735
+ object-position: center;
1736
+ opacity: 0.95;
1737
+ transition: opacity 200ms ease;
1738
+ }
1739
+ .ov-scene-img--dark { opacity: 0; }
1740
+ :root[data-theme='dark'] .ov-scene-img--light { opacity: 0; }
1741
+ :root[data-theme='dark'] .ov-scene-img--dark { opacity: 0.95; }
1742
+ @media (prefers-color-scheme: dark) {
1743
+ :root[data-theme='auto'] .ov-scene-img--light { opacity: 0; }
1744
+ :root[data-theme='auto'] .ov-scene-img--dark { opacity: 0.95; }
1745
+ }
1746
+
1119
1747
  /* Landing-page responsive */
1120
1748
  @media (max-width: 720px) {
1121
1749
  .ov-landing {
@@ -1125,6 +1753,10 @@ body.ov-body-landing {
1125
1753
  margin-inline: calc(-1 * var(--space-s));
1126
1754
  padding-block: var(--space-xl) var(--space-l);
1127
1755
  }
1756
+ .ov-hero[data-media] {
1757
+ padding-block: var(--space-2xl) var(--space-xl);
1758
+ min-block-size: clamp(320px, 70vw, 460px);
1759
+ }
1128
1760
  .ov-hero-title {
1129
1761
  letter-spacing: -0.03em;
1130
1762
  }
@@ -1134,4 +1766,10 @@ body.ov-body-landing {
1134
1766
  .ov-trust-items {
1135
1767
  gap: var(--space-m);
1136
1768
  }
1769
+ .ov-scene {
1770
+ margin-block: var(--space-l);
1771
+ }
1772
+ .ov-scene-art {
1773
+ aspect-ratio: 16 / 10;
1774
+ }
1137
1775
  }