@getvitops/generator 8.2.0-alpha.0 → 8.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +550 -25
  2. package/assets/css/index.css +7 -0
  3. package/assets/css/patterns/banner.css +20 -0
  4. package/assets/css/patterns/callout.css +39 -0
  5. package/assets/css/patterns/copy.css +5 -0
  6. package/assets/css/patterns/embed.css +39 -0
  7. package/assets/css/patterns/forms.css +19 -0
  8. package/assets/css/patterns/logos.css +45 -0
  9. package/assets/css/patterns/marquee.css +16 -0
  10. package/assets/css/patterns/section.css +53 -0
  11. package/assets/css/patterns/sitenav.css +262 -0
  12. package/assets/css/patterns/splitter.css +11 -0
  13. package/assets/css/patterns/stats.css +61 -0
  14. package/assets/css/patterns/steps.css +98 -0
  15. package/assets/css/patterns/table.css +10 -0
  16. package/assets/css/patterns/tone.css +38 -0
  17. package/assets/js/consent.js +1 -1
  18. package/assets/js/elements.js +5 -5
  19. package/assets/js/polyfills/adoptedStyleSheets.js +1 -1
  20. package/assets/js/polyfills/css-anchor-positioning-fn.js +2 -2
  21. package/assets/js/polyfills/popover.js +2 -2
  22. package/config.schema.json +257 -120
  23. package/dist/config-BfcWKH-5.mjs +1743 -0
  24. package/dist/config-BfcWKH-5.mjs.map +1 -0
  25. package/dist/{config-DvbYx3Gi.mjs → config-Bhg3Xv3D.mjs} +10 -207
  26. package/dist/config-Bhg3Xv3D.mjs.map +1 -0
  27. package/dist/config-D5xuz8--.mjs +1807 -0
  28. package/dist/config-D5xuz8--.mjs.map +1 -0
  29. package/dist/config.d.mts +157 -211
  30. package/dist/config.d.mts.map +1 -1
  31. package/dist/config.mjs +1 -1
  32. package/dist/{icons-sprite-Bf_KmKeX.d.mts → icons-sprite-CQ5Zj4go.d.mts} +4 -6
  33. package/dist/icons-sprite-CQ5Zj4go.d.mts.map +1 -0
  34. package/dist/icons-sprite.d.mts +1 -1
  35. package/dist/icons-sprite.mjs +1 -2
  36. package/dist/index.d.mts +49 -35
  37. package/dist/index.d.mts.map +1 -1
  38. package/dist/index.mjs +607 -75
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{schema-Dmq1J7S3.d.mts → schema-BV2n2phj.d.mts} +2 -3
  41. package/dist/{schema-Dmq1J7S3.d.mts.map → schema-BV2n2phj.d.mts.map} +1 -1
  42. package/dist/schema-C-P9dF9F.d.mts +337 -0
  43. package/dist/schema-C-P9dF9F.d.mts.map +1 -0
  44. package/dist/{tokens-8SXMDAd1.d.mts → tokens-BzBbTeTi.d.mts} +2 -6
  45. package/dist/tokens-BzBbTeTi.d.mts.map +1 -0
  46. package/dist/{tokens-zi594IZ3.mjs → tokens-C1RJwFeG.mjs} +3 -5
  47. package/dist/{tokens-zi594IZ3.mjs.map → tokens-C1RJwFeG.mjs.map} +1 -1
  48. package/dist/tokens-CaIGQQLk.mjs +740 -0
  49. package/dist/tokens-CaIGQQLk.mjs.map +1 -0
  50. package/dist/tokens-DH5cpvKd.d.mts +242 -0
  51. package/dist/tokens-DH5cpvKd.d.mts.map +1 -0
  52. package/dist/tokens.d.mts +1 -1
  53. package/dist/tokens.mjs +1 -1
  54. package/package.json +6 -5
  55. package/schema.json +95 -5
  56. package/dist/config-DvbYx3Gi.mjs.map +0 -1
  57. package/dist/icons-sprite-Bf_KmKeX.d.mts.map +0 -1
  58. package/dist/tokens-8SXMDAd1.d.mts.map +0 -1
