ferst-core 0.7.7 → 0.7.9
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.
- package/bin/ferst-modules.mjs +18 -2
- package/components/BlockRenderer.astro +125 -119
- package/components/Button.astro +1 -1
- package/components/CalendarEmbed.astro +2 -1
- package/components/Card.astro +2 -0
- package/components/SiteHeader.astro +24 -0
- package/components/recipes/Accordion.astro +91 -87
- package/components/recipes/Bento.astro +7 -3
- package/components/recipes/ContactForm.astro +6 -3
- package/components/recipes/Cta.astro +5 -1
- package/components/recipes/Faq.astro +5 -1
- package/components/recipes/FeatureCards.astro +115 -112
- package/components/recipes/Hero.astro +521 -495
- package/components/recipes/LatestPosts.astro +1 -1
- package/components/recipes/MediaCards.astro +214 -213
- package/components/recipes/Notice.astro +90 -90
- package/components/recipes/Pricing.astro +7 -4
- package/components/recipes/Stats.astro +155 -152
- package/components/recipes/Steps.astro +124 -121
- package/components/recipes/Tabs.astro +138 -134
- package/components/recipes/Tiles.astro +291 -287
- package/content/blocks.ts +88 -52
- package/layouts/Base.astro +496 -484
- package/lib/inline-markdown.ts +13 -0
- package/lib/slug.ts +1 -1
- package/package.json +1 -1
- package/routes/calendar/index.astro +2 -1
|
@@ -1,152 +1,155 @@
|
|
|
1
|
-
---
|
|
2
|
-
/** Stats — a Level-2 recipe: a band of headline numbers over an optional
|
|
3
|
-
* heading/intro. Composes the Heading primitive; numbers use the brand accent.
|
|
4
|
-
*
|
|
5
|
-
* Self-composing: the grid never reserves more columns than there are stats, so
|
|
6
|
-
* a single item centres as a feature instead of stranding an empty half-band
|
|
7
|
-
* (the "£10 pinned to one side" bug); each stat is a token-driven tile (radius/
|
|
8
|
-
* surface/stroke/shadow knobs, like every Card), so it reads as a designed unit
|
|
9
|
-
* at any count and in either theme. Labels are width-capped so a long caption
|
|
10
|
-
* wraps to a tidy block rather than a full-width line. */
|
|
11
|
-
import Heading from '../blocks/Heading.astro';
|
|
12
|
-
|
|
13
|
-
interface Item { value: string; label: string; description?: string }
|
|
14
|
-
interface Props {
|
|
15
|
-
title?: string;
|
|
16
|
-
intro?: string;
|
|
17
|
-
background?: 'none' | 'muted' | 'accent';
|
|
18
|
-
columns?: 2 | 3 | 4;
|
|
19
|
-
items?: Item[];
|
|
20
|
-
}
|
|
21
|
-
const { title, intro, background = 'muted', columns = 3, items = [] } = Astro.props;
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
</
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.b-stats__grid[data-cols='
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.b-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
1
|
+
---
|
|
2
|
+
/** Stats — a Level-2 recipe: a band of headline numbers over an optional
|
|
3
|
+
* heading/intro. Composes the Heading primitive; numbers use the brand accent.
|
|
4
|
+
*
|
|
5
|
+
* Self-composing: the grid never reserves more columns than there are stats, so
|
|
6
|
+
* a single item centres as a feature instead of stranding an empty half-band
|
|
7
|
+
* (the "£10 pinned to one side" bug); each stat is a token-driven tile (radius/
|
|
8
|
+
* surface/stroke/shadow knobs, like every Card), so it reads as a designed unit
|
|
9
|
+
* at any count and in either theme. Labels are width-capped so a long caption
|
|
10
|
+
* wraps to a tidy block rather than a full-width line. */
|
|
11
|
+
import Heading from '../blocks/Heading.astro';
|
|
12
|
+
|
|
13
|
+
interface Item { value: string; label: string; description?: string }
|
|
14
|
+
interface Props {
|
|
15
|
+
title?: string;
|
|
16
|
+
intro?: string;
|
|
17
|
+
background?: 'none' | 'muted' | 'accent';
|
|
18
|
+
columns?: 2 | 3 | 4;
|
|
19
|
+
items?: Item[];
|
|
20
|
+
}
|
|
21
|
+
const { title, intro, background = 'muted', columns = 3, items = [] } = Astro.props;
|
|
22
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
23
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
24
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
25
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
26
|
+
// Cap the column count at the item count — one stat should never sit in a
|
|
27
|
+
// two-column grid (half empty). Falls back to 1 for an empty list.
|
|
28
|
+
const cols = Math.min(columns, Math.max(items.length, 1)) as 1 | 2 | 3 | 4;
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<section class="b-stats" data-bg={background}>
|
|
32
|
+
<div class="b-stats__inner">
|
|
33
|
+
{(title || intro) && (
|
|
34
|
+
<div class="b-stats__head" data-recipe-head data-content-align={headAlign}>
|
|
35
|
+
{title && <Heading text={title} level={2} />}
|
|
36
|
+
{intro && <p class="b-stats__intro">{intro}</p>}
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
<div class="b-stats__grid" data-cols={cols}>
|
|
40
|
+
{items.map((item) => (
|
|
41
|
+
<div class="b-stats__item ferst-tile">
|
|
42
|
+
<span class="b-stats__value">{item.value}</span>
|
|
43
|
+
{item.label && <span class="b-stats__label">{item.label}</span>}
|
|
44
|
+
{item.description && <span class="b-stats__desc">{item.description}</span>}
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
.b-stats { padding-block: clamp(2.5rem, 6vw, 4rem); }
|
|
53
|
+
.b-stats[data-bg='muted'] { background: var(--bg-section); }
|
|
54
|
+
.b-stats[data-bg='accent'] { background: var(--gold-light); }
|
|
55
|
+
.b-stats__inner {
|
|
56
|
+
max-width: 72rem;
|
|
57
|
+
margin-inline: auto;
|
|
58
|
+
padding-inline: 1rem;
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: 2rem;
|
|
62
|
+
}
|
|
63
|
+
.b-stats__head {
|
|
64
|
+
max-width: var(--max);
|
|
65
|
+
margin-inline: auto;
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 0.75rem;
|
|
69
|
+
}
|
|
70
|
+
.b-stats__intro { margin: 0; color: var(--muted); line-height: 1.6; }
|
|
71
|
+
.b-stats__grid {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: 1fr;
|
|
74
|
+
gap: 1.25rem;
|
|
75
|
+
width: 100%;
|
|
76
|
+
margin-inline: auto;
|
|
77
|
+
}
|
|
78
|
+
@media (min-width: 40em) {
|
|
79
|
+
.b-stats__grid[data-cols='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
80
|
+
.b-stats__grid[data-cols='3'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
81
|
+
.b-stats__grid[data-cols='4'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
82
|
+
}
|
|
83
|
+
@media (min-width: 60em) {
|
|
84
|
+
.b-stats__grid[data-cols='3'] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
85
|
+
.b-stats__grid[data-cols='4'] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
86
|
+
}
|
|
87
|
+
/* A lone stat is a centred feature, not a full-width slab. */
|
|
88
|
+
.b-stats__grid[data-cols='1'] { max-width: 30rem; }
|
|
89
|
+
/* Token-driven tile: the same radius / surface / stroke / shadow knobs as every
|
|
90
|
+
Card, so it stays consistent and theme-correct. On the muted band the tile's
|
|
91
|
+
--bg-surface sits a step above --bg-section, giving depth in both themes. */
|
|
92
|
+
.b-stats__item {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
align-items: center;
|
|
96
|
+
text-align: center;
|
|
97
|
+
gap: 0.5rem;
|
|
98
|
+
padding: clamp(1.5rem, 3vw, 2.25rem) clamp(1.25rem, 3vw, 1.75rem);
|
|
99
|
+
/* surface (bg/border/radius/shadow) comes from .ferst-tile */
|
|
100
|
+
}
|
|
101
|
+
.b-stats__value {
|
|
102
|
+
font-family: var(--font-heading);
|
|
103
|
+
font-weight: 700;
|
|
104
|
+
font-size: clamp(2.25rem, 1.6rem + 2.6vw, 3.25rem);
|
|
105
|
+
line-height: 1;
|
|
106
|
+
color: var(--gold);
|
|
107
|
+
}
|
|
108
|
+
.b-stats__label { font-weight: 600; color: var(--fg); line-height: 1.4; max-width: 30ch; }
|
|
109
|
+
.b-stats__desc { font-size: 0.875rem; color: var(--muted); line-height: 1.5; max-width: 32ch; }
|
|
110
|
+
/* One key figure: centred, not stranded at the start of a wide row. */
|
|
111
|
+
.b-stats__grid[data-cols='1'] {
|
|
112
|
+
max-width: 28rem;
|
|
113
|
+
margin-inline: auto;
|
|
114
|
+
text-align: center;
|
|
115
|
+
justify-items: center;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
118
|
+
|
|
119
|
+
<script>
|
|
120
|
+
// Count-up: numbers tick from 0 to their value when they scroll into view.
|
|
121
|
+
// Progressive — the final value is server-rendered, so with no JS (or under
|
|
122
|
+
// reduced-motion) it just shows the number. Non-numeric values (One, Any…) are
|
|
123
|
+
// left untouched; a leading sign / trailing suffix (+, %, " day") is preserved.
|
|
124
|
+
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
125
|
+
const values = document.querySelectorAll<HTMLElement>('.b-stats__value');
|
|
126
|
+
if (!reduce && 'IntersectionObserver' in window && values.length) {
|
|
127
|
+
const parse = (t: string) => {
|
|
128
|
+
const m = t.match(/^(\D*)(\d[\d,]*(?:\.\d+)?)(.*)$/);
|
|
129
|
+
if (!m) return null;
|
|
130
|
+
const decimals = (m[2].split('.')[1] || '').length;
|
|
131
|
+
return { pre: m[1], num: parseFloat(m[2].replace(/,/g, '')), suf: m[3], decimals };
|
|
132
|
+
};
|
|
133
|
+
const fmt = (n: number, d: number) => (d > 0 ? n.toFixed(d) : Math.round(n).toLocaleString());
|
|
134
|
+
const run = (el: HTMLElement) => {
|
|
135
|
+
const final = (el.textContent || '').trim();
|
|
136
|
+
const p = parse(final);
|
|
137
|
+
if (!p) return;
|
|
138
|
+
const dur = 1100;
|
|
139
|
+
const t0 = performance.now();
|
|
140
|
+
const tick = (now: number) => {
|
|
141
|
+
const t = Math.min(1, (now - t0) / dur);
|
|
142
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
143
|
+
el.textContent = p.pre + fmt(p.num * eased, p.decimals) + p.suf;
|
|
144
|
+
if (t < 1) requestAnimationFrame(tick);
|
|
145
|
+
else el.textContent = final;
|
|
146
|
+
};
|
|
147
|
+
requestAnimationFrame(tick);
|
|
148
|
+
};
|
|
149
|
+
const io = new IntersectionObserver(
|
|
150
|
+
(entries) => entries.forEach((e) => { if (e.isIntersecting) { run(e.target as HTMLElement); io.unobserve(e.target); } }),
|
|
151
|
+
{ threshold: 0.4 }
|
|
152
|
+
);
|
|
153
|
+
values.forEach((v) => io.observe(v));
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
@@ -1,121 +1,124 @@
|
|
|
1
|
-
---
|
|
2
|
-
/** Steps — a Level-2 recipe: a numbered process. `horizontal` lays connected
|
|
3
|
-
* cards in a row; `vertical` a stacked timeline. Composes Heading + Icon. */
|
|
4
|
-
import Heading from '../blocks/Heading.astro';
|
|
5
|
-
import Icon from '../Icon.astro';
|
|
6
|
-
import { isIconKey } from '../../lib/icons';
|
|
7
|
-
|
|
8
|
-
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
9
|
-
interface Item { title: string; text?: string; icon?: string }
|
|
10
|
-
interface Props {
|
|
11
|
-
title?: string;
|
|
12
|
-
intro?: string;
|
|
13
|
-
layout?: 'horizontal' | 'vertical';
|
|
14
|
-
items?: Item[];
|
|
15
|
-
}
|
|
16
|
-
const { title, intro, layout = 'horizontal', items = [] } = Astro.props;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{item.
|
|
35
|
-
</
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
</
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.b-
|
|
91
|
-
|
|
92
|
-
/*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
1
|
+
---
|
|
2
|
+
/** Steps — a Level-2 recipe: a numbered process. `horizontal` lays connected
|
|
3
|
+
* cards in a row; `vertical` a stacked timeline. Composes Heading + Icon. */
|
|
4
|
+
import Heading from '../blocks/Heading.astro';
|
|
5
|
+
import Icon from '../Icon.astro';
|
|
6
|
+
import { isIconKey } from '../../lib/icons';
|
|
7
|
+
|
|
8
|
+
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
9
|
+
interface Item { title: string; text?: string; icon?: string }
|
|
10
|
+
interface Props {
|
|
11
|
+
title?: string;
|
|
12
|
+
intro?: string;
|
|
13
|
+
layout?: 'horizontal' | 'vertical';
|
|
14
|
+
items?: Item[];
|
|
15
|
+
}
|
|
16
|
+
const { title, intro, layout = 'horizontal', items = [] } = Astro.props;
|
|
17
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
18
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
19
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
20
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<section class="b-steps" data-layout={layout}>
|
|
24
|
+
{(title || intro) && (
|
|
25
|
+
<div class="b-steps__head" data-recipe-head data-content-align={headAlign}>
|
|
26
|
+
{title && <Heading text={title} level={2} />}
|
|
27
|
+
{intro && <p class="b-steps__intro" set:html={inlineMarkdown(intro)} />}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
<ol class="b-steps__list">
|
|
31
|
+
{items.map((item, i) => (
|
|
32
|
+
<li class="b-steps__item">
|
|
33
|
+
<span class="b-steps__marker" aria-hidden="true">
|
|
34
|
+
{item.icon && isIconKey(item.icon) ? <Icon name={item.icon} size={20} /> : i + 1}
|
|
35
|
+
</span>
|
|
36
|
+
<div class="b-steps__body">
|
|
37
|
+
<h3 class="b-steps__title">{item.title}</h3>
|
|
38
|
+
{item.text && <p class="b-steps__text">{item.text}</p>}
|
|
39
|
+
</div>
|
|
40
|
+
</li>
|
|
41
|
+
))}
|
|
42
|
+
</ol>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<style>
|
|
46
|
+
.b-steps {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: 2rem;
|
|
50
|
+
max-width: 72rem;
|
|
51
|
+
margin-inline: auto;
|
|
52
|
+
padding-inline: 1rem;
|
|
53
|
+
}
|
|
54
|
+
.b-steps__head {
|
|
55
|
+
max-width: var(--max);
|
|
56
|
+
margin-inline: auto;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: 0.75rem;
|
|
60
|
+
}
|
|
61
|
+
.b-steps__intro { margin: 0; color: var(--muted); line-height: 1.6; }
|
|
62
|
+
.b-steps__list {
|
|
63
|
+
list-style: none;
|
|
64
|
+
margin: 0;
|
|
65
|
+
padding: 0;
|
|
66
|
+
display: grid;
|
|
67
|
+
gap: 1.5rem;
|
|
68
|
+
}
|
|
69
|
+
/* Base (and mobile): marker left, body right. */
|
|
70
|
+
.b-steps__item {
|
|
71
|
+
display: flex;
|
|
72
|
+
gap: 1rem;
|
|
73
|
+
align-items: flex-start;
|
|
74
|
+
}
|
|
75
|
+
.b-steps__marker {
|
|
76
|
+
flex-shrink: 0;
|
|
77
|
+
width: 2.5rem;
|
|
78
|
+
height: 2.5rem;
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
border-radius: var(--radius-pill);
|
|
83
|
+
background: var(--gold);
|
|
84
|
+
color: var(--text-light);
|
|
85
|
+
font-family: var(--font-heading);
|
|
86
|
+
font-weight: 700;
|
|
87
|
+
font-size: 1.05rem;
|
|
88
|
+
}
|
|
89
|
+
.b-steps__title { margin: 0 0 0.35rem; font-size: 1.1rem; color: var(--fg); }
|
|
90
|
+
.b-steps__text { margin: 0; color: var(--muted); line-height: 1.6; }
|
|
91
|
+
|
|
92
|
+
/* Vertical: a centred, stacked timeline. */
|
|
93
|
+
.b-steps[data-layout='vertical'] .b-steps__list { max-width: 40rem; margin-inline: auto; }
|
|
94
|
+
|
|
95
|
+
/* Horizontal (≥48em): connected cards in a row, marker over centred body. */
|
|
96
|
+
@media (min-width: 48em) {
|
|
97
|
+
.b-steps[data-layout='horizontal'] .b-steps__list {
|
|
98
|
+
grid-auto-flow: column;
|
|
99
|
+
grid-auto-columns: 1fr;
|
|
100
|
+
gap: 1rem;
|
|
101
|
+
}
|
|
102
|
+
.b-steps[data-layout='horizontal'] .b-steps__item {
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
align-items: center;
|
|
105
|
+
text-align: center;
|
|
106
|
+
gap: 0.85rem;
|
|
107
|
+
position: relative;
|
|
108
|
+
}
|
|
109
|
+
/* Connector from this marker's right edge to the next marker's left edge.
|
|
110
|
+
The +1.5rem / -2rem account for the 2.5rem marker AND the 1rem grid gap,
|
|
111
|
+
so the line sits centred between markers (not falling short). */
|
|
112
|
+
.b-steps[data-layout='horizontal'] .b-steps__item:not(:last-child)::after {
|
|
113
|
+
content: '';
|
|
114
|
+
position: absolute;
|
|
115
|
+
top: calc(1.25rem - 1px);
|
|
116
|
+
left: calc(50% + 1.5rem);
|
|
117
|
+
width: calc(100% - 2rem);
|
|
118
|
+
height: 2px;
|
|
119
|
+
background: var(--border);
|
|
120
|
+
z-index: 0;
|
|
121
|
+
}
|
|
122
|
+
.b-steps[data-layout='horizontal'] .b-steps__marker { position: relative; z-index: 1; }
|
|
123
|
+
}
|
|
124
|
+
</style>
|