@uncinq/css-components 1.16.2 → 1.16.3

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 (50) hide show
  1. package/README.md +10 -10
  2. package/css/components/alert.css +5 -0
  3. package/css/components/panel.css +8 -6
  4. package/docs/_index.md +13 -11
  5. package/docs/buttons/_index.md +75 -0
  6. package/docs/buttons/btn-close.md +58 -0
  7. package/docs/buttons/btn-filter.md +36 -0
  8. package/docs/buttons/btn-menu.md +39 -0
  9. package/docs/buttons/btn-search.md +40 -0
  10. package/docs/buttons/btn-share.md +33 -0
  11. package/docs/buttons/btn-toc.md +23 -0
  12. package/docs/buttons/btn-toggle-video.md +58 -0
  13. package/docs/buttons/btn.md +95 -0
  14. package/docs/cascade-layers.md +8 -6
  15. package/docs/content/_index.md +39 -0
  16. package/docs/content/alert.md +97 -0
  17. package/docs/content/badge.md +50 -0
  18. package/docs/content/banner.md +43 -0
  19. package/docs/content/card.md +93 -0
  20. package/docs/content/items.md +76 -0
  21. package/docs/content/list.md +41 -0
  22. package/docs/content/media.md +88 -0
  23. package/docs/content/surtitle.md +43 -0
  24. package/docs/embeds/_index.md +25 -0
  25. package/docs/embeds/embed.md +53 -0
  26. package/docs/embeds/map.md +57 -0
  27. package/docs/embeds/video.md +53 -0
  28. package/docs/forms/_index.md +31 -0
  29. package/docs/forms/form-check.md +77 -0
  30. package/docs/forms/form.md +67 -0
  31. package/docs/navigation/_index.md +29 -0
  32. package/docs/navigation/breadcrumb.md +59 -0
  33. package/docs/navigation/nav-accessibility.md +67 -0
  34. package/docs/navigation/nav-title.md +43 -0
  35. package/docs/navigation/nav.md +65 -0
  36. package/docs/navigation/pagination.md +77 -0
  37. package/docs/overlays/_index.md +34 -0
  38. package/docs/overlays/drawer.md +58 -0
  39. package/docs/overlays/dropdown.md +87 -0
  40. package/docs/overlays/modal.md +75 -0
  41. package/docs/overlays/panel.md +159 -0
  42. package/docs/utilities/_index.md +21 -0
  43. package/docs/{utilities.md → utilities/scrollsnap.md} +26 -9
  44. package/package.json +1 -1
  45. package/docs/buttons.md +0 -103
  46. package/docs/content.md +0 -145
  47. package/docs/embeds.md +0 -66
  48. package/docs/forms.md +0 -80
  49. package/docs/navigation.md +0 -93
  50. package/docs/overlays.md +0 -133