@@ -72,6 +72,13 @@
72
72
  @import './patterns/text-effects.css';
73
73
  @import './patterns/svg.css';
74
74
  @import './patterns/marquee.css';
75
+ @import './patterns/tone.css';
76
+ @import './patterns/section.css';
77
+ @import './patterns/steps.css';
78
+ @import './patterns/stats.css';
79
+ @import './patterns/callout.css';
80
+ @import './patterns/logos.css';
81
+ @import './patterns/embed.css';
75
82
  @import './patterns/counter.css';
76
83
  @import './patterns/nav.css';
77
84
  @import './patterns/carousel.css';
@@ -69,3 +69,23 @@
69
69
  .banner[hidden] {
70
70
  display: none;
71
71
  }
72
+
73
+ /*
74
+ * `<wc-dismissable>` wraps a banner or notification — block content — and animates its own
75
+ * opacity/scale on dismiss, so it needs a box of its own: `display: contents` would drop
76
+ * that animation. Pre-upgrade it would otherwise be an inline box around a block child.
77
+ */
78
+ :where(wc-dismissable) {
79
+ display: block;
80
+ }
81
+
82
+ /*
83
+ * A dismiss button does nothing until `<wc-dismissable>` upgrades — an inert control that
84
+ * looks live is worse than none. `:not(:defined)` is true with no JS (and until elements.js
85
+ * has run), so the close affordance only appears once something can act on it. The banner
86
+ * itself stays fully readable. Markup that puts a `[data-dismiss]` button inside the element
87
+ * gets the same treatment, whatever its class.
88
+ */
89
+ wc-dismissable:not(:defined) :is(.banner__close, .notification__close, [data-dismiss]) {
90
+ display: none;
91
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Callout — a call-to-action band: a short message and the action it points at, set
3
+ * apart from the page. Not a banner: `banner` is a dismissible announcement; this is
4
+ * content that stays.
5
+ *
6
+ * Message left and actions right when there is room, stacked when there is not. Colour
7
+ * is `.tone` (tone.css); `--center` stacks and centres at every width.
8
+ */
9
+ .callout {
10
+ display: grid;
11
+ gap: var(--callout-gap, 1rem 2rem);
12
+ align-items: center;
13
+ padding: var(--callout-padding, clamp(1.25rem, 4cqi, 2.5rem));
14
+ border: var(--b-card, var(--b-card-group, var(--b-default)));
15
+ border-radius: var(--br-card, var(--br-card-group, var(--br-default)));
16
+
17
+ .callout__body {
18
+ display: grid;
19
+ gap: 0.5rem;
20
+ min-inline-size: 0;
21
+ }
22
+
23
+ .callout__title,
24
+ .callout__text,
25
+ .callout__actions {
26
+ margin: 0;
27
+ }
28
+
29
+ &.callout--center {
30
+ text-align: center;
31
+ justify-items: center;
32
+ }
33
+ }
34
+
35
+ @container (min-width: 48rem) {
36
+ .callout:not(.callout--center) {
37
+ grid-template-columns: minmax(0, 1fr) auto;
38
+ }
39
+ }
@@ -80,3 +80,8 @@
80
80
  visibility: visible;
81
81
  }
82
82
  }
