@getvitops/generator 8.3.0-alpha.3 → 8.3.0-alpha.5

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 (43) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/assets/css/index.css +1 -0
  3. package/assets/css/layout-utilities.css +13 -0
  4. package/assets/css/layout.css +23 -0
  5. package/assets/css/patterns/carousel.css +9 -2
  6. package/assets/css/patterns/details.css +14 -0
  7. package/assets/css/patterns/forms.css +14 -1
  8. package/assets/css/patterns/logos.css +14 -0
  9. package/assets/css/patterns/masthead.css +201 -0
  10. package/assets/css/patterns/section.css +10 -0
  11. package/assets/css/patterns/sitenav.css +111 -0
  12. package/assets/css/patterns/steps.css +85 -3
  13. package/assets/css/patterns/subgrid.css +9 -0
  14. package/assets/css/patterns/tone.css +16 -4
  15. package/assets/js/elements.js +1 -1
  16. package/config.schema.json +228 -5
  17. package/dist/config-BaMpGSUZ.mjs +1856 -0
  18. package/dist/config-BaMpGSUZ.mjs.map +1 -0
  19. package/dist/{config-D5xuz8--.mjs → config-Bie2MqRh.mjs} +7 -4
  20. package/dist/config-Bie2MqRh.mjs.map +1 -0
  21. package/dist/config-C1p83v26.mjs +1832 -0
  22. package/dist/config-C1p83v26.mjs.map +1 -0
  23. package/dist/config.d.mts +270 -7
  24. package/dist/config.d.mts.map +1 -1
  25. package/dist/config.mjs +1 -1
  26. package/dist/index.d.mts +2 -2
  27. package/dist/index.d.mts.map +1 -1
  28. package/dist/index.mjs +63 -9
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/{schema-C-P9dF9F.d.mts → schema-7FhnyZZy.d.mts} +9 -1
  31. package/dist/schema-7FhnyZZy.d.mts.map +1 -0
  32. package/dist/{tokens-CaIGQQLk.mjs → tokens-D0DsePUl.mjs} +11 -7
  33. package/dist/tokens-D0DsePUl.mjs.map +1 -0
  34. package/dist/{tokens-DH5cpvKd.d.mts → tokens-GQUicLBj.d.mts} +10 -3
  35. package/dist/tokens-GQUicLBj.d.mts.map +1 -0
  36. package/dist/tokens.d.mts +1 -1
  37. package/dist/tokens.mjs +1 -1
  38. package/package.json +4 -4
  39. package/schema.json +19 -0
  40. package/dist/config-D5xuz8--.mjs.map +0 -1
  41. package/dist/schema-C-P9dF9F.d.mts.map +0 -1
  42. package/dist/tokens-CaIGQQLk.mjs.map +0 -1
  43. package/dist/tokens-DH5cpvKd.d.mts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # @getvitops/generator
2
2
 