@@ -0,0 +1,39 @@
1
+ ---
2
+ isIndex: false
3
+ title: Content
4
+ description: Alerts, badges, banners, cards, the items grid, labelled lists, media objects and surtitles.
5
+ weight: 4
6
+ icon: card-text
7
+ ---
8
+
9
+ Eight components for laying out page content.
10
+
11
+ | Page | Class | Purpose |
12
+ | --- | --- | --- |
13
+ | [Alert](alert/) | `.alert` | An inline notification block |
14
+ | [Badge](badge/) | `.badge` | A small inline label |
15
+ | [Banner](banner/) | `.banner` | A centred modifier on `.alert` |
16
+ | [Card](card/) | `.card` | A self-contained content card |
17
+ | [Items](items/) | `.items` | The responsive grid that holds cards |
18
+ | [List](list/) | `.list` | A compact labelled list |
19
+ | [Media](media/) | `.media` | The structural base for images and video |
20
+ | [Surtitle](surtitle/) | `.surtitle` | An eyebrow label above a heading |
21
+
22
+ ## Three of them are meant to nest
23
+
24
+ `.items` holds `.card`, a card holds a `.media`, and a `.surtitle` sits above the card's title. Each one reassigns the next one's tokens from its own namespace, so a media block or a surtitle adapts to its context without a variant class. The [card page](card/) covers what it hands down.
25
+
26
+ {{< alert-block state="info" >}}
27
+ Several source comments describe `.card` as a backwards-compatible alias for `.item` and recommend using `.item` directly. That is misleading for anyone consuming this package on its own: **`.item` is not defined here**. It lives in the Hugolify design system theme. Within `@uncinq/css-components`, `.card` is a complete implementation and the class to use.
28
+ {{< /alert-block >}}
29
+
30
+ ## Colour variants follow one of two patterns
31
+
32
+ `.alert` and `.badge` both ship the same ten intent names, and they resolve them differently.
33
+
34
+ | | Background | Text |
35
+ | --- | --- | --- |
36
+ | `.alert-*` | `--color-*-muted` | `--color-*-strong` |
37
+ | `.badge-*` | `--color-*` | `--color-text-on-*` |
38
+
39
+ An alert is a tinted block meant to hold a paragraph, so it uses the muted pair. A badge is a solid chip meant to be read at a glance, so it uses the full colour with its guaranteed-contrast text token. Neither ever hardcodes a colour, which is what makes a rebrand a token change rather than a search and replace.
@@ -0,0 +1,97 @@
1
+ ---
2
+ isIndex: false
3
+ title: Alert
4
+ description: An inline notification block, with ten colour variants and an automatic horizontal layout when it leads with an icon.
5
+ weight: 1
6
+ icon: exclamation-triangle
7
+ ---
8
+
9
+ An inline notification block.
10
+
11
+ ```html
12
+ <div class="alert alert-info">
13
+ <p class="alert-heading">Heads up</p>
14
+ <p>Your changes were saved.</p>
15
+ </div>
16
+ ```
17
+
18
+ ## Variants
19
+
20
+ `.alert-primary`, `.alert-secondary`, `.alert-brand`, `.alert-neutral`, `.alert-dark`, `.alert-light`, `.alert-success`, `.alert-danger`, `.alert-warning`, `.alert-info`.
21
+
22
+ Each sets background, border and text colour from the matching `-muted` and `-strong` semantic tokens, so the pairing stays legible if the underlying colour changes.
23
+
24
+ ```css
25
+ .alert-success {
26
+ --alert-color-background: var(--color-success-muted);
27
+ --alert-color-border: var(--color-success-muted);
28
+ --alert-color-text: var(--color-success-strong);
29
+ }
30
+ ```
31
+
32
+ `.alert-primary` and `.alert-brand` resolve identically, as do `.alert-secondary` and `.alert-neutral`. `.alert-dark` is the one exception to the muted pattern: it takes `--color-text-on-dark` rather than `--color-dark-strong`.
33
+
34
+ The base `.alert` declares no colours of its own, so an alert with no variant class shows whatever `--alert-color-*` resolves to in your token set.
35
+
36
+ ## Links inside an alert
37
+
38
+ `--color-link` is reassigned to the alert's own text colour:
39
+
40
+ ```css
41
+ .alert { --color-link: var(--alert-color-text); }
42
+ ```
43
+
44
+ Without that, a link inside a tinted block would fall back to the page link colour, which is chosen for contrast against the page background rather than against a muted tint.
45
+
46
+ ## Leading with an icon
47
+
48
+ An alert whose first child is an `.icon`, and which has no `.alert-heading`, switches to a horizontal layout on its own:
49
+
50
+ ```css
51
+ &:has(.icon:first-child):not(:has(.alert-heading)) {
52
+ flex-direction: row;
53
+ align-items: flex-start;
54
+ }
55
+ ```
56
+
57
+ ```html
58
+ <div class="alert alert-warning">
59
+ <span class="icon icon-triangle-alert"></span>
60
+ <p>This action cannot be undone.</p>
61
+ </div>
62
+ ```
63
+
64
+ With a heading present the alert stays stacked, because the icon then belongs beside the heading rather than beside the whole block. `.alert-heading` is itself a flex row with `--spacing-xs` between its children, so an icon inside the heading lines up with the text.
65
+
66
+ ## `.container` inside an alert
67
+
68
+ `.container` inherits the same flex layout as the alert itself:
69
+
70
+ ```css
71
+ &,
72
+ .container {
73
+ display: flex;
74
+ flex-direction: var(--alert-direction, column);
75
+ gap: var(--alert-gap);
76
+ }
77
+ ```
78
+
79
+ That is what lets a full-bleed alert keep its content aligned with the rest of the page: the alert spans the viewport, the container inside it caps the measure, and both lay their children out the same way.
80
+
81
+ ## Hiding an alert
82
+
83
+ `.alert` declares `display: flex`, which would otherwise beat the user agent rule for `[hidden]`. The file restores it:
84
+
85
+ ```css
86
+ .alert[hidden] { display: none; }
87
+ ```
88
+
89
+ So a dismissible alert can be hidden with the attribute alone, with no state class to manage:
90
+
91
+ ```js
92
+ alert.hidden = true;
93
+ ```
94
+
95
+ ## Margins
96
+
97
+ `.alert *` sets `margin-block: 0` on every descendant, so paragraphs inside an alert are spaced by the flex `gap` rather than by their own margins. Vertical rhythm around the alert itself comes from `--alert-margin`.
@@ -0,0 +1,50 @@
1
+ ---
2
+ isIndex: false
3
+ title: Badge
4
+ description: A small inline label, with ten solid colour variants and hover styles reserved for links.
5
+ weight: 2
6
+ icon: tag
7
+ ---
8
+
9
+ A small inline label.
10
+
11
+ ```html
12
+ <span class="badge badge-success">Published</span>
13
+ <a class="badge" href="/tag/css">css</a>
14
+ ```
15
+
16
+ It is `inline-flex`, so it sits in a line of text and can hold an icon beside its label.
17
+
18
+ ## Variants
19
+
20
+ The names mirror the [alert](../alert/) set: `.badge-primary`, `.badge-secondary`, `.badge-brand`, `.badge-neutral`, `.badge-dark`, `.badge-light`, `.badge-success`, `.badge-danger`, `.badge-warning`, `.badge-info`.
21
+
22
+ The colours do not. A badge is a **solid** chip, not a tinted block:
23
+
24
+ ```css
25
+ .badge-success {
26
+ --badge-color-background: var(--color-success);
27
+ --badge-color-background-hover: var(--color-success-strong);
28
+ --badge-color-text: var(--color-text-on-success);
29
+ }
30
+ ```
31
+
32
+ Full colour for the background, and the matching `--color-text-on-*` token for the text, which is the pair that guarantees contrast when the underlying colour is overridden. Compare with `.alert-*`, which uses `-muted` and `-strong` because it has to remain readable behind a paragraph.
33
+
34
+ Note that a variant sets no border colour. `--badge-color-border` keeps whatever the base token resolves to, so a bordered badge needs that token set explicitly.
35
+
36
+ ## Hover is reserved for links
37
+
38
+ ```css
39
+ @media (hover: hover) and (pointer: fine) {
40
+ a.badge:hover { ... }
41
+ }
42
+ ```
43
+
44
+ Two guards, doing two different jobs. The element selector means a static `<span class="badge">` never looks interactive, which matters because badges are used for both states and actions. The media query means a tap on a touch device does not leave the badge stuck in its hover colour.
45
+
46
+ Both hover values fall back to the resting one, so a variant that defines no hover colour simply does not change on hover rather than resolving to nothing.
47
+
48
+ ## Text decoration
49
+
50
+ The base rule sets `text-decoration: none`, so `a.badge` never carries the page's link underline. That is the one thing the class overrides on a link; everything else it leaves to the variant.
@@ -0,0 +1,43 @@
1
+ ---
2
+ isIndex: false
3
+ title: Banner
4
+ description: A centred modifier on .alert, for site-wide announcements.
5
+ weight: 3
6
+ icon: megaphone
7
+ ---
8
+
9
+ A thin variation on the [alert](../alert/), centred rather than start-aligned.
10
+
11
+ ```html
12
+ <div class="alert banner">
13
+ <p>Site-wide announcement.</p>
14
+ </div>
15
+ ```
16
+
17
+ **It is a modifier, so use both classes.** `.banner` on its own has no background, no border and no padding.
18
+
19
+ ## The whole component
20
+
21
+ ```css
22
+ .banner {
23
+ --alert-align: center;
24
+
25
+ p { margin-block: 0; }
26
+ }
27
+ ```
28
+
29
+ That is the entire file. `--alert-align` feeds both `align-items` and `text-align` in the alert rule, so one token centres the block on both axes at once. The paragraph reset flattens the margins that would otherwise show up when the banner holds a single line.
30
+
31
+ ## Variants come from the alert
32
+
33
+ Every `.alert-*` colour works here, because the variant only sets `--alert-color-*` tokens and the banner only sets alignment:
34
+
35
+ ```html
36
+ <div class="alert alert-warning banner">
37
+ <p>Scheduled maintenance on Sunday.</p>
38
+ </div>
39
+ ```
40
+
41
+ ## Full-bleed announcements
42
+
43
+ A banner is usually the one alert that spans the viewport. Put a `.container` inside it rather than constraining the banner itself: the alert passes its flex layout down to `.container`, so the announcement stays aligned with the rest of the page while the tint runs edge to edge. See [the alert page](../alert/#container-inside-an-alert).
@@ -0,0 +1,93 @@
1
+ ---
2
+ isIndex: false
3
+ title: Card
4
+ description: A self-contained content card, the tokens it hands down to its media, and the whole-card link pattern.
5
+ weight: 4
6
+ icon: card-heading
7
+ ---
8
+
9
+ A self-contained content card.
10
+
11
+ ```html
12
+ <article class="card">
13
+ <div class="content">
14
+ <p class="surtitle">Category</p>
15
+ <p class="title">Card title</p>
16
+ <p class="description">Short description.</p>
17
+ </div>
18
+ <div class="media">
19
+ <picture>...</picture>
20
+ </div>
21
+ </article>
22
+ ```
23
+
24
+ | Child | Role |
25
+ | --- | --- |
26
+ | `.content` | The text block, grows to fill the card |
27
+ | `.title` | The heading line, styled from `--card-title-*` |
28
+ | `.description` | The body text, at `--card-font-size` |
29
+ | `.media` | The image or video, see [Media](../media/) |
30
+ | `.surtitle` | An optional eyebrow, see [Surtitle](../surtitle/) |
31
+
32
+ {{< alert-block state="info" >}}
33
+ The source comment in `card.css` describes `.card` as a backwards-compatible alias for `.item` and recommends using `.item` directly. That is misleading for anyone consuming this package on its own: **`.item` is not defined here**. It lives in the Hugolify design system theme. Within `@uncinq/css-components`, `.card` is a complete implementation and the class to use.
34
+ {{< /alert-block >}}
35
+
36
+ ## Put the media last in the markup
37
+
38
+ The media block sits first **visually**, through `--card-media-order: -1`, regardless of where it appears in the source. Put it after the content so that screen readers and search engines reach the text first.
39
+
40
+ Set `--card-media-order` to anything else, or to `initial`, to put the image below the text on a given card.
41
+
42
+ ## What the card hands down
43
+
44
+ Three reassignments at the top of the rule:
45
+
46
+ ```css
47
+ --media-color-background: var(--card-media-color-background);
48
+ --media-ratio: var(--card-media-ratio);
49
+ --surtitle-margin-block-end: calc(var(--card-gap) * -1);
50
+ ```
51
+
52
+ A [`.media`](../media/) inside a card therefore takes the card's ratio and background without any variant class, and a [`.surtitle`](../surtitle/) is pulled back by exactly one gap so that it reads as part of the title rather than as a separate line.
53
+
54
+ `.media-icon` gets a longer handover of its own, mapping eight `--card-media-icon-*` tokens onto the media namespace, which is what lets an icon card look nothing like a photo card while using the same two classes.
55
+
56
+ ## The whole-card link
57
+
58
+ Wrapping the content in `a.content` makes the entire card clickable:
59
+
60
+ ```html
61
+ <article class="card">
62
+ <a class="content" href="/article">
63
+ <p class="title">Card title</p>
64
+ <p class="description">Short description.</p>
65
+ </a>
66
+ <div class="media">...</div>
67
+ </article>
68
+ ```
69
+
70
+ The link lays a `::before` overlay across the card, and its own focus ring is suppressed so the focus outline can be drawn on the card instead:
71
+
72
+ ```css
73
+ a.content::before { content: ''; inset: 0; position: absolute; }
74
+ a.content:focus-visible { outline: none; }
75
+
76
+ .card:has(a.content:focus-visible) {
77
+ outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-color-outline);
78
+ }
79
+ ```
80
+
81
+ Without the `:has()` rule the ring would outline an invisible box, which is the usual failure of this pattern.
82
+
83
+ Two things to know before using it. **Nested interactive elements become unreachable**, since the overlay covers them: a card with both a whole-card link and a button inside needs a different structure. And the overlay resolves against the nearest positioned ancestor, so the card must establish one; give it `position: relative` if your layout does not already.
84
+
85
+ ## Sizing
86
+
87
+ `--card-max-width` and `--card-width` are unset by default, so a card fills its grid track. Prefer `ch` units when you do set them, so the width tracks the text measure rather than a fixed pixel count.
88
+
89
+ `overflow: hidden` on the card is what lets `--card-border-radius` clip a full-bleed media block at the corners.
90
+
91
+ ## States
92
+
93
+ Hover swaps the shadow, `--card-shadow` taking `--card-shadow-hover`, behind `@media (hover: hover) and (pointer: fine)`. The transition is behind `prefers-reduced-motion`. There is no hover colour change, deliberately: a card that changes background on hover competes with the link inside it.
@@ -0,0 +1,76 @@
1
+ ---
2
+ isIndex: false
3
+ title: Items
4
+ description: A responsive grid whose column count is intrinsic rather than breakpoint-driven.
5
+ weight: 5
6
+ icon: grid-3x2-gap
7
+ ---
8
+
9
+ A responsive grid, and the usual container for [cards](../card/).
10
+
11
+ ```html
12
+ <div class="items">
13
+ <article class="card">...</article>
14
+ <article class="card">...</article>
15
+ <article class="card">...</article>
16
+ </div>
17
+ ```
18
+
19
+ The children are yours to provide. Within this package that means `.card`; `.item` is defined by the Hugolify design system theme, not here.
20
+
21
+ ## The column count is intrinsic
22
+
23
+ ```css
24
+ grid-template-columns: repeat(
25
+ var(--items-cols, auto-fill),
26
+ minmax(min(var(--items-min-width, var(--max-width-item)), 100%), 1fr)
27
+ );
28
+ ```
29
+
30
+ Items wrap once they no longer fit at `--items-min-width`, **with no media query involved**. The grid answers to the width of its container rather than to the width of the viewport, so the same markup works in a page, in a sidebar and inside another grid.
31
+
32
+ The inner `min(..., 100%)` is what keeps a single item from overflowing a container narrower than `--items-min-width`. Without it, a 30ch minimum in a 20ch column would blow out the layout.
33
+
34
+ Two ways to change the result:
35
+
36
+ | Token | Effect |
37
+ | --- | --- |
38
+ | `--items-min-width` | The width below which items wrap. Defaults to `--max-width-item` |
39
+ | `--items-cols` | A fixed column count, replacing `auto-fill` |
40
+
41
+ ```css
42
+ .related { --items-cols: 2; }
43
+ ```
44
+
45
+ ## Gaps
46
+
47
+ `--items-gap` sets both axes. `--items-column-gap` and `--items-row-gap` override one at a time, each falling back to `--items-gap`.
48
+
49
+ ## It is a container query context
50
+
51
+ ```css
52
+ container-type: inline-size;
53
+ container-name: items;
54
+ ```
55
+
56
+ So children can query the grid rather than the viewport:
57
+
58
+ ```css
59
+ @container items (min-width: 60ch) {
60
+ .card { --card-media-order: initial; }
61
+ }
62
+ ```
63
+
64
+ That is the companion to the intrinsic columns: the grid decides how many tracks fit, and the children adapt to the space they actually got.
65
+
66
+ Be aware that `container-type: inline-size` makes the grid its own containment context, which affects how absolutely positioned descendants resolve.
67
+
68
+ ## Turning it into a carousel
69
+
70
+ Add a [`.scrollsnap-*`](../../utilities/scrollsnap/) class to the same element. The utility replaces the grid template with a single scrolling row below its breakpoint and hands everything back above it.
71
+
72
+ ```html
73
+ <div class="items scrollsnap-md">...</div>
74
+ ```
75
+
76
+ The carousel reads the same `--items-min-width`, so item width stays consistent between the two modes.
@@ -0,0 +1,41 @@
1
+ ---
2
+ isIndex: false
3
+ title: List
4
+ description: A compact labelled list, with the label as a sibling of the list rather than inside it.
5
+ weight: 6
6
+ icon: list-ul
7
+ ---
8
+
9
+ A compact labelled list.
10
+
11
+ ```html
12
+ <div class="list">
13
+ <p>Optional label</p>
14
+ <ul>
15
+ <li>First</li>
16
+ <li>Second</li>
17
+ </ul>
18
+ </div>
19
+ ```
20
+
21
+ **The wrapper is a `div`, and the label is a sibling of the list, not inside it.** That is the one structural thing to get right. Either `ul` or `ol` works.
22
+
23
+ ## What it changes
24
+
25
+ Three small things, and nothing else:
26
+
27
+ | Selector | Token | Effect |
28
+ | --- | --- | --- |
29
+ | `p` | `--list-label-font-weight`, `--list-label-margin-block` | The label's weight and the space under it |
30
+ | `li` | `--list-item-font-size`, `--list-item-gap` | Item size, and the space between items as a block margin |
31
+ | `.list, ul, ol` | — | `margin-block-end: 0`, so the block ends flush |
32
+
33
+ Item spacing is a **margin**, not a `gap`, because the list is not a flex container. A `gap` here would do nothing.
34
+
35
+ ## Markers come from css-base
36
+
37
+ The dash marker is not declared in this file. Bullets, numbering and the marker colour all come from the native list styling in [css-base](../../../css-base/base/), which is also why the component works on `ol` without a variant.
38
+
39
+ ## Where it fits
40
+
41
+ It is the plain-text counterpart to [`.nav`](../../navigation/nav/), which resets the markers and lays its items out with flexbox because they are links. Use `.list` for content, `.nav` for navigation.
@@ -0,0 +1,88 @@
1
+ ---
2
+ isIndex: false
3
+ title: Media
4
+ description: The structural base for images and video, holding the aspect ratio, plus the logo and icon variants.
5
+ weight: 7
6
+ icon: image
7
+ ---
8
+
9
+ The structural base for media blocks. It holds an aspect ratio and crops whatever is inside it.
10
+
11
+ ```html
12
+ <div class="media">
13
+ <picture>
14
+ <img src="..." alt="...">
15
+ </picture>
16
+ </div>
17
+ ```
18
+
19
+ | Class | Use |
20
+ | --- | --- |
21
+ | `.media` | Base, for a picture or a video |
22
+ | `.media-logo` | Logo, contained rather than cropped |
23
+ | `.media-icon` | Icon, no ratio and no background |
24
+
25
+ `.media-logo` and `.media-icon` are **modifiers**: use them alongside `.media`.
26
+
27
+ ## What the base does
28
+
29
+ ```css
30
+ .media {
31
+ aspect-ratio: var(--media-ratio);
32
+ background-color: var(--media-color-background);
33
+ overflow: hidden;
34
+ }
35
+ ```
36
+
37
+ and then stretches `img`, `picture` and `video` to fill it with `object-fit: cover`. The background shows through while an image loads, which is what keeps a grid of cards from flashing white.
38
+
39
+ Note that the base declares **no border and no radius**. A bordered media block is a card's doing, not the component's: see below.
40
+
41
+ ## `.media-logo`
42
+
43
+ Swaps the crop for a contain:
44
+
45
+ ```css
46
+ .media-logo {
47
+ picture { display: flex; align-items: center; justify-content: center; }
48
+ img { object-fit: contain; max-height: var(--media-logo-max-height); max-width: var(--media-logo-max-width); }
49
+ }
50
+ ```
51
+
52
+ A logo must never be cropped, and it must not fill the box either, hence the two maxima and the centring. Everything else, ratio and background, still comes from `.media`.
53
+
54
+ ## `.media-icon`
55
+
56
+ Removes the two things that make a media block a media block:
57
+
58
+ ```css
59
+ .media-icon {
60
+ --media-ratio: none;
61
+ --media-color-background: transparent;
62
+ }
63
+ ```
64
+
65
+ So it takes the size of its content. Inside a [card](../card/) it gets far more than that: the card maps eight `--card-media-icon-*` tokens onto the media namespace, including `--icon-size` and `--icon-color`, which is what gives icon cards their own look without a third class.
66
+
67
+ ## Context over variants
68
+
69
+ A media block inside a card reads the card's tokens rather than its own:
70
+
71
+ ```css
72
+ .card {
73
+ --media-color-background: var(--card-media-color-background);
74
+ --media-ratio: var(--card-media-ratio);
75
+ }
76
+ ```
77
+
78
+ and the card adds the border, radius and margins on `.media` directly. So the same `<div class="media">` is a full-bleed card header in one place and a standalone framed image in another, with no class changing.
79
+
80
+ That is the pattern to copy when you need a media block to look different in a new context: set the `--media-*` tokens on the context, not a variant on the block.
81
+
82
+ ## Video inside a media block
83
+
84
+ ```css
85
+ .media .video { height: 100%; }
86
+ ```
87
+
88
+ A [`.video`](../../embeds/video/) wrapper is stretched to fill the media box, so a video and an image are interchangeable inside a card.
@@ -0,0 +1,43 @@
1
+ ---
2
+ isIndex: false
3
+ title: Surtitle
4
+ description: An eyebrow label above a heading, tightened automatically when it sits inside a card.
5
+ weight: 8
6
+ icon: type-h2
7
+ ---
8
+
9
+ An eyebrow label above a heading.
10
+
11
+ ```html
12
+ <p class="surtitle">Category</p>
13
+ <h2>Main title</h2>
14
+ ```
15
+
16
+ It is a leaf component: no children, no variants, no states. Every property reads a `--surtitle-*` token, including the border set, so it can be a plain label in one theme and a chip in another without a second class.
17
+
18
+ | Token group | Covers |
19
+ | --- | --- |
20
+ | `--surtitle-color-*` | Background and text |
21
+ | `--surtitle-border-*` | Colour, style, width and radius |
22
+ | `--surtitle-font-size`, `-font-weight`, `-letter-spacing`, `-line-height`, `-text-transform` | Typography |
23
+ | `--surtitle-padding-*`, `-margin-block-*` | Spacing |
24
+
25
+ ## It pulls itself closer inside a card
26
+
27
+ A [card](../card/) reassigns one token:
28
+
29
+ ```css
30
+ .card { --surtitle-margin-block-end: calc(var(--card-gap) * -1); }
31
+ ```
32
+
33
+ The negative margin cancels exactly one grid gap, so the surtitle and the title read as a single unit rather than as two stacked lines. The card also zeroes `margin-block-start` on it, so the pair sits flush against the top of the content block.
34
+
35
+ That is the whole mechanism, and it is worth copying: a context that stacks its children with a `gap` can tighten any pair by handing back one negative gap, with no change to the component.
36
+
37
+ ## Use a paragraph, not a heading
38
+
39
+ A surtitle is a label, not a level in the document outline. Marking it up as `<h3>` above an `<h2>` inverts the heading order and breaks navigation by headings. If the category needs to be part of the heading, put it inside one:
40
+
41
+ ```html
42
+ <h2><span class="surtitle">Category</span> Main title</h2>
43
+ ```
@@ -0,0 +1,25 @@
1
+ ---
2
+ isIndex: false
3
+ title: Embeds
4
+ description: Aspect-ratio wrappers for third-party embeds, native video and maps.
5
+ weight: 6
6
+ icon: play-btn
7
+ ---
8
+
9
+ Three containers, all built on the same idea: hold the space before the content arrives.
10
+
11
+ | Page | Class | For |
12
+ | --- | --- | --- |
13
+ | [Embed](embed/) | `.embed` | Third-party iframes, players, social cards |
14
+ | [Video](video/) | `.video` | A native `<video>`, with an optional pause control |
15
+ | [Map](map/) | `.map` | A Leaflet map container |
16
+
17
+ ## They all reserve an aspect ratio
18
+
19
+ An iframe, a video or a map that sizes itself once its content loads pushes the rest of the page down at an unpredictable moment. Each of these components declares `aspect-ratio` from a token, so the box exists at its final size from the first paint.
20
+
21
+ That is also why all three set `overflow: hidden`: the content is stretched to fill a box whose proportions it did not choose.
22
+
23
+ ## They stop at the container
24
+
25
+ None of the three ships the thing it wraps. `.embed` gives you no player, `.video` gives you no controls beyond the [toggle button](../buttons/btn-toggle-video/), and `.map` gives you neither Leaflet nor its stylesheet. Loading those, and keeping them out of your own layers, is the consuming project's job. See [Cascade layers](../cascade-layers/) for where third-party CSS belongs.
@@ -0,0 +1,53 @@
1
+ ---
2
+ isIndex: false
3
+ title: Embed
4
+ description: A responsive container for third-party embeds, holding an aspect ratio so the page does not reflow.
5
+ weight: 1
6
+ icon: aspect-ratio
7
+ ---
8
+
9
+ A responsive container for third-party embeds, holding an aspect ratio so the page does not reflow once the iframe loads.
10
+
11
+ ```html
12
+ <div class="embed">
13
+ <iframe src="https://..." title="Video title" allowfullscreen></iframe>
14
+ </div>
15
+ ```
16
+
17
+ Accepts `iframe`, `video`, `embed` or `object` as its child, each stretched to fill the box.
18
+
19
+ ## The ratio
20
+
21
+ `--embed-ratio` defaults to the video ratio token. Override it per instance for a square or portrait embed:
22
+
23
+ ```html
24
+ <div class="embed" style="--embed-ratio: 1">...</div>
25
+ ```
26
+
27
+ or per context, which is usually better:
28
+
29
+ ```css
30
+ .podcast-player { --embed-ratio: 4 / 1; }
31
+ ```
32
+
33
+ ## Frame and shadow
34
+
35
+ `--embed-border`, `--embed-border-radius` and `--embed-box-shadow` are the three decoration tokens. `overflow: hidden` is what lets the radius clip the iframe at the corners, since an iframe cannot be given one of its own.
36
+
37
+ Note that `--embed-border` is a **shorthand** token taking width, style and colour together, unlike most components in this package which split the three. Set it as one value:
38
+
39
+ ```css
40
+ .embed { --embed-border: 1px solid var(--color-border); }
41
+ ```
42
+
43
+ ## Always give the iframe a title
44
+
45
+ ```html
46
+ <iframe src="..." title="2024 annual report, presented by the CFO"></iframe>
47
+ ```
48
+
49
+ It is the only accessible name an embedded frame has. Without it a screen reader announces "frame" and the reader has to enter it to find out what it holds.
50
+
51
+ ## Embed or video?
52
+
53
+ Use `.embed` for a **third-party player**, where the content is an iframe you do not control. Use [`.video`](../video/) for a native `<video>` element, which needs a positioning context for its pause control rather than a ratio wrapper.