ferst-core 0.7.7 → 0.7.8
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/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 +502 -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 +75 -49
- package/layouts/Base.astro +495 -484
- package/lib/slug.ts +1 -1
- package/package.json +1 -1
|
@@ -1,287 +1,291 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Tiles — a Level-2 recipe: a responsive grid of tiles in one of five looks,
|
|
4
|
-
* chosen with `variant`:
|
|
5
|
-
* outline — stroke only, no shadow (the "wireframe" look)
|
|
6
|
-
* elevated — soft shadow, lifts on hover
|
|
7
|
-
* filled — soft brand-tinted background
|
|
8
|
-
* gradient — brand → derived accent-2 gradient, light text
|
|
9
|
-
* image — full-bleed photo with a legibility scrim, light text
|
|
10
|
-
*
|
|
11
|
-
* Radius, stroke and elevation come from the GLOBAL surface tokens (--radius,
|
|
12
|
-
* --card-border, --shadow-*), so tiles stay consistent with every other card and
|
|
13
|
-
* a single knob restyles them all. A tile with `href` becomes one big link.
|
|
14
|
-
* Composes the Icon primitive.
|
|
15
|
-
*/
|
|
16
|
-
import Heading from '../blocks/Heading.astro';
|
|
17
|
-
import Icon from '../Icon.astro';
|
|
18
|
-
import { isIconKey } from '../../lib/icons';
|
|
19
|
-
|
|
20
|
-
import { inlineMarkdown, inlineMarkdownNoLinks } from '../../lib/inline-markdown';
|
|
21
|
-
interface Tile {
|
|
22
|
-
icon?: string;
|
|
23
|
-
eyebrow?: string;
|
|
24
|
-
title?: string;
|
|
25
|
-
text?: string;
|
|
26
|
-
href?: string;
|
|
27
|
-
linkLabel?: string;
|
|
28
|
-
image?: { src: string; alt?: string };
|
|
29
|
-
}
|
|
30
|
-
interface Props {
|
|
31
|
-
title?: string;
|
|
32
|
-
intro?: string;
|
|
33
|
-
columns?: 2 | 3 | 4;
|
|
34
|
-
variant?: 'outline' | 'elevated' | 'filled' | 'gradient' | 'image';
|
|
35
|
-
spotlight?: boolean;
|
|
36
|
-
tiles?: Tile[];
|
|
37
|
-
}
|
|
38
|
-
const { title, intro, columns = 3, variant = 'elevated', spotlight = false, tiles = [] } = Astro.props;
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
{tile.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{tile.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
</
|
|
81
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.b-tiles__grid[data-cols='4'] { grid-template-columns: repeat(
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
border: var(--card-border);
|
|
144
|
-
}
|
|
145
|
-
.b-tiles__grid[data-variant='
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
.b-tiles__grid[data-variant='
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
content:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
.b-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
color:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
.b-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
color:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.b-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
color:
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Tiles — a Level-2 recipe: a responsive grid of tiles in one of five looks,
|
|
4
|
+
* chosen with `variant`:
|
|
5
|
+
* outline — stroke only, no shadow (the "wireframe" look)
|
|
6
|
+
* elevated — soft shadow, lifts on hover
|
|
7
|
+
* filled — soft brand-tinted background
|
|
8
|
+
* gradient — brand → derived accent-2 gradient, light text
|
|
9
|
+
* image — full-bleed photo with a legibility scrim, light text
|
|
10
|
+
*
|
|
11
|
+
* Radius, stroke and elevation come from the GLOBAL surface tokens (--radius,
|
|
12
|
+
* --card-border, --shadow-*), so tiles stay consistent with every other card and
|
|
13
|
+
* a single knob restyles them all. A tile with `href` becomes one big link.
|
|
14
|
+
* Composes the Icon primitive.
|
|
15
|
+
*/
|
|
16
|
+
import Heading from '../blocks/Heading.astro';
|
|
17
|
+
import Icon from '../Icon.astro';
|
|
18
|
+
import { isIconKey } from '../../lib/icons';
|
|
19
|
+
|
|
20
|
+
import { inlineMarkdown, inlineMarkdownNoLinks } from '../../lib/inline-markdown';
|
|
21
|
+
interface Tile {
|
|
22
|
+
icon?: string;
|
|
23
|
+
eyebrow?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
text?: string;
|
|
26
|
+
href?: string;
|
|
27
|
+
linkLabel?: string;
|
|
28
|
+
image?: { src: string; alt?: string };
|
|
29
|
+
}
|
|
30
|
+
interface Props {
|
|
31
|
+
title?: string;
|
|
32
|
+
intro?: string;
|
|
33
|
+
columns?: 2 | 3 | 4;
|
|
34
|
+
variant?: 'outline' | 'elevated' | 'filled' | 'gradient' | 'image';
|
|
35
|
+
spotlight?: boolean;
|
|
36
|
+
tiles?: Tile[];
|
|
37
|
+
}
|
|
38
|
+
const { title, intro, columns = 3, variant = 'elevated', spotlight = false, tiles = [] } = Astro.props;
|
|
39
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
40
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
41
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
42
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
43
|
+
// Never reserve more columns than there are tiles; `data-single` centres a lone one.
|
|
44
|
+
const cols = Math.min(columns, Math.max(tiles.length, 1)) as 1 | 2 | 3 | 4;
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<section class="b-tiles">
|
|
48
|
+
{(title || intro) && (
|
|
49
|
+
<div class="b-tiles__head" data-recipe-head data-content-align={headAlign}>
|
|
50
|
+
{title && <Heading text={title} level={2} />}
|
|
51
|
+
{intro && <p class="b-tiles__intro" set:html={inlineMarkdown(intro)} />}
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
<div class="b-tiles__grid" data-cols={cols} data-single={tiles.length === 1 ? 'true' : undefined} data-variant={variant} data-spotlight={spotlight ? 'true' : 'false'}>
|
|
55
|
+
{tiles.map((tile) => {
|
|
56
|
+
const Tag = tile.href ? 'a' : 'div';
|
|
57
|
+
// A picture tile: an image with no words, shown WHOLE as a real <img> (posters,
|
|
58
|
+
// wide logos, badges must never be cropped), its alt naming the tile / link.
|
|
59
|
+
const plain = variant === 'image' && !!tile.image && !tile.title && !tile.text && !tile.eyebrow;
|
|
60
|
+
const bg =
|
|
61
|
+
variant === 'image' && tile.image && !plain
|
|
62
|
+
? `--tile-image: url('${tile.image.src}')`
|
|
63
|
+
: undefined;
|
|
64
|
+
return (
|
|
65
|
+
<Tag
|
|
66
|
+
class="b-tile"
|
|
67
|
+
href={tile.href}
|
|
68
|
+
style={bg}
|
|
69
|
+
data-plain={plain ? 'true' : undefined}
|
|
70
|
+
>
|
|
71
|
+
{plain && <img class="b-tile__picture" src={tile.image!.src} alt={tile.image!.alt ?? ''} loading="lazy" decoding="async" />}
|
|
72
|
+
{tile.icon && isIconKey(tile.icon) && (
|
|
73
|
+
<span class="b-tile__icon"><Icon name={tile.icon} size={26} /></span>
|
|
74
|
+
)}
|
|
75
|
+
{tile.eyebrow && <span class="b-tile__eyebrow">{tile.eyebrow}</span>}
|
|
76
|
+
{tile.title && <h3 class="b-tile__title">{tile.title}</h3>}
|
|
77
|
+
{/* A linked tile is one big link: its text keeps breaks and emphasis, no inner links. */}
|
|
78
|
+
{tile.text && <p class="b-tile__text" set:html={tile.href ? inlineMarkdownNoLinks(tile.text) : inlineMarkdown(tile.text)} />}
|
|
79
|
+
{tile.href && !plain && <span class="b-tile__more" aria-hidden="true">{tile.linkLabel ?? 'Learn more'} →</span>}
|
|
80
|
+
</Tag>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<style>
|
|
87
|
+
.b-tiles {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: 2rem;
|
|
91
|
+
max-width: 72rem;
|
|
92
|
+
margin-inline: auto;
|
|
93
|
+
padding-inline: 1rem;
|
|
94
|
+
}
|
|
95
|
+
.b-tiles__head {
|
|
96
|
+
max-width: var(--max);
|
|
97
|
+
margin-inline: auto;
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 0.75rem;
|
|
101
|
+
}
|
|
102
|
+
.b-tiles__intro {
|
|
103
|
+
margin: 0;
|
|
104
|
+
color: var(--muted);
|
|
105
|
+
font-size: 1.0625rem;
|
|
106
|
+
line-height: 1.6;
|
|
107
|
+
}
|
|
108
|
+
.b-tiles__grid {
|
|
109
|
+
display: grid;
|
|
110
|
+
grid-template-columns: 1fr;
|
|
111
|
+
gap: 1.25rem;
|
|
112
|
+
}
|
|
113
|
+
/* A solitary tile is a centred feature, not a full-width slab. */
|
|
114
|
+
.b-tiles__grid[data-single] { max-width: 26rem; margin-inline: auto; }
|
|
115
|
+
@media (min-width: 48em) {
|
|
116
|
+
.b-tiles__grid[data-cols='2'] { grid-template-columns: repeat(2, 1fr); }
|
|
117
|
+
.b-tiles__grid[data-cols='3'] { grid-template-columns: repeat(3, 1fr); }
|
|
118
|
+
.b-tiles__grid[data-cols='4'] { grid-template-columns: repeat(2, 1fr); }
|
|
119
|
+
}
|
|
120
|
+
@media (min-width: 64em) {
|
|
121
|
+
.b-tiles__grid[data-cols='4'] { grid-template-columns: repeat(4, 1fr); }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Base tile — radius/stroke/shadow all from the global surface tokens. */
|
|
125
|
+
.b-tile {
|
|
126
|
+
position: relative;
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
align-items: var(--content-items, stretch); /* follows the page / section alignment */
|
|
130
|
+
gap: 0.5rem;
|
|
131
|
+
padding: 1.5rem;
|
|
132
|
+
border-radius: var(--radius);
|
|
133
|
+
background: var(--bg-surface);
|
|
134
|
+
color: var(--fg);
|
|
135
|
+
text-decoration: none;
|
|
136
|
+
overflow: hidden;
|
|
137
|
+
isolation: isolate;
|
|
138
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ── Variants ── */
|
|
142
|
+
.b-tiles__grid[data-variant='outline'] .b-tile {
|
|
143
|
+
border: var(--card-border);
|
|
144
|
+
}
|
|
145
|
+
.b-tiles__grid[data-variant='elevated'] .b-tile {
|
|
146
|
+
box-shadow: var(--shadow-md);
|
|
147
|
+
border: var(--card-border);
|
|
148
|
+
}
|
|
149
|
+
.b-tiles__grid[data-variant='filled'] .b-tile {
|
|
150
|
+
background: var(--gold-light);
|
|
151
|
+
}
|
|
152
|
+
.b-tiles__grid[data-variant='gradient'] .b-tile {
|
|
153
|
+
background: linear-gradient(135deg, var(--gold), var(--accent-2, var(--gold)));
|
|
154
|
+
color: var(--text-light);
|
|
155
|
+
}
|
|
156
|
+
.b-tiles__grid[data-variant='image'] .b-tile {
|
|
157
|
+
color: #fff;
|
|
158
|
+
min-height: 15rem;
|
|
159
|
+
justify-content: flex-end;
|
|
160
|
+
background: var(--bg-section);
|
|
161
|
+
}
|
|
162
|
+
.b-tiles__grid[data-variant='image'] .b-tile::before {
|
|
163
|
+
content: '';
|
|
164
|
+
position: absolute;
|
|
165
|
+
inset: 0;
|
|
166
|
+
z-index: -1;
|
|
167
|
+
background-image: linear-gradient(to top, rgba(10, 12, 20, 0.85), rgba(10, 12, 20, 0.15)), var(--tile-image);
|
|
168
|
+
background-size: cover;
|
|
169
|
+
background-position: center;
|
|
170
|
+
}
|
|
171
|
+
/* A picture tile shows the whole image, whatever its shape: no scrim, no crop. */
|
|
172
|
+
.b-tiles__grid[data-variant='image'] .b-tile[data-plain] {
|
|
173
|
+
min-height: 0;
|
|
174
|
+
justify-content: center;
|
|
175
|
+
align-items: center;
|
|
176
|
+
padding: 0.75rem;
|
|
177
|
+
background: var(--bg-surface);
|
|
178
|
+
border: var(--card-border);
|
|
179
|
+
}
|
|
180
|
+
.b-tiles__grid[data-variant='image'] .b-tile[data-plain]::before {
|
|
181
|
+
content: none;
|
|
182
|
+
}
|
|
183
|
+
.b-tile__picture {
|
|
184
|
+
display: block;
|
|
185
|
+
width: 100%;
|
|
186
|
+
height: auto;
|
|
187
|
+
max-height: 18rem;
|
|
188
|
+
object-fit: contain;
|
|
189
|
+
}
|
|
190
|
+
a.b-tile[data-plain]:focus-visible {
|
|
191
|
+
outline: 3px solid var(--accent);
|
|
192
|
+
outline-offset: 3px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Hover: only tiles that link lift. */
|
|
196
|
+
a.b-tile:hover {
|
|
197
|
+
transform: translateY(-4px);
|
|
198
|
+
}
|
|
199
|
+
.b-tiles__grid[data-variant='elevated'] a.b-tile:hover {
|
|
200
|
+
box-shadow: var(--shadow-lg);
|
|
201
|
+
}
|
|
202
|
+
.b-tiles__grid[data-variant='outline'] a.b-tile:hover {
|
|
203
|
+
border-color: var(--gold);
|
|
204
|
+
}
|
|
205
|
+
a.b-tile:focus-visible {
|
|
206
|
+
outline: 2px solid var(--gold);
|
|
207
|
+
outline-offset: 2px;
|
|
208
|
+
}
|
|
209
|
+
@media (prefers-reduced-motion: reduce) {
|
|
210
|
+
.b-tile { transition: none; }
|
|
211
|
+
a.b-tile:hover { transform: none; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.b-tile__icon {
|
|
215
|
+
display: inline-flex;
|
|
216
|
+
color: var(--gold);
|
|
217
|
+
margin-bottom: 0.25rem;
|
|
218
|
+
}
|
|
219
|
+
.b-tiles__grid[data-variant='gradient'] .b-tile__icon,
|
|
220
|
+
.b-tiles__grid[data-variant='image'] .b-tile__icon {
|
|
221
|
+
color: currentColor;
|
|
222
|
+
}
|
|
223
|
+
.b-tile__eyebrow {
|
|
224
|
+
font-size: 0.75rem;
|
|
225
|
+
font-weight: 700;
|
|
226
|
+
letter-spacing: 0.1em;
|
|
227
|
+
text-transform: uppercase;
|
|
228
|
+
color: var(--gold);
|
|
229
|
+
}
|
|
230
|
+
.b-tiles__grid[data-variant='gradient'] .b-tile__eyebrow,
|
|
231
|
+
.b-tiles__grid[data-variant='image'] .b-tile__eyebrow {
|
|
232
|
+
color: currentColor;
|
|
233
|
+
opacity: 0.85;
|
|
234
|
+
}
|
|
235
|
+
.b-tile__title {
|
|
236
|
+
margin: 0;
|
|
237
|
+
font-size: 1.2rem;
|
|
238
|
+
color: inherit;
|
|
239
|
+
}
|
|
240
|
+
.b-tile__text {
|
|
241
|
+
margin: 0;
|
|
242
|
+
color: var(--muted);
|
|
243
|
+
line-height: 1.6;
|
|
244
|
+
}
|
|
245
|
+
.b-tiles__grid[data-variant='gradient'] .b-tile__text,
|
|
246
|
+
.b-tiles__grid[data-variant='image'] .b-tile__text,
|
|
247
|
+
.b-tiles__grid[data-variant='filled'] .b-tile__text {
|
|
248
|
+
color: inherit;
|
|
249
|
+
opacity: 0.92;
|
|
250
|
+
}
|
|
251
|
+
.b-tile__more {
|
|
252
|
+
margin-top: auto;
|
|
253
|
+
padding-top: 0.5rem;
|
|
254
|
+
font-size: 0.85rem;
|
|
255
|
+
font-weight: 600;
|
|
256
|
+
color: var(--gold);
|
|
257
|
+
}
|
|
258
|
+
.b-tiles__grid[data-variant='gradient'] .b-tile__more,
|
|
259
|
+
.b-tiles__grid[data-variant='image'] .b-tile__more {
|
|
260
|
+
color: currentColor;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* Spotlight — a soft glow that follows the pointer on hover (opt-in). The glow
|
|
264
|
+
sits above the surface but below the content, so text stays crisp. */
|
|
265
|
+
.b-tiles__grid[data-spotlight='true'] .b-tile > * { position: relative; z-index: 1; }
|
|
266
|
+
.b-tiles__grid[data-spotlight='true'] .b-tile::after {
|
|
267
|
+
content: '';
|
|
268
|
+
position: absolute;
|
|
269
|
+
inset: 0;
|
|
270
|
+
z-index: 0;
|
|
271
|
+
border-radius: inherit;
|
|
272
|
+
background: radial-gradient(18rem 18rem at var(--mx, 50%) var(--my, 50%), color-mix(in srgb, var(--gold) 22%, transparent), transparent 60%);
|
|
273
|
+
opacity: 0;
|
|
274
|
+
transition: opacity 0.25s ease;
|
|
275
|
+
pointer-events: none;
|
|
276
|
+
}
|
|
277
|
+
.b-tiles__grid[data-spotlight='true'] .b-tile:hover::after { opacity: 1; }
|
|
278
|
+
</style>
|
|
279
|
+
|
|
280
|
+
<script>
|
|
281
|
+
// Spotlight: track the pointer over opt-in tiles and drive the glow position.
|
|
282
|
+
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
283
|
+
document.querySelectorAll<HTMLElement>('.b-tiles__grid[data-spotlight="true"] .b-tile').forEach((tile) => {
|
|
284
|
+
tile.addEventListener('pointermove', (e) => {
|
|
285
|
+
const r = tile.getBoundingClientRect();
|
|
286
|
+
tile.style.setProperty('--mx', `${((e.clientX - r.left) / r.width) * 100}%`);
|
|
287
|
+
tile.style.setProperty('--my', `${((e.clientY - r.top) / r.height) * 100}%`);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
</script>
|