83
+
84
+ /* Matches `<wc-copy>`'s upgraded `:host` (inline-flex) so registering causes no reflow. */
85
+ :where(wc-copy) {
86
+ display: inline-flex;
87
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Embed — a third-party iframe (map, video) held at a fixed aspect ratio, behind consent.
3
+ *
4
+ * The iframe ships with NO `src`: the address rides on `data-consent-src`, and the consent
5
+ * runtime assigns it only once the visitor has granted the category. Until then the
6
+ * `.embed__placeholder` — a real link to the same content, so it is useful with no JS — is
7
+ * what the page shows; `[data-vitops-activated]` on the iframe (set by the runtime) hides
8
+ * it. No third-party request is made before consent.
9
+ */
10
+ .embed {
11
+ position: relative;
12
+ aspect-ratio: var(--embed-ratio, 16 / 9);
13
+ overflow: hidden;
14
+ border: var(--b-card, var(--b-card-group, var(--b-default)));
15
+ border-radius: var(--br-card, var(--br-card-group, var(--br-default)));
16
+
17
+ .embed__frame {
18
+ position: absolute;
19
+ inset: 0;
20
+ inline-size: 100%;
21
+ block-size: 100%;
22
+ border: 0;
23
+ }
24
+
25
+ .embed__placeholder {
26
+ position: absolute;
27
+ inset: 0;
28
+ display: grid;
29
+ place-content: center;
30
+ gap: 0.5rem;
31
+ padding: 1rem;
32
+ text-align: center;
33
+ }
34
+
35
+ /* Once the runtime has loaded the frame, the placeholder has nothing left to say. */
36
+ .embed__frame[data-vitops-activated] ~ .embed__placeholder {
37
+ display: none;
38
+ }
39
+ }
@@ -364,3 +364,22 @@ select.custom-select {
364
364
  outline-offset: 2px;
365
365
  }
366
366
  }
367
+
368
+ /* `<wc-multi-field>` is a group of controls, a block. Its shadow `:host` declares no display. */
369
+ :where(wc-multi-field) {
370
+ display: block;
371
+ }
372
+
373
+ /* `<wc-turnstile>` wraps a fallback paragraph and the widget box, both blocks (light DOM, no `:host`). */
374
+ :where(wc-turnstile) {
375
+ display: block;
376
+ }
377
+
378
+ /*
379
+ * A message shown only when the URL fragment names it. A prerendered page cannot know whether a
380
+ * no-JS post succeeded, so a form's success and error notices are always in the markup and the
381
+ * endpoint's redirect (`#form-sent` / `#form-error`) reveals one.
382
+ */
383
+ .form-notice--target:not(:target) {
384
+ display: none;
385
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Logos — a static grid of partner/client marks. `marquee` is the scrolling version;
3
+ * this one holds still, which is what most "key partners" rows should do (nothing to
4
+ * pause, nothing to track, no motion to opt out of).
5
+ *
6
+ * Each item is a plain image, or a link around one. Marks are normalised to a common
7
+ * height rather than a common width, because logos vary far more in aspect ratio than
8
+ * in how tall they should read. `--grayscale` quiets them until hover or focus.
9
+ */
10
+ .logos {
11
+ display: flex;
12
+ flex-wrap: wrap;
13
+ align-items: center;
14
+ justify-content: var(--logos-justify, center);
15
+ gap: var(--logos-gap, 1.5rem 2.5rem);
16
+ margin: 0;
17
+ padding: 0;
18
+ list-style: none;
19
+
20
+ .logos__item {
21
+ display: grid;
22
+ place-items: center;
23
+ margin: 0;
24
+ }
25
+
26
+ .logos__item img {
27
+ block-size: var(--logos-height, 2.5rem);
28
+ inline-size: auto;
29
+ max-inline-size: 100%;
30
+ object-fit: contain;
31
+ }
32
+
33
+ &.logos--grayscale .logos__item img {
34
+ filter: grayscale(1);
35
+ opacity: 0.7;
36
+ transition:
37
+ filter 0.2s,
38
+ opacity 0.2s;
39
+ }
40
+
41
+ &.logos--grayscale .logos__item:is(:hover, :focus-within) img {
42
+ filter: none;
43
+ opacity: 1;
44
+ }
45
+ }
@@ -56,6 +56,13 @@
56
56
  animation-play-state: paused;
57
57
  }