3
+ ## 8.3.0-alpha.5
4
+
5
+ ### Minor Changes
6
+
7
+ - bb61a35: **Call tracking numbers: show an ad visitor a number that attributes the phone call.** Two additive config blocks, no CSS, no breaking change — all formats unaffected.
8
+
9
+ - **`site.ads.google.callTracking`** — Google Ads _website call conversions_. `{ label, number, cache? }` makes the consent-gated Google tag fetch a Google forwarding number (Canada supported) and swap it in for visitors who arrived from a Google ad. Google replaces text only, so the capture script also rewrites the matching `tel:` href. Needs `pixelId` and `site.tracking.enabled`; rejected on any other platform (only Google can put a number on the website — Microsoft's exist only inside the ad, in the US/UK/AU).
10
+ - **`site.tracking.phoneNumbers`** — per-platform numbers for every other platform: the real `number`, `sources` keyed by `site.ads` platform (plus `default-paid`), and an informational `provider`. You provision the numbers with a call-tracking service (Twilio, CallRail, …). The landing URL decides first, then the `_ac` cookie, so a visitor who declines `marketing` sees a tracking number on the landing page only. An organic visitor is never swapped, and the server-rendered HTML always carries the real number.
11
+ - The `tel:` tap beacon now carries which number was shown (`phoneSource`), and the conversion notification says so.
12
+ - `vitops ads lint` flags a call-tracking provider that is not in `site.legal.privacyPolicy.processors`, and notes that `callTracking` plus `conversionLabel.call` can count one call twice (make one secondary in Google Ads).
13
+ - `@getvitops/utils/tracking` exports the pure `selectPhoneNumber()` and helpers; the capture script mirrors it and `tracking.test.ts` pins the two.
14
+ - `@getvitops/skills` gains a **`vitops-ads`** skill (overview plus a `call-tracking.md` topic). The templates gitignore synced skills by prefix (`vitops-*`, `emdash-*`), so it needs no `.gitignore` line of its own.
15
+
16
+ `@getvitops/core`, `@getvitops/vite` and `@getvitops/emdash` — no consumer-facing change in these packages.
17
+
18
+ - 4d94c8b: **Added: `site.chrome` — a configurable masthead and nav behaviour.** Non-breaking.
19
+
20
+ - `site.chrome.masthead` (new `@getvitops/config` schema): `position` (`static|sticky|fixed`), `behavior` (`always|hide-on-scroll`), `surface` (`solid|glass|transparent|transparent-until-scrolled`), `tone` (a `colors.roles` key), `layout` (`full|inset|floating`), `width`, `elevation` (`shadow`, `border`, `onScroll`), `height` and `inset`. `height` and `inset` take fluid lengths (`calc()`, `clamp()`, `min()`, `max()`, with `cqi`/`vw`/… units), checked against a closed grammar because they are written into CSS. `validateConfig` rejects a `tone` or `shadow` the design system does not define (only when the default theme states them directly, not via `extends`).
21
+ - `site.chrome.nav`: `compact` (`drawer|dropdown`), `drawerSide`, `breakpoint`/`mobileBreakpoint`/`megaBreakpoint`, `depth`, `megaDepth`.
22
+ - `<Masthead config={site}>` (`@getvitops/astro`, slots `brand`/default/`actions`) and `resolveMasthead()` / `resolveNavOptions()`. A prop overrides one field per layout. The key is `chrome`/`masthead` on purpose: `<header>` is valid in any section and `banner` already means the announcement bar and the landmark role.
23
+ - `<SiteNav>` gains `compact="dropdown"` (a columnar panel anchored under the toggle), `drawerSide` (previously hardcoded to the end edge in Astro), `megaDepth` and `megaPanel`. `.sitenav--mega` now styles second-level items as column headings and has an optional featured column.
24
+ - `@getvitops/emdash`: `VitopsMegaPanel` fills that column from the widget area `mega-<item url slug>` (or a `mega-area:<name>` class on the menu item). No area means no column.
25
+ - `@getvitops/create` (emdash template): the layout renders through `<Masthead>`; `site.json` ships a sticky glass masthead with a shadow once scrolled. The hand-written `.navbar { block-size: 64px }` is gone (use `masthead.height`).
26
+ - CSS (`patterns/masthead.css`, `patterns/sitenav.css`): **all formats**, including tailwind, where the patterns are inlined into `@layer components`. Hide-on-scroll and the `*-on-scroll` states are `scroll-state()` container queries that only subtract from a settled (solid, elevated, visible) bar, so a browser without them (Firefox today) shows that bar rather than a transparent one. A sticky or fixed masthead sets `--header-offset` / `--top-offset` so anchors land below it.
27
+ - `@getvitops/generator`: new `TIERS.masthead`; `TIERS.sitenav` documents the new props.
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies [785b8b3]
32
+ - Updated dependencies [bb61a35]
33
+ - Updated dependencies [4d94c8b]
34
+ - @getvitops/config@8.3.0-alpha.5
35
+ - @getvitops/utils@8.3.0-alpha.5
36
+
37
+ ## 8.3.0-alpha.4
38
+
39
+ ### Minor Changes
40
+
41
+ - 99e66a2: Fixes from the vitops.ca build (alpha.3 findings 1–5, 19, 29).
42
+
43
+ - **Scaffold patterns.** Both templates now define `cta`, `link` and `details` in `patterns.items` (a `content` group too), and a `ui-accent` role. `.cta` had no CSS at all, so every hero, card and callout button rendered as plain text; a test now fails if a template omits a pattern the blocks emit. `vitops_accordion` renders a sprite chevron like `vitops_details`, and `details.css` styles an `.accordion` group (a divider and padded summaries from the `details` pattern tokens) — all formats.
44
+ - **Tone paints only on surfaces.** `.tone` now sets colour variables and `border-color`; the background comes from `.tone--tint` (callout, panel card, hero card, toned section/hero/nav/reviews, `theme`) or `.tone--solid`. A steps, stats or form block with a tone is no longer turned into a band. `ResolvedTone` gains `surface` (`tone tone--tint`). **If you styled `.tone` directly,** add `tone--tint` where you want the background. A `.cta` inside a tinted or filled tone takes the tone's solid. all formats.
45
+ - **`colors.tint: { mix: "8%" }`** derives the tint from the role's solid (`--tone-tint`) instead of its `muted` step, which turns brown for amber on a dark page. Off by default; not covered by the contrast contract.
46
+ - **Per-card backdrop.** `vitops_cards` and `vitops_hero` rows take a `theme` (tint or fill of a site colour), so one card of a pair can be tinted.
47
+ - **`colors.roles.<r>.acknowledgeContrast`**: a number of Lc a pairing may miss its APCA bar by and still only warn (e.g. pine's solid at Lc 59.5 against 60). A larger miss still fails.
48
+ - **Select:** where `appearance: base-select` is supported the wrapper's own chevron is hidden, so a select no longer shows two arrows, and the wrapper and control fill the field (`.custom-select` is `display: block`; add `display: inline-block` if you used it inline). all formats.
49
+
50
+ - d64fb20: Layout and spacing fixes from the vitops.ca build (findings 6–9).
51
+
52
+ - **`vitops_section` is the page opener.** New fields `cta_label`/`cta_href`/`secondary_label`/`secondary_href` (a `.cta` and a `.link` in the head), `*accent*` runs in the heading, and `stack: rhythm`, which puts `.rhythm` on `.section__inner` so head and body use the vertical-rhythm scale. With level 1 it replaces nesting a `vitops_hero` in a section, which rendered `section > .centered > … > section > .centered`. `vitops_hero` stays for a stand-alone hero with option cards. A plain (`stack: grid`) section also puts a gap between head and body now (`--section-gap`, default 2rem) instead of sitting flush. css/bricks/tailwind.
53
+ - **`.rhythm` spaces blocks.** A card row (`wc-cards`, classed `ul`/`ol`), accordion, `details`, form, table, figure or `.section__body` directly under a `.rhythm` is `--rhythm-block` (2) apart, not a paragraph apart; a button row (`p.cluster`) is `--rhythm-block-action` (1.6). Classless lists stay prose and a form's own field spacing is unchanged. A bare `<a>` that is a direct child of `.rhythm` is now `display: block; inline-size: fit-content`, since an inline box ignored `margin-top` and sat flush under the paragraph above (`.cta` and `.btn` are unaffected). all formats.
54
+ - **`rhythm-flex`** utility: an opt-in flex-column stack for the case the margin stack does not suit; margins apply to any child. all formats (`@utility` in tailwind).
55
+
56
+ - 322dd6a: Block fixes from the vitops.ca build (findings 11–13, 15, 17, 18, 21).
57
+
58
+ - **Steps.** The marker is an outlined disc in the tone (number and ring in `--tone-solid`, falling back to `currentColor`), so a steps block with no tone no longer shows black discs with no number. Items keep `align-content: start` and no `.rhythm li + li` margin, so markers in a row line up. Two modifiers through the block's `className`: `steps--cards` (each step in a card) and `steps--animated` (the connector draws and the marker fills as each step scrolls in; static under reduced motion or without scroll-driven animations). `steps` is now a config pattern: `patterns.items.steps` in `site.json` owns the card geometry (`content` group tokens) and the marker size (`--steps-marker-size`, `--steps-line-width`); both templates and the reference config ship an entry. all formats.
59
+ - **Stats.** `<wc-counter>` leaves a figure it cannot count as authored (`"1 in 6"`, `"24/7"`, `"—"`) instead of counting down from zero to a wrong value. `animate: false` on the block still turns the count-up off for a whole block.
60
+ - **Logos.** `vitops_logos` takes an optional `kicker`, `note` and `linkLabel`/`linkHref`; with any of them set, the row is wrapped in `.logos-block`. The list keeps the block's attributes either way.
61
+ - **Pricing.** Plans take `yearly` (a second price line) and `note` (under the plan name).
62
+ - **`<SiteFooter>`** has a `brand` named slot, rendered as the first column (`.sitefooter__brand`) in the site's own markup.
63
+
64
+ ### Patch Changes
65
+
66
+ - 322dd6a: Tooling fixes from the vitops.ca build (findings 14, 23, 24, 25).
67
+
68
+ - **`vitops emdash form-template --from <seed.json | export.json> [--name contact] [--site site.json] [--dry]`** copies a `vitops_form` block into `site.templates` with the renderer's own field resolver, so the generated privacy policy and `check-prerender` know what the form collects. A re-run is a no-op, and the result is validated before it is written.
69
+ - `site.templates` form fields accept `checkboxes` (the block's spelling), and the template union now discriminates on `type`, so a bad entry is reported at its path instead of as a bare "Invalid input".
70
+ - **`prepare-db` fails fast and says why.** It validates `site.json` before starting the dev server, writes the server's output to `.vitops/prepare-db.log`, stops waiting as soon as the log shows an error, and prints the log's tail on failure, instead of waiting out a five-minute timeout with nothing to read.
71
+ - **`check-prerender`** skips the toolkit's own assets under `vitops/` (no more "0 h1" for the theme-editor page) and warns when the sitemap lists URLs the build produced no page for (a collection with no `urlPattern` is listed as `/<collection>/<slug>` while the site serves `/<slug>`).
72
+ - The emdash template ignores the duplicate `pnpm-@getvitops+skills-*` links `skills experimental_sync` leaves beside each skill; the `content-and-deploys` skill explains the sitemap warning.
73
+
74
+ - Updated dependencies [00a9d86]
75
+ - @getvitops/utils@8.3.0-alpha.4
76
+ - @getvitops/config@8.3.0-alpha.4
77
+
3
78
  ## 8.3.0-alpha.3
4
79
 
5
80
  ### Patch Changes
@@ -40,6 +40,7 @@
40
40
  /* navbar before sitenav: sitenav's wide state composes it. */
41
41
  @import './patterns/navbar.css';
42
42
  @import './patterns/sitenav.css';
43
+ @import './patterns/masthead.css';
43
44
  /* After sitenav (the nav family stays contiguous) and after drawer.css, whose
44
45
  panel surface navshell's desktop block has to undo. */
45
46
  @import './patterns/navshell.css';
@@ -283,6 +283,19 @@
283
283
  margin-top: 0;
284
284
  }
285
285
 
286
+ /* Opt-in second stack: add `rhythm-flex` beside `rhythm` (a block's `className`) where the margin
287
+ stack fails. Flex items are blockified, so margins apply to every child, bare links included.
288
+ No `gap`: the `.rhythm` pair rules still decide the spacing. */
289
+ .rhythm-flex {
290
+ display: flex;
291
+ flex-direction: column;
292
+ align-items: flex-start;
293
+ }
294
+ .rhythm-flex
295
+ > :is(wc-cards, ul[class], ol[class], details, form, table, figure, .accordion, .section__body) {
296
+ align-self: stretch;
297
+ }
298
+
286
299
  /* ── Flex direction (display + direction; tiered classes set both) ── */
287
300
  .flex {
288
301
  display: flex;
@@ -36,6 +36,8 @@
36
36
  --rhythm-li-li: 0.3;
37
37
  --rhythm-text-media: 1.2;
38
38
  --rhythm-media-text: 1.2;
39
+ --rhythm-block: 2; /* a card row, list, accordion or form under prose */
40
+ --rhythm-block-action: 1.6; /* a button row after the lead */
39
41
  }
40
42
 
41
43
  /* .rhythm establishes a flow-root: contains child top-margins (no collapse-out)
@@ -174,6 +176,27 @@
174
176
  margin-top: calc(var(--rhythm-base) * var(--rhythm-media-text));
175
177
  }
176
178
 
179
+ /* Block-level children are a block gap apart, not a paragraph apart: a card row, a stats list, an
180
+ accordion or a form under a lead sat flush at the prose spacing. `:is()` takes its most specific
181
+ argument (0-1-1), so this beats the paragraph-to-list pairs above (0-1-2 would not tie it).
182
+ Classless `ul`/`ol` stay prose. A form's own `.rhythm` holds `div` fields, which are not listed,
183
+ so its tight field spacing is untouched. */
184
+ .rhythm
185
+ > *
186
+ + :is(wc-cards, ul[class], ol[class], details, form, table, figure, .accordion, .section__body) {
187
+ margin-top: calc(var(--rhythm-base) * var(--rhythm-block));
188
+ }
189
+ /* A row of buttons after the lead. */
190
+ .rhythm > * + p.cluster {
191
+ margin-top: calc(var(--rhythm-base) * var(--rhythm-block-action));
192
+ }
193
+ /* A bare link is inline, and an inline box ignores `margin-top`, so it sat flush under the
194
+ paragraph above. Buttons (`.cta`, `.btn`) are inline-flex and already space. */
195
+ .rhythm > :where(a:not(.cta, .btn)) {
196
+ display: block;
197
+ inline-size: fit-content;
198
+ }
199
+
177
200
  /* ═══════════════════════════════════════════════════════════════════════════
178
201
  4. CENTERED SECTIONS — named-track grid
179
202
  Put .centered on a Div. Children default to the measure (reading) track;
@@ -529,13 +529,20 @@ wc-carousel {
529
529
  margin: var(--carousel-hint-inset, 0.5rem);
530
530
  z-index: var(--z-tier-raised);
531
531
  pointer-events: none;
532
- color: var(--color-text-surface-x-muted);
532
+ /* `muted`, not `x-muted`: the hint is visible text, and `x-muted` is the placeholder tier the
533
+ contrast contract exempts (axe flagged it at 1.34:1). */
534
+ color: var(--color-text-surface-muted);
533
535
  transition: opacity 0.3s ease;
534
536
  }
535
537
 
538
+ /* The hint fades out over the first 12% of scroll and is then hidden outright: an element left at
539
+ 0.2 opacity is still text to an audit (axe reported 1.34:1), and once the visitor has scrolled it
540
+ has nothing left to say. `visibility` flips only at the end of the range, so the fade still shows.
541
+ Set --carousel-hint-dimmed above 0 to keep a ghost of it instead. */
536
542
  @keyframes carousel-hint-dim {
537
543
  to {
538
- opacity: var(--carousel-hint-dimmed, 0.2);
544
+ opacity: var(--carousel-hint-dimmed, 0);
545
+ visibility: hidden;
539
546
  }
540
547
  }
541
548
 
@@ -116,3 +116,17 @@ details,
116
116
  rotate: none;
117
117
  }
