ferst-core 0.1.0 → 0.2.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.
- package/LICENSE +102 -201
- package/LICENSE-Apache-2.0 +201 -0
- package/NOTICE +24 -5
- package/README.md +89 -73
- package/bin/check-thin.mjs +74 -0
- package/components/BlockRenderer.astro +89 -0
- package/components/Button.astro +85 -33
- package/components/CalendarEmbed.astro +188 -0
- package/components/Card.astro +7 -17
- package/components/Icon.astro +35 -0
- package/components/PostArticle.astro +133 -0
- package/components/PostCard.astro +85 -225
- package/components/SiteFooter.astro +2 -2
- package/components/SiteHeader.astro +104 -51
- package/components/blocks/Badge.astro +29 -0
- package/components/blocks/ButtonBlock.astro +13 -0
- package/components/blocks/Divider.astro +12 -0
- package/components/blocks/Grid.astro +48 -0
- package/components/blocks/Heading.astro +42 -0
- package/components/blocks/ImageBlock.astro +80 -0
- package/components/blocks/List.astro +78 -0
- package/components/blocks/Prose.astro +29 -0
- package/components/blocks/Quote.astro +32 -0
- package/components/blocks/Section.astro +52 -0
- package/components/blocks/Spacer.astro +20 -0
- package/components/blocks/Stack.astro +30 -0
- package/components/blocks/Stat.astro +30 -0
- package/components/recipes/Accordion.astro +84 -0
- package/components/recipes/Announcement.astro +74 -0
- package/components/recipes/Banner.astro +134 -0
- package/components/recipes/Bento.astro +138 -0
- package/components/recipes/ContactForm.astro +172 -0
- package/components/recipes/Cta.astro +83 -0
- package/components/recipes/Faq.astro +80 -0
- package/components/recipes/FeatureCards.astro +105 -0
- package/components/recipes/Gallery.astro +55 -0
- package/components/recipes/Hero.astro +191 -0
- package/components/recipes/Logos.astro +77 -0
- package/components/recipes/Marquee.astro +75 -0
- package/components/recipes/Pricing.astro +149 -0
- package/components/recipes/Stats.astro +124 -0
- package/components/recipes/Steps.astro +120 -0
- package/components/recipes/Tabs.astro +133 -0
- package/components/recipes/Testimonial.astro +66 -0
- package/components/recipes/Tiles.astro +248 -0
- package/content/blocks.ts +534 -0
- package/content/calendar.ts +40 -0
- package/content/collections.ts +25 -14
- package/content/posts.ts +89 -0
- package/content/schemas.ts +58 -123
- package/layouts/Base.astro +30 -56
- package/lib/calendar.ts +12 -0
- package/lib/icons.ts +64 -0
- package/lib/pages.ts +42 -0
- package/lib/posts.ts +93 -0
- package/lib/siteSettings.ts +18 -36
- package/lib/themeTokens.ts +163 -0
- package/package.json +61 -48
- package/styles/theme.css +42 -0
- package/components/DocLayout.astro +0 -292
- package/components/GroupCard.astro +0 -164
- package/components/LatestPostList.astro +0 -64
- package/components/PaginationNav.astro +0 -81
- package/components/PostsToolbar.astro +0 -83
- package/content/filters.ts +0 -107
- package/lib/mailLinks.ts +0 -13
- package/lib/mapsLink.ts +0 -7
- package/utils/excerpt.ts +0 -56
- package/utils/formatDate.ts +0 -22
- package/utils/freshness.ts +0 -5
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Banner — a Level-2 recipe: a full-bleed feature band with a fixed/parallax
|
|
4
|
+
* background image, a legibility scrim, and a floating "shaded" content card
|
|
5
|
+
* over it. Composes Heading + Button.
|
|
6
|
+
*
|
|
7
|
+
* Parallax is pure CSS (`background-attachment: fixed`) — no JS, GPU-composited.
|
|
8
|
+
* It's auto-disabled on touch/coarse pointers (where `fixed` is janky or ignored,
|
|
9
|
+
* e.g. iOS) and under `prefers-reduced-motion`, both falling back to a clean
|
|
10
|
+
* `scroll` background. The card uses the surface tokens (radius, shadow) so it
|
|
11
|
+
* matches every other surface, and a backdrop blur for the "shaded glass" look.
|
|
12
|
+
*/
|
|
13
|
+
import Heading from '../blocks/Heading.astro';
|
|
14
|
+
import Button from '../Button.astro';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
image: string;
|
|
18
|
+
eyebrow?: string;
|
|
19
|
+
title: string;
|
|
20
|
+
text?: string;
|
|
21
|
+
cta?: { label: string; href: string };
|
|
22
|
+
overlay?: number;
|
|
23
|
+
align?: 'start' | 'center' | 'end';
|
|
24
|
+
parallax?: boolean;
|
|
25
|
+
}
|
|
26
|
+
const { image, eyebrow, title, text, cta, overlay = 50, align = 'start', parallax = true } = Astro.props;
|
|
27
|
+
const scrim = overlay / 100;
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<section
|
|
31
|
+
class="b-banner"
|
|
32
|
+
data-align={align}
|
|
33
|
+
data-parallax={parallax ? 'true' : 'false'}
|
|
34
|
+
style={`--banner-image: url('${image}'); --banner-scrim: ${scrim};`}
|
|
35
|
+
>
|
|
36
|
+
<div class="b-banner__scrim" aria-hidden="true"></div>
|
|
37
|
+
<div class="b-banner__card">
|
|
38
|
+
{eyebrow && <p class="b-banner__eyebrow">{eyebrow}</p>}
|
|
39
|
+
<Heading text={title} level={2} align="start" />
|
|
40
|
+
{text && <p class="b-banner__text">{text}</p>}
|
|
41
|
+
{cta && (
|
|
42
|
+
<div class="b-banner__cta">
|
|
43
|
+
<Button href={cta.href} variant="cta">{cta.label}</Button>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
.b-banner {
|
|
51
|
+
position: relative;
|
|
52
|
+
isolation: isolate;
|
|
53
|
+
display: grid;
|
|
54
|
+
padding: clamp(3rem, 9vw, 7rem) clamp(1.25rem, 5vw, 4rem);
|
|
55
|
+
min-height: min(60vh, 34rem);
|
|
56
|
+
border-radius: var(--radius);
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
background-image: var(--banner-image);
|
|
59
|
+
background-size: cover;
|
|
60
|
+
background-position: center;
|
|
61
|
+
background-repeat: no-repeat;
|
|
62
|
+
}
|
|
63
|
+
/* Parallax: pin the image to the viewport so it slides under the content.
|
|
64
|
+
Only on fine pointers with motion allowed — `fixed` is ignored/janky on
|
|
65
|
+
touch (iOS) and unwelcome under reduced-motion. */
|
|
66
|
+
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
|
|
67
|
+
.b-banner[data-parallax='true'] {
|
|
68
|
+
background-attachment: fixed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.b-banner[data-align='start'] { justify-items: start; }
|
|
73
|
+
.b-banner[data-align='center'] { justify-items: center; text-align: center; }
|
|
74
|
+
.b-banner[data-align='end'] { justify-items: end; }
|
|
75
|
+
|
|
76
|
+
.b-banner__scrim {
|
|
77
|
+
position: absolute;
|
|
78
|
+
inset: 0;
|
|
79
|
+
z-index: -1;
|
|
80
|
+
background: linear-gradient(to top, rgba(8, 10, 18, calc(var(--banner-scrim) + 0.2)), rgba(8, 10, 18, var(--banner-scrim)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* The floating "shaded glass" card. */
|
|
84
|
+
.b-banner__card {
|
|
85
|
+
max-width: 34rem;
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
gap: 1rem;
|
|
89
|
+
padding: clamp(1.5rem, 3vw, 2.25rem);
|
|
90
|
+
border-radius: var(--radius);
|
|
91
|
+
background: color-mix(in srgb, var(--bg-surface) 82%, transparent);
|
|
92
|
+
border: var(--card-border);
|
|
93
|
+
box-shadow: var(--shadow-lg);
|
|
94
|
+
-webkit-backdrop-filter: blur(10px);
|
|
95
|
+
backdrop-filter: blur(10px);
|
|
96
|
+
color: var(--fg);
|
|
97
|
+
}
|
|
98
|
+
.b-banner__eyebrow {
|
|
99
|
+
margin: 0;
|
|
100
|
+
text-transform: uppercase;
|
|
101
|
+
letter-spacing: 0.12em;
|
|
102
|
+
font-size: 0.8125rem;
|
|
103
|
+
font-weight: 700;
|
|
104
|
+
color: var(--gold);
|
|
105
|
+
}
|
|
106
|
+
.b-banner__text {
|
|
107
|
+
margin: 0;
|
|
108
|
+
color: var(--muted);
|
|
109
|
+
line-height: 1.65;
|
|
110
|
+
}
|
|
111
|
+
.b-banner__cta {
|
|
112
|
+
margin-top: 0.25rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Multi-layer parallax: the background is pinned (fixed), and the card itself
|
|
116
|
+
drifts at a different rate as the banner scrolls through view — two layers,
|
|
117
|
+
two speeds. Native scroll-timeline, so no JS; off under reduced-motion or
|
|
118
|
+
where unsupported (the card just sits still). Transforming the card is safe:
|
|
119
|
+
it's not an ancestor of the fixed background. */
|
|
120
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
121
|
+
@supports (animation-timeline: view()) {
|
|
122
|
+
.b-banner__card {
|
|
123
|
+
animation: b-banner-drift linear both;
|
|
124
|
+
animation-timeline: view();
|
|
125
|
+
animation-range: cover;
|
|
126
|
+
will-change: transform;
|
|
127
|
+
}
|
|
128
|
+
@keyframes b-banner-drift {
|
|
129
|
+
from { transform: translateY(48px); }
|
|
130
|
+
to { transform: translateY(-48px); }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
</style>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Bento — a Level-2 recipe: an asymmetric grid where each card picks a `size`
|
|
3
|
+
* (sm / wide / tall / lg) and a `variant` look (surface / filled / gradient /
|
|
4
|
+
* image). Dense auto-flow packs them like a Japanese bento box. Composes Icon;
|
|
5
|
+
* radius/stroke/shadow come from the global surface tokens. */
|
|
6
|
+
import Heading from '../blocks/Heading.astro';
|
|
7
|
+
import Icon from '../Icon.astro';
|
|
8
|
+
import { isIconKey } from '../../lib/icons';
|
|
9
|
+
|
|
10
|
+
interface Item {
|
|
11
|
+
size?: 'sm' | 'wide' | 'tall' | 'lg';
|
|
12
|
+
variant?: 'surface' | 'filled' | 'gradient' | 'image';
|
|
13
|
+
icon?: string;
|
|
14
|
+
eyebrow?: string;
|
|
15
|
+
title: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
href?: string;
|
|
18
|
+
image?: { src: string; alt?: string };
|
|
19
|
+
}
|
|
20
|
+
interface Props {
|
|
21
|
+
title?: string;
|
|
22
|
+
intro?: string;
|
|
23
|
+
items?: Item[];
|
|
24
|
+
}
|
|
25
|
+
const { title, intro, items = [] } = Astro.props;
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<section class="b-bento">
|
|
29
|
+
{(title || intro) && (
|
|
30
|
+
<div class="b-bento__head">
|
|
31
|
+
{title && <Heading text={title} level={2} align="center" />}
|
|
32
|
+
{intro && <p class="b-bento__intro">{intro}</p>}
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
<div class="b-bento__grid">
|
|
36
|
+
{items.map((item) => {
|
|
37
|
+
const Tag = item.href ? 'a' : 'div';
|
|
38
|
+
const style = item.variant === 'image' && item.image ? `--bento-image: url('${item.image.src}')` : undefined;
|
|
39
|
+
return (
|
|
40
|
+
<Tag class="b-bento__item" data-size={item.size ?? 'sm'} data-variant={item.variant ?? 'surface'} href={item.href} style={style}>
|
|
41
|
+
{item.icon && isIconKey(item.icon) && (
|
|
42
|
+
<span class="b-bento__icon"><Icon name={item.icon} size={24} /></span>
|
|
43
|
+
)}
|
|
44
|
+
{item.eyebrow && <span class="b-bento__eyebrow">{item.eyebrow}</span>}
|
|
45
|
+
<h3 class="b-bento__title">{item.title}</h3>
|
|
46
|
+
{item.text && <p class="b-bento__text">{item.text}</p>}
|
|
47
|
+
</Tag>
|
|
48
|
+
);
|
|
49
|
+
})}
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
<style>
|
|
54
|
+
.b-bento {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
gap: 2rem;
|
|
58
|
+
max-width: 72rem;
|
|
59
|
+
margin-inline: auto;
|
|
60
|
+
padding-inline: 1rem;
|
|
61
|
+
}
|
|
62
|
+
.b-bento__head {
|
|
63
|
+
max-width: var(--max);
|
|
64
|
+
margin-inline: auto;
|
|
65
|
+
text-align: center;
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 0.75rem;
|
|
69
|
+
}
|
|
70
|
+
.b-bento__intro { margin: 0; color: var(--muted); line-height: 1.6; }
|
|
71
|
+
.b-bento__grid {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: 1fr;
|
|
74
|
+
gap: 1rem;
|
|
75
|
+
}
|
|
76
|
+
@media (min-width: 48em) {
|
|
77
|
+
.b-bento__grid {
|
|
78
|
+
grid-template-columns: repeat(2, 1fr);
|
|
79
|
+
grid-auto-rows: minmax(11rem, auto);
|
|
80
|
+
grid-auto-flow: dense;
|
|
81
|
+
}
|
|
82
|
+
.b-bento__item[data-size='wide'] { grid-column: span 2; }
|
|
83
|
+
.b-bento__item[data-size='tall'] { grid-row: span 2; }
|
|
84
|
+
.b-bento__item[data-size='lg'] { grid-column: span 2; grid-row: span 2; }
|
|
85
|
+
}
|
|
86
|
+
@media (min-width: 64em) {
|
|
87
|
+
.b-bento__grid { grid-template-columns: repeat(4, 1fr); }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.b-bento__item {
|
|
91
|
+
position: relative;
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
gap: 0.4rem;
|
|
95
|
+
padding: 1.5rem;
|
|
96
|
+
border-radius: var(--radius);
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
isolation: isolate;
|
|
99
|
+
text-decoration: none;
|
|
100
|
+
color: var(--fg);
|
|
101
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
|
102
|
+
}
|
|
103
|
+
.b-bento__item[data-variant='surface'] { background: var(--bg-surface); border: var(--card-border); box-shadow: var(--shadow-md); }
|
|
104
|
+
.b-bento__item[data-variant='filled'] { background: var(--gold-light); }
|
|
105
|
+
.b-bento__item[data-variant='gradient'] { background: linear-gradient(135deg, var(--gold), var(--accent-2, var(--gold))); color: var(--text-light); }
|
|
106
|
+
.b-bento__item[data-variant='image'] { color: #fff; justify-content: flex-end; min-height: 11rem; }
|
|
107
|
+
.b-bento__item[data-variant='image']::before {
|
|
108
|
+
content: '';
|
|
109
|
+
position: absolute;
|
|
110
|
+
inset: 0;
|
|
111
|
+
z-index: -1;
|
|
112
|
+
background-image: linear-gradient(to top, rgba(8,10,18,0.82), rgba(8,10,18,0.15)), var(--bento-image);
|
|
113
|
+
background-size: cover;
|
|
114
|
+
background-position: center;
|
|
115
|
+
}
|
|
116
|
+
a.b-bento__item:hover { transform: translateY(-3px); }
|
|
117
|
+
a.b-bento__item[data-variant='surface']:hover { box-shadow: var(--shadow-lg); }
|
|
118
|
+
a.b-bento__item:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
|
|
119
|
+
@media (prefers-reduced-motion: reduce) { .b-bento__item { transition: none; } a.b-bento__item:hover { transform: none; } }
|
|
120
|
+
|
|
121
|
+
.b-bento__icon { display: inline-flex; color: var(--gold); }
|
|
122
|
+
.b-bento__item[data-variant='gradient'] .b-bento__icon,
|
|
123
|
+
.b-bento__item[data-variant='image'] .b-bento__icon { color: currentColor; }
|
|
124
|
+
.b-bento__eyebrow {
|
|
125
|
+
font-size: 0.72rem;
|
|
126
|
+
font-weight: 700;
|
|
127
|
+
letter-spacing: 0.1em;
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
color: var(--gold);
|
|
130
|
+
}
|
|
131
|
+
.b-bento__item[data-variant='gradient'] .b-bento__eyebrow,
|
|
132
|
+
.b-bento__item[data-variant='image'] .b-bento__eyebrow { color: currentColor; opacity: 0.85; }
|
|
133
|
+
.b-bento__title { margin: 0; font-size: 1.15rem; color: inherit; }
|
|
134
|
+
.b-bento__text { margin: 0; line-height: 1.55; color: var(--muted); }
|
|
135
|
+
.b-bento__item[data-variant='gradient'] .b-bento__text,
|
|
136
|
+
.b-bento__item[data-variant='image'] .b-bento__text,
|
|
137
|
+
.b-bento__item[data-variant='filled'] .b-bento__text { color: inherit; opacity: 0.92; }
|
|
138
|
+
</style>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ContactForm — a Level-2 recipe: a labelled, accessible form in a centred
|
|
4
|
+
* column (optional heading + intro, a list of fields, a submit, optional note).
|
|
5
|
+
* Composes the Heading + Button primitives.
|
|
6
|
+
*
|
|
7
|
+
* `action` is optional. The core ships no backend, so with no action this
|
|
8
|
+
* renders a standard semantic form that a client later wires to their own
|
|
9
|
+
* endpoint (or a form service). Every colour reads a token, so the form
|
|
10
|
+
* recolours with the brand like every other block.
|
|
11
|
+
*/
|
|
12
|
+
import Heading from '../blocks/Heading.astro';
|
|
13
|
+
import Button from '../Button.astro';
|
|
14
|
+
|
|
15
|
+
interface Field {
|
|
16
|
+
name: string;
|
|
17
|
+
label: string;
|
|
18
|
+
type?: 'text' | 'email' | 'tel' | 'textarea';
|
|
19
|
+
required?: boolean;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
}
|
|
22
|
+
interface Props {
|
|
23
|
+
title?: string;
|
|
24
|
+
intro?: string;
|
|
25
|
+
action?: string;
|
|
26
|
+
method?: 'post' | 'get';
|
|
27
|
+
submitLabel?: string;
|
|
28
|
+
note?: string;
|
|
29
|
+
fields?: Field[];
|
|
30
|
+
}
|
|
31
|
+
const {
|
|
32
|
+
title,
|
|
33
|
+
intro,
|
|
34
|
+
action,
|
|
35
|
+
method = 'post',
|
|
36
|
+
submitLabel = 'Send message',
|
|
37
|
+
note,
|
|
38
|
+
fields = [],
|
|
39
|
+
} = Astro.props;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<section class="b-contact-form">
|
|
43
|
+
{(title || intro) && (
|
|
44
|
+
<div class="b-contact-form__head">
|
|
45
|
+
{title && <Heading text={title} level={2} align="center" />}
|
|
46
|
+
{intro && <p class="b-contact-form__intro">{intro}</p>}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
<form class="b-contact-form__form" action={action} method={method}>
|
|
50
|
+
{fields.map((field) => {
|
|
51
|
+
const id = `cf-${field.name}`;
|
|
52
|
+
const type = field.type ?? 'text';
|
|
53
|
+
return (
|
|
54
|
+
<div class="b-contact-form__field">
|
|
55
|
+
<label class="b-contact-form__label" for={id}>
|
|
56
|
+
{field.label}
|
|
57
|
+
{field.required && <span class="b-contact-form__req" aria-hidden="true"> *</span>}
|
|
58
|
+
</label>
|
|
59
|
+
{type === 'textarea' ? (
|
|
60
|
+
<textarea id={id} name={field.name} rows="5" required={field.required} placeholder={field.placeholder}></textarea>
|
|
61
|
+
) : (
|
|
62
|
+
<input id={id} type={type} name={field.name} required={field.required} placeholder={field.placeholder} />
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
<div class="b-contact-form__action">
|
|
68
|
+
<Button type="submit" variant="cta">{submitLabel}</Button>
|
|
69
|
+
</div>
|
|
70
|
+
{note && <p class="b-contact-form__note">{note}</p>}
|
|
71
|
+
</form>
|
|
72
|
+
</section>
|
|
73
|
+
|
|
74
|
+
<style>
|
|
75
|
+
.b-contact-form {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
gap: 1.5rem;
|
|
79
|
+
max-width: 40rem;
|
|
80
|
+
margin-inline: auto;
|
|
81
|
+
padding-inline: 1rem;
|
|
82
|
+
}
|
|
83
|
+
.b-contact-form__head {
|
|
84
|
+
text-align: center;
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: 0.75rem;
|
|
88
|
+
}
|
|
89
|
+
.b-contact-form__intro {
|
|
90
|
+
margin: 0;
|
|
91
|
+
color: var(--muted);
|
|
92
|
+
line-height: 1.6;
|
|
93
|
+
}
|
|
94
|
+
.b-contact-form__form {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
gap: 1.1rem;
|
|
98
|
+
}
|
|
99
|
+
.b-contact-form__field {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: 0.4rem;
|
|
103
|
+
}
|
|
104
|
+
.b-contact-form__label {
|
|
105
|
+
font-family: var(--font-body);
|
|
106
|
+
font-size: 0.9375rem;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
color: var(--fg);
|
|
109
|
+
}
|
|
110
|
+
.b-contact-form__req {
|
|
111
|
+
color: var(--gold);
|
|
112
|
+
}
|
|
113
|
+
.b-contact-form__form input,
|
|
114
|
+
.b-contact-form__form textarea {
|
|
115
|
+
font: inherit;
|
|
116
|
+
font-size: 1rem;
|
|
117
|
+
color: var(--fg);
|
|
118
|
+
background: var(--field-bg, var(--bg-surface));
|
|
119
|
+
border: var(--field-border, 1px solid var(--border));
|
|
120
|
+
border-radius: var(--field-radius, var(--radius-sm));
|
|
121
|
+
padding: 0.7rem 0.85rem;
|
|
122
|
+
width: 100%;
|
|
123
|
+
box-sizing: border-box;
|
|
124
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Site-wide input TREATMENT — chosen once (a `data-field` attribute on <html>,
|
|
128
|
+
like the theme), applied to every field. Boxed is the default above; these
|
|
129
|
+
override the field tokens for the other two looks. */
|
|
130
|
+
:global(html[data-field='filled']) .b-contact-form__form input,
|
|
131
|
+
:global(html[data-field='filled']) .b-contact-form__form textarea {
|
|
132
|
+
background: var(--bg-section);
|
|
133
|
+
border-color: transparent;
|
|
134
|
+
}
|
|
135
|
+
:global(html[data-field='underline']) .b-contact-form__form input,
|
|
136
|
+
:global(html[data-field='underline']) .b-contact-form__form textarea {
|
|
137
|
+
background: transparent;
|
|
138
|
+
border: none;
|
|
139
|
+
border-bottom: 1.5px solid var(--border);
|
|
140
|
+
border-radius: 0;
|
|
141
|
+
padding-inline: 0;
|
|
142
|
+
}
|
|
143
|
+
:global(html[data-field='underline']) .b-contact-form__form input:focus,
|
|
144
|
+
:global(html[data-field='underline']) .b-contact-form__form textarea:focus {
|
|
145
|
+
border-bottom-color: var(--gold);
|
|
146
|
+
box-shadow: none;
|
|
147
|
+
}
|
|
148
|
+
.b-contact-form__form textarea {
|
|
149
|
+
resize: vertical;
|
|
150
|
+
min-height: 7rem;
|
|
151
|
+
}
|
|
152
|
+
.b-contact-form__form input::placeholder,
|
|
153
|
+
.b-contact-form__form textarea::placeholder {
|
|
154
|
+
color: var(--muted);
|
|
155
|
+
opacity: 0.7;
|
|
156
|
+
}
|
|
157
|
+
.b-contact-form__form input:focus,
|
|
158
|
+
.b-contact-form__form textarea:focus {
|
|
159
|
+
outline: none;
|
|
160
|
+
border-color: var(--gold);
|
|
161
|
+
box-shadow: 0 0 0 3px var(--gold-light);
|
|
162
|
+
}
|
|
163
|
+
.b-contact-form__action {
|
|
164
|
+
margin-top: 0.25rem;
|
|
165
|
+
}
|
|
166
|
+
.b-contact-form__note {
|
|
167
|
+
margin: 0;
|
|
168
|
+
font-size: 0.85rem;
|
|
169
|
+
color: var(--muted);
|
|
170
|
+
line-height: 1.5;
|
|
171
|
+
}
|
|
172
|
+
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** CTA — a Level-2 recipe: a centred call-to-action band (heading + optional
|
|
3
|
+
* text + a single button), composing the Heading + Button primitives. */
|
|
4
|
+
import Heading from '../blocks/Heading.astro';
|
|
5
|
+
import Button from '../Button.astro';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
cta: { label: string; href: string };
|
|
11
|
+
background?: 'muted' | 'accent';
|
|
12
|
+
}
|
|
13
|
+
const { title, text, cta, background = 'accent' } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<section class="b-cta" data-bg={background}>
|
|
17
|
+
<div class="b-cta__inner">
|
|
18
|
+
<Heading text={title} level={2} align="center" />
|
|
19
|
+
{text && <p class="b-cta__text">{text}</p>}
|
|
20
|
+
<div class="b-cta__action">
|
|
21
|
+
<Button href={cta.href} variant="primary">{cta.label}</Button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.b-cta {
|
|
28
|
+
padding-block: 3rem;
|
|
29
|
+
}
|
|
30
|
+
.b-cta[data-bg='muted'] {
|
|
31
|
+
background: var(--bg-section);
|
|
32
|
+
}
|
|
33
|
+
.b-cta[data-bg='accent'] {
|
|
34
|
+
background: var(--gold-light);
|
|
35
|
+
}
|
|
36
|
+
/* Mesh: a soft drifting brand gradient behind the text (static under
|
|
37
|
+
reduced-motion). Low opacity + blur keeps the copy readable. */
|
|
38
|
+
.b-cta[data-bg='mesh'] {
|
|
39
|
+
position: relative;
|
|
40
|
+
isolation: isolate;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
background: var(--bg);
|
|
43
|
+
}
|
|
44
|
+
.b-cta[data-bg='mesh']::before {
|
|
45
|
+
content: '';
|
|
46
|
+
position: absolute;
|
|
47
|
+
inset: -25%;
|
|
48
|
+
z-index: -1;
|
|
49
|
+
background:
|
|
50
|
+
radial-gradient(35% 50% at 18% 20%, var(--gold), transparent 70%),
|
|
51
|
+
radial-gradient(35% 50% at 82% 25%, var(--accent-2, var(--gold)), transparent 70%),
|
|
52
|
+
radial-gradient(45% 55% at 60% 90%, var(--gold-light), transparent 70%);
|
|
53
|
+
opacity: 0.35;
|
|
54
|
+
filter: blur(50px);
|
|
55
|
+
animation: b-cta-mesh 22s ease-in-out infinite alternate;
|
|
56
|
+
}
|
|
57
|
+
@keyframes b-cta-mesh {
|
|
58
|
+
from { transform: translate3d(-3%, -2%, 0) scale(1.06); }
|
|
59
|
+
to { transform: translate3d(3%, 4%, 0) scale(1.18); }
|
|
60
|
+
}
|
|
61
|
+
@media (prefers-reduced-motion: reduce) {
|
|
62
|
+
.b-cta[data-bg='mesh']::before { animation: none; }
|
|
63
|
+
}
|
|
64
|
+
.b-cta__inner {
|
|
65
|
+
max-width: var(--max);
|
|
66
|
+
margin-inline: auto;
|
|
67
|
+
padding-inline: 1rem;
|
|
68
|
+
text-align: center;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 1rem;
|
|
73
|
+
}
|
|
74
|
+
.b-cta__text {
|
|
75
|
+
margin: 0;
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
font-size: 1.0625rem;
|
|
78
|
+
line-height: 1.6;
|
|
79
|
+
}
|
|
80
|
+
.b-cta__action {
|
|
81
|
+
margin-top: 0.5rem;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** FAQ — a Level-2 recipe: an optional heading over a list of expandable
|
|
3
|
+
* question/answer items. Uses native <details>/<summary> — accessible and
|
|
4
|
+
* interactive with zero JavaScript. Composes the Heading primitive. */
|
|
5
|
+
import Heading from '../blocks/Heading.astro';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title?: string;
|
|
9
|
+
items?: { question: string; answer: string }[];
|
|
10
|
+
}
|
|
11
|
+
const { title, items = [] } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<section class="b-faq">
|
|
15
|
+
{title && <Heading text={title} level={2} align="center" />}
|
|
16
|
+
<div class="b-faq__list">
|
|
17
|
+
{items.map((item) => (
|
|
18
|
+
<details class="b-faq__item">
|
|
19
|
+
<summary class="b-faq__q">{item.question}</summary>
|
|
20
|
+
<div class="b-faq__a"><p>{item.answer}</p></div>
|
|
21
|
+
</details>
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.b-faq {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: 1.5rem;
|
|
31
|
+
padding-inline: 1rem;
|
|
32
|
+
}
|
|
33
|
+
.b-faq__list {
|
|
34
|
+
max-width: var(--max);
|
|
35
|
+
margin-inline: auto;
|
|
36
|
+
width: 100%;
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
gap: 0.5rem;
|
|
40
|
+
}
|
|
41
|
+
.b-faq__item {
|
|
42
|
+
border: 1px solid var(--border);
|
|
43
|
+
border-radius: 8px;
|
|
44
|
+
padding: 0.25rem 1rem;
|
|
45
|
+
background: var(--bg-surface);
|
|
46
|
+
}
|
|
47
|
+
.b-faq__q {
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
color: var(--fg);
|
|
51
|
+
padding: 0.75rem 0;
|
|
52
|
+
list-style: none;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: baseline;
|
|
55
|
+
}
|
|
56
|
+
.b-faq__q::-webkit-details-marker {
|
|
57
|
+
display: none;
|
|
58
|
+
}
|
|
59
|
+
.b-faq__q::before {
|
|
60
|
+
content: '+';
|
|
61
|
+
color: var(--gold);
|
|
62
|
+
font-weight: 700;
|
|
63
|
+
margin-right: 0.6rem;
|
|
64
|
+
}
|
|
65
|
+
.b-faq__item[open] .b-faq__q::before {
|
|
66
|
+
content: '\2013'; /* en dash */
|
|
67
|
+
}
|
|
68
|
+
.b-faq__q:focus-visible {
|
|
69
|
+
outline: 2px solid var(--gold);
|
|
70
|
+
outline-offset: 2px;
|
|
71
|
+
}
|
|
72
|
+
.b-faq__a {
|
|
73
|
+
padding: 0 0 0.9rem;
|
|
74
|
+
color: var(--muted);
|
|
75
|
+
line-height: 1.6;
|
|
76
|
+
}
|
|
77
|
+
.b-faq__a p {
|
|
78
|
+
margin: 0;
|
|
79
|
+
}
|
|
80
|
+
</style>
|