58
58
 
59
+ /* Keyboard users can't hover; focusing anything inside (a link in an item) also pauses, and
60
+ `<wc-marquee>`'s pause button sets `data-paused` (WCAG 2.2.2). */
61
+ &:focus-within .marquee__content,
62
+ &[data-paused] .marquee__content {
63
+ animation-play-state: paused;
64
+ }
65
+
59
66
  &.marquee--reverse {
60
67
  --_marquee-direction: reverse;
61
68
  }
@@ -100,3 +107,12 @@
100
107
  translate: 0 calc(-1 * var(--_marquee-distance, calc(100% + var(--marquee-gap, 1rem))));
101
108
  }
102
109
  }
110
+
111
+ /*
112
+ * The `.marquee` class carries `display: flex` when present (the Astro component always sets
113
+ * it). This is the fallback for a hand-written `<wc-marquee>` without the class, and it is
114
+ * zero specificity so `.marquee` still wins.
115
+ */
116
+ :where(wc-marquee) {
117
+ display: block;
118
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Section — the editorial wrapper most pages are made of: an optional kicker, a heading,
3
+ * a lead-in line, then a body of other blocks.
4
+ *
5
+ * Composed from the layout primitives rather than reinventing them: the outer element
6
+ * is `.region` (vertical rhythm) and the content sits on `.centered`'s tracks, so this
7
+ * partial only adds what neither has — the head's own rhythm and the `--split` layout,
8
+ * which puts the head beside the body (heading-left) above the container breakpoint.
9
+ * Backdrop and colour come from `.tone` (tone.css), never from here.
10
+ */
11
+ .section {
12
+ .section__head {
13
+ display: grid;
14
+ gap: var(--section-head-gap, 0.75rem);
15
+ align-content: start;
16
+ }
17
+
18
+ .section__kicker,
19
+ .section__title {
20
+ margin: 0;
21
+ }
22
+
23
+ .section__sub {
24
+ margin: 0;
25
+ max-inline-size: var(--width-measure, 65ch);
26
+ }
27
+
28
+ .section__body {
29
+ display: grid;
30
+ gap: var(--section-body-gap, 1.5rem);
31
+ min-inline-size: 0;
32
+ }
33
+
34
+ /* Head and body stack until there is room for two columns (below). */
35
+ &.section--split .section__inner {
36
+ display: grid;
37
+ gap: var(--section-split-gap, clamp(1.5rem, 4cqi, 3rem));
38
+ }
39
+
40
+ &.section--center .section__head {
41
+ text-align: center;
42
+ justify-items: center;
43
+ }
44
+ }
45
+
46
+ @container (min-width: 48rem) {
47
+ .section.section--split .section__inner {
48
+ grid-template-columns:
49
+ minmax(0, var(--section-split-head, 1fr))
50
+ minmax(0, var(--section-split-body, 2fr));
51
+ align-items: start;
52
+ }
53
+ }
@@ -425,3 +425,265 @@
425
425
  display: none;
426
426
  }
427
427
  }