118
118
  }
119
+
120
+ /* ── Accordion group (`vitops_accordion`) ───────────────────────────────────
121
+ A stack of `<details>`: a divider between items and padded summaries, both from the
122
+ `details` pattern's group tokens so a site restyles them through `patterns.items.details`. */
123
+ .accordion {
124
+ > details + details {
125
+ border-block-start: var(--b-details-group, var(--b-default, 1px solid currentColor));
126
+ }
127
+
128
+ > details > summary {
129
+ padding-block: var(--p-details-group, var(--p-default, 1rem));
130
+ gap: 0.75rem;
131
+ }
132
+ }
@@ -333,7 +333,7 @@ input[type='color'] {
333
333
  * `select` (`select.custom-select`); style both so the control radius applies. */
334
334
  .custom-select {
335
335
  position: relative;
336
- display: inline-block;
336
+ display: block;
337
337
 
338
338
  &::after {
339
339
  content: '';
@@ -349,6 +349,19 @@ input[type='color'] {
349
349
  pointer-events: none;
350
350
  opacity: 0.5;
351
351
  }
352
+
353
+ /* The wrapper fills its field, and so does the control inside it. */
354
+ > select {
355
+ inline-size: 100%;
356
+ }
357
+ }
358
+
359
+ /* `appearance: base-select` draws its own arrow (`::picker-icon`); the wrapper's would be a second
360
+ chevron at the far edge. It only stands in where base-select is unsupported. */
361
+ @supports (appearance: base-select) {
362
+ .custom-select:has(> select)::after {
363
+ content: none;
364
+ }
352
365
  }
353
366
 
354
367
  .custom-select select,
@@ -63,3 +63,17 @@
63
63
  opacity: 1;
64
64
  }
65
65
  }
66
+
67
+ /* The optional frame: a line above the row, and a closing note and link below it. */
68
+ .logos-block {
69
+ display: grid;
70
+ justify-items: center;
71
+ gap: var(--logos-block-gap, 1.25rem);
72
+ text-align: center;
73
+
74
+ .logos__kicker,
75
+ .logos__note,
76
+ .logos__link {
77
+ margin: 0;
78
+ }
79
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Masthead — the site's top bar: brand, navigation, actions. Pure CSS (tier 1); `<Masthead>`
3
+ * (`@getvitops/astro`) only emits the classes below from `site.chrome.masthead`.
4
+ *
5
+ * <header class="masthead masthead--sticky masthead--glass masthead--shadow-on-scroll">
6
+ * <div class="masthead__row navbar centered">brand · nav · actions</div>
7
+ * </header>
8
+ *
9
+ * Named "masthead", not "header"/"banner": `<header>` is valid in any section, and `.banner` is
10
+ * the announcement bar. The element is a plain `<header>` with no explicit `role`.
11
+ *
12
+ * ── SAFE BASE, SUBTRACTIVE ENHANCEMENTS ──
13
+ * The un-modified rule is the settled state: visible, solid (or glass), elevated, in flow.
14
+ * Scroll behaviour is layered on with `scroll-state()` container queries that only ever
15
+ * SUBTRACT from it — `*-on-scroll` clears the surface/shadow/border while the page is at the
16
+ * top; `hide-on-scroll` slides the bar away. Where the query is unsupported (Firefox today)
17
+ * nothing subtracts, so the bar stays visible, solid and elevated: a query can change how the
18
+ * bar looks, never leave it unreadable or take the nav away. (Writing these the other way
19
+ * round — base clear, query reveals — leaves an unsupporting browser with a transparent bar
20
+ * over body text forever.)
21
+ *
22
+ * Hooks (all optional custom properties): `--masthead-height`, `--masthead-bg`, `--masthead-fg`,
23
+ * `--masthead-blur`, `--masthead-glass-opacity`, `--masthead-inset`, `--masthead-shadow`,
24
+ * `--masthead-border`. `site.chrome.masthead` sets `--masthead-height` and `--masthead-inset`
25
+ * on `:root` (see `mastheadRootCss()`), so a clamp()/cqi length there reaches `--header-offset`.
26
+ */
27
+
28
+ /* Zero-specificity colour defaults, so a `.tone` (or any class) on the element wins. */
29
+ :where(.masthead) {
30
+ --_masthead-base: var(--tone-bg, var(--color-bg-surface));
31
+
32
+ background-color: var(--masthead-bg, var(--_masthead-base));
33
+ color: var(--masthead-fg, var(--tone-fg, var(--color-text-surface)));
34
+ }
35
+
36
+ .masthead {
37
+ --_masthead-h: var(--masthead-height, 4rem);
38
+ --_masthead-inset: 0px;
39
+
40
+ position: relative;
41
+ z-index: var(--z-tier-sticky);
42
+ display: block;
43
+ box-sizing: border-box;
44
+ transition:
45
+ translate 0.25s ease,
46
+ background-color 0.25s ease,
47
+ box-shadow 0.25s ease,
48
+ backdrop-filter 0.25s ease;
49
+ }
50
+
51
+ .masthead__row {
52
+ display: block;
53
+ }
54
+
55
+ /* The row's flex child carries the height; `.centered` (when used) is the grid around it. */
56
+ .masthead__row > .navbar {
57
+ min-block-size: var(--_masthead-h);
58
+ }
59
+
60
+ /* `width: full` has no track to supply a gutter. */
61
+ .masthead__row:not(.centered) > .navbar {
62
+ padding-inline: var(--masthead-gutter, 1rem);
63
+ }
64
+
65
+ /* ── Position ──────────────────────────────────────────────────────────────── */
66
+ .masthead--sticky {
67
+ position: sticky;
68
+ inset-block-start: var(--_masthead-inset);
69
+ }
70
+
71
+ /* Out of flow: overlays the first section. Pair with `--transparent*`. */
72
+ .masthead--fixed {
73
+ position: fixed;
74
+ inset-block-start: var(--_masthead-inset);
75
+ inset-inline: 0;
76
+ }
77
+
78
+ /* Anchors and `scroll-padding` land below a pinned masthead. Sticky keeps its space in
79
+ flow, fixed does not, but both cover the top of the viewport. */
80
+ :root:has(.masthead--sticky, .masthead--fixed) {
81
+ --header-offset: calc(var(--masthead-height, 4rem) + var(--masthead-inset, 0px));
82
+ --top-offset: var(--header-offset);
83
+ }
84
+
85
+ /* ── Surface ───────────────────────────────────────────────────────────────── */
86
+ .masthead--glass {
87
+ background-color: color-mix(
88
+ in oklch,
89
+ var(--masthead-bg, var(--_masthead-base)) var(--masthead-glass-opacity, 72%),
90
+ transparent
91
+ );
92
+ backdrop-filter: blur(var(--masthead-blur, 12px));
93
+ -webkit-backdrop-filter: blur(var(--masthead-blur, 12px));
94
+ }
95
+
96
+ /* A browser without backdrop-filter would show page content through a tint. Go solid. */
97
+ @supports not (backdrop-filter: blur(1px)) {
98
+ .masthead--glass {
99
+ background-color: var(--masthead-bg, var(--_masthead-base));
100
+ }
101
+ }
102
+
103
+ .masthead--transparent {
104
+ background-color: transparent;
105
+ }
106
+
107
+ /* ── Layout ────────────────────────────────────────────────────────────────── */
108
+ .masthead--inset,
109
+ .masthead--floating {
110
+ --_masthead-inset: var(--masthead-inset, 0.75rem);
111
+
112
+ margin-inline: var(--_masthead-inset);
113
+ margin-block-start: var(--_masthead-inset);
114
+ }
115
+
116
+ /* `fixed` has no flow to margin against: pin the box in by its insets instead. */
117
+ .masthead--fixed.masthead--inset,
118
+ .masthead--fixed.masthead--floating {
119
+ margin: 0;
120
+ inset-inline: var(--_masthead-inset);
121
+ }
122
+
123
+ .masthead--inset {
124
+ border-radius: var(--br-control, 0.375rem);
125
+ }
126
+
127
+ .masthead--floating {
128
+ border-radius: var(--br-panel, 1rem);
129
+ overflow: clip;
130
+ }
131
+
132
+ /* ── Elevation ─────────────────────────────────────────────────────────────── */
133
+ .masthead--shadow {
134
+ box-shadow: var(--masthead-shadow, var(--shadow-sm));
135
+ }
136
+
137
+ .masthead--border {
138
+ border-block-end: var(
139
+ --masthead-border,
140
+ 1px solid var(--tone-border, var(--color-border-surface))
141
+ );
142
+ }
143
+
144
+ /* A full outline once the masthead is detached from the viewport edge. */
145
+ .masthead--border:is(.masthead--inset, .masthead--floating) {
146
+ border: var(--masthead-border, 1px solid var(--tone-border, var(--color-border-surface)));
147
+ }
148
+
149
+ /* ── Scroll behaviour ───────────────────────────────────────────────────────
150
+ The page is the scroll-state container. Only when a masthead asks for one, so a
151
+ site that uses none pays nothing. */
152
+ :root:has(
153
+ .masthead--hide-on-scroll,
154
+ .masthead--transparent-until-scrolled,
155
+ .masthead--shadow-on-scroll,
156
+ .masthead--border-on-scroll
157
+ ) {
158
+ container: masthead-page / scroll-state;
159
+ }
160
+
161
+ /* At the top of the page, the *-on-scroll modifiers subtract their settled look. `not
162
+ scroll-state(scrollable: top)` is "nothing above the viewport", i.e. scrollY = 0. */
163
+ @container masthead-page not scroll-state(scrollable: top) {
164
+ .masthead--transparent-until-scrolled {
165
+ background-color: transparent;
166
+ /* A glass bar would otherwise blur the hero behind an otherwise clear bar. */
167
+ backdrop-filter: none;
168
+ -webkit-backdrop-filter: none;
169
+ }
170
+ .masthead--shadow-on-scroll {
171
+ box-shadow: none;
172
+ }
173
+ .masthead--border-on-scroll {
174
+ border-color: transparent;
175
+ }
176
+ }
177
+
178
+ /* Slide away while the last scroll was downward; back when it reverses. Never while the
179
+ visitor is inside it: focus, an open popover or an open disclosure keeps it on screen. */
180
+ @container masthead-page scroll-state(scrolled: block-end) {
181
+ .masthead--hide-on-scroll:not(:focus-within, :has(:popover-open, details[open])) {
182
+ translate: 0 calc(-100% - var(--_masthead-inset) - 1px);
183
+ }
184
+ }
185
+
186
+ @media (prefers-reduced-motion: reduce) {
187
+ .masthead {
188
+ transition: none;
189
+ }
190
+ }
191
+
192
+ /* Shadows and tints disappear in forced-colors; keep a real edge. */
193
+ @media (forced-colors: active) {
194
+ .masthead {
195
+ border-block-end: 1px solid CanvasText;
196
+ }
197
+ .masthead--inset,
198
+ .masthead--floating {
199
+ border: 1px solid CanvasText;
200
+ }
201
+ }
@@ -59,3 +59,13 @@
59
59
  .block-anchor {
60
60
  scroll-margin-top: var(--header-offset, 5rem);
61
61
  }
62
+
63
+ /* Head and body of a plain (stack) section sit a gap apart; split already has a grid gap and a
64
+ `rhythm` stack its own scale. */
65
+ .section:not(.section--split) .section__inner:not(.rhythm) > .section__head + .section__body {
66
+ margin-block-start: var(--section-gap, 2rem);
67
+ }
68
+
69
+ .section .section__actions {
70
+ align-items: center;
71
+ }
@@ -179,6 +179,77 @@
179
179
  }