428
+
429
+ /* ── Tiers (`<SiteNav>`'s optional `primary-mobile` / `primary-mega` menus) ──────
430
+ Each tier is its own `<nav>` in a `.sitenav-tier` wrapper, and a wrapper is
431
+ `display: none` outside its container-width window — so exactly one tier is ever
432
+ in the accessibility tree. Same container as the `--bp-*` flag above (the nearest
433
+ ancestor `container-type`, `body` per layout.css), so the hand-over width and the
434
+ drawer→navbar flip can never disagree. One pair per breakpoint; `sitenav.ts`'s
435
+ `VISIBILITY_CLASSES` lists them and `sitenav.test.ts` checks each is defined here. */
436
+ @container (width < 30rem) {
437
+ .sitenav-tier--from-sm {
438
+ display: none;
439
+ }
440
+ }
441
+ @container (width >= 30rem) {
442
+ .sitenav-tier--below-sm {
443
+ display: none;
444
+ }
445
+ }
446
+ @container (width < 48rem) {
447
+ .sitenav-tier--from-md {
448
+ display: none;
449
+ }
450
+ }
451
+ @container (width >= 48rem) {
452
+ .sitenav-tier--below-md {
453
+ display: none;
454
+ }
455
+ }
456
+ @container (width < 64rem) {
457
+ .sitenav-tier--from-lg {
458
+ display: none;
459
+ }
460
+ }
461
+ @container (width >= 64rem) {
462
+ .sitenav-tier--below-lg {
463
+ display: none;
464
+ }
465
+ }
466
+ @container (width < 80rem) {
467
+ .sitenav-tier--from-xl {
468
+ display: none;
469
+ }
470
+ }
471
+ @container (width >= 80rem) {
472
+ .sitenav-tier--below-xl {
473
+ display: none;
474
+ }
475
+ }
476
+
477
+ /* ── Item description (EmDash `titleAttr` convention — see SiteNav.astro) ───── */
478
+ .sitenav__link--described {
479
+ flex-direction: column;
480
+ align-items: flex-start;
481
+ gap: 0.125rem;
482
+ }
483
+
484
+ .sitenav__desc {
485
+ font-size: 0.875em;
486
+ opacity: 0.75;
487
+ font-weight: normal;
488
+ }
489
+
490
+ /* The current page. Text-weight + a non-colour cue, so it survives forced-colors. */
491
+ .sitenav__link[aria-current='page'] {
492
+ font-weight: 600;
493
+ text-decoration: underline;
494
+ text-underline-offset: 0.25em;
495
+ }
496
+
497
+ /* ── Mega variant — WIDE ONLY ───────────────────────────────────────────────
498
+ Top-level submenus become full-width panels of columns under the nav row. The
499
+ branch item stops being the positioning context so the panel anchors to
500
+ `.sitenav--mega` instead. Inside the wide branch, with the same reasoning as the
501
+ rest of the file: a rule here competes at its bare selector weight. */
502
+ @container style(--_sitenav-wide: 1) {
503
+ .sitenav--mega {
504
+ position: relative;
505
+ }
506
+ .sitenav--mega .sitenav__list > .sitenav__item--branch {
507
+ position: static;
508
+ }
509
+ .sitenav--mega .sitenav__list > .sitenav__item--branch > .sitenav__submenu {
510
+ inset-inline: 0;
511
+ inline-size: 100%;
512
+ flex-direction: row;
513
+ flex-wrap: wrap;
514
+ align-items: flex-start;
515
+ gap: 1.5rem;
516
+ padding: 1.5rem;
517
+ }
518
+ /* Third level is a column inside the panel, not a fly-out. */
519
+ .sitenav--mega .sitenav__submenu .sitenav__item--branch {
520
+ position: static;
521
+ display: block;
522
+ }
523
+ .sitenav--mega .sitenav__submenu .sitenav__item--branch > .sitenav__submenu {
524
+ position: static;
525
+ opacity: 1;
526
+ visibility: visible;
527
+ pointer-events: auto;
528
+ translate: none;
529
+ box-shadow: none;
530
+ border: none;
531
+ padding: 0.25rem 0 0;
532
+ min-inline-size: 0;
533
+ }
534
+ .sitenav--mega .sitenav__submenu .sitenav__disclosure {
535
+ display: none;
536
+ }
537
+ }
538
+
539
+ /* ── In-page menu (`vitops.nav` block) ─────────────────────────────────────────
540
+ *
541
+ * A menu read in the body of a page — a site map, a section's links, footer-style columns —
542
+ * as plain nested lists. No disclosure and no drawer: that is the header's job (`.sitenav`).
543
+ *
544
+ * <nav aria-label="Services"><ul class="navlist navlist--columns">
545
+ * <li class="navlist__item"><a class="navlist__link">…</a><ul class="navlist__sub">…</ul></li>
546
+ *
547
+ * Modifiers: `--inline` (one wrapping row) and `--columns` (an auto-fit grid, each top-level
548
+ * item a column with its sub-links beneath it). */
549
+ .navlist,
550
+ .navlist__sub {
551
+ list-style: none;
552
+ margin: 0;
553
+ padding: 0;
554
+ display: flex;
555
+ flex-direction: column;
556
+ gap: 0.25rem;
557
+ }
558
+
559
+ /* The rhythm utility's `li + li` margin is the flow's job; the list's own gap does it here. */
560
+ .navlist__item {
561
+ margin: 0;
562
+ }
563
+
564
+ .navlist__sub {
565
+ margin-block-start: 0.25rem;
566
+ padding-inline-start: 1rem;
567
+ }
568
+
569
+ .navlist__link {
570
+ color: inherit;
571
+ text-decoration: underline;
572
+ text-underline-offset: 0.2em;
573
+ padding-block: 0.25rem;
574
+ border-radius: var(--br-control, 0.25rem);
575
+
576
+ &:focus-visible {
577
+ outline: 2px solid var(--sitenav-focus-color, var(--color-border-focus));
578
+ outline-offset: 2px;
579
+ }
580
+
581
+ &[aria-current='page'] {
582
+ font-weight: 600;
583
+ }
584
+ }
585
+
586
+ .navlist__label {
587
+ font-weight: 600;
588
+ }
589
+
590
+ .navlist--inline {
591
+ flex-direction: row;
592
+ flex-wrap: wrap;
593
+ gap: 0.25rem 1.25rem;
594
+ }
595
+
596
+ .navlist--columns {
597
+ display: grid;
598
+ grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
599
+ gap: 1.5rem;
600
+
601
+ > .navlist__item > :is(.navlist__link, .navlist__label) {
602
+ font-weight: 600;
603
+ }
604
+
605
+ > .navlist__item > .navlist__sub {
606
+ padding-inline-start: 0;
607
+ }
608
+ }
609
+
610
+ @media (forced-colors: active) {
611
+ .navlist__link:focus-visible {
612
+ outline-color: Highlight;
613
+ }
614
+ }
615
+
616
+ /* ── Footer (`<SiteFooter>`) ───────────────────────────────────────────────── */
617
+ .sitefooter {
618
+ margin-block-start: var(--space-2xl, 3rem);
619
+ display: flex;
620
+ flex-direction: column;
621
+ gap: 1rem;
622
+ padding-block: var(--space-l, 2rem);
623
+ border-block-start: 1px solid var(--color-border-neutral, currentColor);
624
+ }
625
+
626
+ .sitefooter__nav {
627
+ display: flex;
628
+ flex-wrap: wrap;
629
+ gap: 1rem 3rem;
630
+ justify-content: space-between;
631
+ }
632
+
633
+ .sitefooter__list {
634
+ list-style: none;
635
+ margin: 0;
636
+ padding: 0;
637
+ display: flex;
638
+ flex-wrap: wrap;
639
+ gap: 0.25rem 1.25rem;
640
+ }
641
+
642
+ .sitefooter__link {
643
+ color: inherit;
644
+ text-decoration: underline;
645
+ text-underline-offset: 0.2em;
646
+ padding-block: 0.25rem;
647
+ border-radius: var(--br-control, 0.25rem);
648
+
649
+ &:focus-visible {
650
+ outline: 2px solid var(--sitenav-focus-color, var(--color-border-focus));
651
+ outline-offset: 2px;
652
+ }
653
+
654
+ &[aria-current='page'] {
655
+ font-weight: 600;
656
+ }
657
+ }
658
+
659
+ .sitefooter__copy {
660
+ margin: 0;
661
+ font-size: 0.875em;
662
+ opacity: 0.8;
663
+ }
664
+
665
+ /* ── Motion + forced colours (both site-nav and footer) ────────────────────── */
666
+ @media (prefers-reduced-motion: reduce) {
667
+ .sitenav__panel,
668
+ .sitenav__panel::backdrop,
669
+ .sitenav__submenu,
670
+ .sitenav__item--branch {
671
+ transition: none;
672
+ }
673
+ }
674
+
675
+ @media (forced-colors: active) {
676
+ .sitenav__link:focus-visible,
677
+ .sitenav__toggle:focus-visible,
678
+ .sitenav__close:focus-visible,
679
+ .sitefooter__link:focus-visible {
680
+ outline-color: Highlight;
681
+ }
682
+ .sitenav__panel,
683
+ .sitenav__submenu {
684
+ border: 1px solid CanvasText;
685
+ }
686
+ .sitenav__link[aria-current='page'] {
687
+ text-decoration-thickness: 2px;
688
+ }
689
+ }
@@ -210,3 +210,14 @@
210
210
  }