180
180
  }
181
181
 
182
+ /* ── Compact dropdown — NARROW ONLY ─────────────────────────────────────────
183
+ `compact: 'dropdown'` swaps the edge drawer for a panel anchored under the toggle,
184
+ laid out as columns (each top-level item a column, its sub-links beneath). Same
185
+ markup; `navClass` emits `sitenav--panel-dropdown` INSTEAD of a `drawer-*` class,
186
+ so nothing here competes with the drawer's equal-specificity edge rules. Scoped to
187
+ the narrow state for the reason the whole file is (see "TWO BRANCHES"). */
188
+ .sitenav--panel-dropdown {
189
+ anchor-scope: --sitenav-toggle;
190
+ }
191
+ .sitenav--panel-dropdown > .sitenav__toggle {
192
+ anchor-name: --sitenav-toggle;
193
+ }
194
+
195
+ @container style(--_sitenav-wide: 0) {
196
+ .sitenav--panel-dropdown .sitenav__panel {
197
+ position-anchor: --sitenav-toggle;
198
+ position-area: block-end span-inline-start;
199
+ inset: auto;
200
+ margin: var(--sitenav-dropdown-gap, 0.5rem) 0;
201
+ inline-size: min(calc(100dvw - 1.5rem), var(--sitenav-dropdown-size, 36rem));
202
+ max-block-size: calc(100dvh - 6rem);
203
+ padding: 1.25rem;
204
+ border: var(--b-panel);
205
+ border-radius: var(--br-panel);
206
+ box-shadow: var(--ds-panel);
207
+ translate: 0 -0.5rem;
208
+ }
209
+ .sitenav--panel-dropdown .sitenav__panel:popover-open {
210
+ translate: 0 0;
211
+ }
212
+ @starting-style {
213
+ .sitenav--panel-dropdown .sitenav__panel:popover-open {
214
+ opacity: 0;
215
+ translate: 0 -0.5rem;
216
+ }
217
+ }
218
+ /* The dropdown is anchored, not modal: no close button, no dimmed page. */
219
+ .sitenav--panel-dropdown .sitenav__close {
220
+ display: none;
221
+ }
222
+ .sitenav--panel-dropdown .sitenav__panel {
223
+ padding-block-start: 1.25rem;
224
+ }
225
+ .sitenav--panel-dropdown .sitenav__panel::backdrop {
226
+ background: transparent;
227
+ }
228
+ .sitenav--panel-dropdown .sitenav__panel:popover-open::backdrop {
229
+ background: transparent;
230
+ }
231
+ .sitenav--panel-dropdown .sitenav__list {
232
+ display: grid;
233
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
234
+ gap: 1rem 1.5rem;
235
+ align-items: start;
236
+ }
237
+ /* Columns, not accordions: every sub-list is open, so the caret has nothing to do. */
238
+ .sitenav--panel-dropdown .sitenav__item--branch {
239
+ display: block;
240
+ }
241
+ .sitenav--panel-dropdown .sitenav__item--branch > .sitenav__submenu {
242
+ overflow: visible;
243
+ padding-inline-start: 0;
244
+ }
245
+ .sitenav--panel-dropdown .sitenav__disclosure {
246
+ display: none;
247
+ }
248
+ .sitenav--panel-dropdown .sitenav__list > .sitenav__item--branch > .sitenav__link {
249
+ font-weight: 600;
250
+ }
251
+ }
252
+
182
253
  /* ── Lists + links ─────────────────────────────────────────────────────────── */