211
211
  }
212
212
  }
213
+
214
+ /*
215
+ * Pre-upgrade display for the two elements that live in this partial — see the note on
216
+ * `wc-entries` in table.css. Both are a framed block of content (two panes; two images),
217
+ * and `wc-image-compare`'s upgraded `:host` is already `display: block`, so declaring it
218
+ * here removes the layout shift when it registers. `wc-split-panel` has no `:host` display
219
+ * at all, so this also fixes it after upgrade.
220
+ */
221
+ :where(wc-split-panel, wc-image-compare) {
222
+ display: block;
223
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Stats — a row of headline figures ("11 hrs/week", "82%") and, as `.stats--tiles`, a
3
+ * grid of labelled tiles with a group, a delta and a state (the generic kernel of a
4
+ * dashboard mock: "Open jobs 24 +3 this week").
5
+ *
6
+ * The figure is the `.counter` pattern, so the count-up is `<wc-counter>`'s; this
7
+ * partial only lays the figures out. With no JS and no CSS each figure is plain text
8
+ * beside its label.
9
+ *
10
+ * `data-state` on a tile (`up` / `down`) drives the delta's colour through the semantic
11
+ * roles, so "good" and "bad" follow the site's own success/danger rather than a hard-coded
12
+ * green and red. The delta's words stay in the text: colour is never the only carrier.
13
+ */
14
+ .stats {
15
+ display: grid;
16
+ gap: var(--stats-gap, 1.5rem);
17
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--stats-min, 12rem)), 1fr));
18
+ margin: 0;
19
+ padding: 0;
20
+ list-style: none;
21
+
22
+ .stats__item {
23
+ display: grid;
24
+ gap: 0.25rem;
25
+ align-content: start;
26
+ margin: 0;
27
+ }
28
+
29
+ .stats__figure,
30
+ .stats__label,
31
+ .stats__group,
32
+ .stats__delta {
33
+ margin: 0;
34
+ }
35
+
36
+ .stats__figure {
37
+ color: var(--tone-fg, inherit);
38
+ font-variant-numeric: tabular-nums;
39
+ }
40
+
41
+ .stats__group {
42
+ text-transform: uppercase;
43
+ letter-spacing: 0.05em;
44
+ opacity: 0.8;
45
+ }
46
+
47
+ .stats__item[data-state='up'] .stats__delta {
48
+ color: var(--color-text-success, inherit);
49
+ }
50
+
51
+ .stats__item[data-state='down'] .stats__delta {
52
+ color: var(--color-text-danger, inherit);
53
+ }
54
+
55
+ &.stats--tiles .stats__item {
56
+ padding: var(--stats-tile-padding, 1rem 1.25rem);
57
+ border: var(--b-card, var(--b-card-group, var(--b-default)));
58
+ border-radius: var(--br-card, var(--br-card-group, var(--br-default)));
59
+ background-color: var(--tone-bg, transparent);
60
+ }
61
+ }