183
254
  .sitenav__list,
184
255
  .sitenav__submenu {
@@ -534,6 +605,38 @@
534
605
  .sitenav--mega .sitenav__submenu .sitenav__disclosure {
535
606
  display: none;
536
607
  }
608
+ /* Second level reads as a column heading; its description (`titleAttr`) stays. */
609
+ .sitenav--mega
610
+ .sitenav__list
611
+ > .sitenav__item--branch
612
+ > .sitenav__submenu
613
+ > .sitenav__item--branch
614
+ > .sitenav__link {
615
+ font-weight: 600;
616
+ padding-inline: 0;
617
+ }
618
+ .sitenav--mega .sitenav__submenu .sitenav__item--branch > .sitenav__submenu {
619
+ padding-inline: 0;
620
+ }
621
+ /* Optional featured column (EmDash widget area via `megaPanel`). */
622
+ .sitenav--mega .sitenav__mega-extra {
623
+ flex: 1 1 14rem;
624
+ margin-inline-start: auto;
625
+ max-inline-size: 22rem;
626
+ padding-inline-start: 1.5rem;
627
+ border-inline-start: 1px solid var(--color-border-surface, currentColor);
628
+ }
629
+ .sitenav--mega .sitenav__mega-extra:empty {
630
+ display: none;
631
+ }
632
+ }
633
+
634
+ /* Extra column is a wide-mega feature: in a drawer (a mega menu that is the only tier) it
635
+ would be an unlabelled block between accordions. Empty (no widget area) shows nothing. */
636
+ @container style(--_sitenav-wide: 0) {
637
+ .sitenav__mega-extra {
638
+ display: none;
639
+ }
537
640
  }
538
641
 
539
642
  /* ── In-page menu (`vitops_nav` block) ─────────────────────────────────────────
@@ -638,6 +741,14 @@
638
741
  justify-content: space-between;
639
742
  }
640
743
 
744
+ .sitefooter__brand {
745
+ flex: 1 1 16rem;
746
+ max-inline-size: 26rem;
747
+ display: grid;
748
+ gap: 0.75rem;
749
+ align-content: start;
750
+ }
751
+
641
752
  .sitefooter__list {
642
753
  list-style: none;
643
754
  margin: 0;