ferst-core 0.1.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.
@@ -0,0 +1,232 @@
1
+ ---
2
+ /**
3
+ * The site's one real cookie/privacy control, opened by the footer's
4
+ * "Cookie preferences" button (SiteFooter.astro dispatches
5
+ * `open-cookie-preferences`).
6
+ *
7
+ * There used to also be a general "essential cookies, Got it" notice
8
+ * (CookieBar.astro) shown on every page. Removed: this site sets no
9
+ * cookies of its own (local storage only, see /cookie-policy/), so that
10
+ * notice was pure FYI with no actual decision behind it — and it showed
11
+ * *alongside* this panel's real toggle when both happened to be
12
+ * unacknowledged (e.g. after a full localStorage clear), reading as two
13
+ * redundant cookie popups stacked on screen. The Google Calendar embed
14
+ * (the site's one real third-party cookie source, see
15
+ * /cookie-policy/#third-party) is the only place with an actual choice,
16
+ * so it's the only thing here.
17
+ */
18
+ ---
19
+
20
+ <div class="cookie-prefs" id="cookie-prefs" aria-label="Cookie preferences">
21
+ <div class="cookie-prefs__head">
22
+ <h2>Cookie preferences</h2>
23
+ <button type="button" class="cookie-prefs__close" id="cookie-prefs-close" aria-label="Close cookie preferences">
24
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
25
+ <path d="M18 6 6 18M6 6l12 12"></path>
26
+ </svg>
27
+ </button>
28
+ </div>
29
+
30
+ <div class="cookie-prefs__row">
31
+ <div class="cookie-prefs__row-text">
32
+ <p class="cookie-prefs__row-title">Google Calendar</p>
33
+ <p class="cookie-prefs__row-desc">Loads the embedded calendar on the <a href="/calendar/">Calendar</a> page. Google may set its own cookies once it's loaded.</p>
34
+ </div>
35
+ <button
36
+ type="button"
37
+ class="cookie-prefs__switch"
38
+ id="cookie-prefs-calendar-toggle"
39
+ role="switch"
40
+ aria-checked="false"
41
+ aria-label="Allow Google Calendar embed"
42
+ >
43
+ <span class="cookie-prefs__switch-thumb"></span>
44
+ </button>
45
+ </div>
46
+
47
+ <div class="cookie-prefs__foot">
48
+ <a href="/cookie-policy/" class="cookie-prefs__link">Cookie policy</a>
49
+ </div>
50
+ </div>
51
+
52
+ <style is:global>
53
+ .cookie-prefs {
54
+ position: fixed;
55
+ left: 20px;
56
+ bottom: 20px;
57
+ z-index: 200;
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 0.9rem;
61
+ width: min(400px, calc(100vw - 40px));
62
+ padding: 1.1rem 1.2rem;
63
+ background: var(--bg-surface);
64
+ border: 1px solid var(--border);
65
+ border-radius: 14px;
66
+ box-shadow: 0 16px 40px rgba(0,0,0,0.18);
67
+ transform: translateY(12px);
68
+ opacity: 0;
69
+ visibility: hidden;
70
+ transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s ease, visibility 0.4s;
71
+ }
72
+ .cookie-prefs.visible {
73
+ transform: translateY(0);
74
+ opacity: 1;
75
+ visibility: visible;
76
+ }
77
+ @media (prefers-reduced-motion: reduce) {
78
+ .cookie-prefs { transition: opacity 0.2s ease, visibility 0.2s; transform: none; }
79
+ }
80
+
81
+ .cookie-prefs__head {
82
+ display: flex;
83
+ align-items: flex-start;
84
+ justify-content: space-between;
85
+ gap: 0.75rem;
86
+ }
87
+ .cookie-prefs__head h2 {
88
+ margin: 0;
89
+ font-family: 'Poppins', sans-serif;
90
+ font-size: 0.92rem;
91
+ font-weight: 600;
92
+ color: var(--fg);
93
+ }
94
+ .cookie-prefs__close {
95
+ flex-shrink: 0;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ width: 26px;
100
+ height: 26px;
101
+ padding: 0;
102
+ background: none;
103
+ border: none;
104
+ border-radius: 6px;
105
+ color: var(--muted);
106
+ cursor: pointer;
107
+ transition: background 0.15s ease, color 0.15s ease;
108
+ }
109
+ .cookie-prefs__close:hover { background: var(--bg-section); color: var(--fg); }
110
+ .cookie-prefs__close:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
111
+
112
+ .cookie-prefs__row {
113
+ display: flex;
114
+ align-items: flex-start;
115
+ gap: 0.9rem;
116
+ padding-top: 0.9rem;
117
+ border-top: 1px solid var(--border);
118
+ }
119
+ .cookie-prefs__row-text { min-width: 0; }
120
+ .cookie-prefs__row-title {
121
+ margin: 0 0 0.2rem;
122
+ font-family: 'Poppins', sans-serif;
123
+ font-size: 0.82rem;
124
+ font-weight: 600;
125
+ color: var(--fg);
126
+ }
127
+ .cookie-prefs__row-desc {
128
+ margin: 0;
129
+ font-family: 'Poppins', sans-serif;
130
+ font-size: 0.76rem;
131
+ line-height: 1.5;
132
+ color: var(--muted);
133
+ }
134
+ .cookie-prefs__row-desc a {
135
+ color: var(--muted);
136
+ text-decoration: underline;
137
+ text-underline-offset: 2px;
138
+ }
139
+ .cookie-prefs__row-desc a:hover { color: var(--fg); }
140
+
141
+ .cookie-prefs__switch {
142
+ flex-shrink: 0;
143
+ position: relative;
144
+ width: 40px;
145
+ height: 24px;
146
+ margin-top: 0.1rem;
147
+ padding: 2px;
148
+ background: var(--border);
149
+ border: none;
150
+ border-radius: 999px;
151
+ cursor: pointer;
152
+ transition: background 0.18s ease;
153
+ touch-action: manipulation;
154
+ }
155
+ .cookie-prefs__switch-thumb {
156
+ display: block;
157
+ width: 20px;
158
+ height: 20px;
159
+ border-radius: 50%;
160
+ background: var(--bg-surface);
161
+ box-shadow: 0 1px 2px rgba(0,0,0,0.25);
162
+ transition: transform 0.18s ease;
163
+ }
164
+ .cookie-prefs__switch[aria-checked="true"] { background: var(--gold); }
165
+ .cookie-prefs__switch[aria-checked="true"] .cookie-prefs__switch-thumb { transform: translateX(16px); }
166
+ .cookie-prefs__switch:focus-visible {
167
+ outline: 2px solid var(--fg);
168
+ outline-offset: 2px;
169
+ }
170
+
171
+ .cookie-prefs__foot {
172
+ padding-top: 0.9rem;
173
+ border-top: 1px solid var(--border);
174
+ }
175
+ .cookie-prefs__link {
176
+ font-family: 'Poppins', sans-serif;
177
+ font-size: 0.78rem;
178
+ color: var(--muted);
179
+ text-decoration: underline;
180
+ text-underline-offset: 2px;
181
+ }
182
+ .cookie-prefs__link:hover { color: var(--fg); }
183
+ .cookie-prefs__link:focus-visible {
184
+ outline: 2px solid var(--fg);
185
+ outline-offset: 2px;
186
+ }
187
+
188
+ @media (max-width: 47.9375em) {
189
+ .cookie-prefs {
190
+ left: 12px;
191
+ right: 12px;
192
+ bottom: 12px;
193
+ width: auto;
194
+ padding: 1rem;
195
+ }
196
+ }
197
+ </style>
198
+
199
+ <script>
200
+ const CALENDAR_KEY = 'calendar-consent';
201
+
202
+ const panel = document.getElementById('cookie-prefs') as HTMLElement;
203
+ const closeBtn = document.getElementById('cookie-prefs-close') as HTMLButtonElement;
204
+ const calendarToggle = document.getElementById('cookie-prefs-calendar-toggle') as HTMLButtonElement;
205
+
206
+ function syncToggle() {
207
+ const allowed = localStorage.getItem(CALENDAR_KEY) === '1';
208
+ calendarToggle.setAttribute('aria-checked', String(allowed));
209
+ }
210
+
211
+ function open() {
212
+ syncToggle();
213
+ panel.classList.add('visible');
214
+ }
215
+
216
+ function close() {
217
+ panel.classList.remove('visible');
218
+ }
219
+
220
+ calendarToggle.addEventListener('click', () => {
221
+ const next = calendarToggle.getAttribute('aria-checked') !== 'true';
222
+ calendarToggle.setAttribute('aria-checked', String(next));
223
+ localStorage.setItem(CALENDAR_KEY, next ? '1' : '0');
224
+ document.dispatchEvent(new CustomEvent('calendar-consent-changed', { detail: { allowed: next } }));
225
+ });
226
+
227
+ closeBtn.addEventListener('click', close);
228
+ document.addEventListener('keydown', (e) => {
229
+ if (e.key === 'Escape' && panel.classList.contains('visible')) close();
230
+ });
231
+ document.addEventListener('open-cookie-preferences', open);
232
+ </script>
@@ -0,0 +1,292 @@
1
+ ---
2
+ /**
3
+ * Shared chrome for the site's legal/policy pages (e.g. Privacy, Terms &
4
+ * Conditions, Accessibility): eyebrow +
5
+ * title + optional dek/"updated" badge, an optional highlighted callout,
6
+ * and — once a page has more than one `sections` entry — a sticky "On
7
+ * this page" rail with scroll-spy, matching the anchor ids of the
8
+ * <section> elements the page itself supplies via the default slot.
9
+ *
10
+ * Deliberately data-driven (an explicit `sections` prop) rather than
11
+ * auto-walking the slotted h2s at runtime: only 4 pages use this, their
12
+ * structure rarely changes, and a static id/label pair is simpler and more
13
+ * robust than id-slugification.
14
+ */
15
+ import Base from '../layouts/Base.astro';
16
+
17
+ interface Props {
18
+ title: string;
19
+ eyebrow?: string;
20
+ dek?: string;
21
+ updated?: string;
22
+ noteLabel?: string;
23
+ noteText?: string;
24
+ sections?: { id: string; label: string }[];
25
+ }
26
+
27
+ const {
28
+ title,
29
+ eyebrow = 'Legal & Policies',
30
+ dek,
31
+ updated,
32
+ noteLabel,
33
+ noteText,
34
+ sections = [],
35
+ } = Astro.props;
36
+
37
+ const showToc = sections.length > 1;
38
+ ---
39
+
40
+ <Base title={title} description={dek} showSecondaryLogo>
41
+ <div class="doc-page" id="top">
42
+ <div class="doc-head">
43
+ <h4>{eyebrow}</h4>
44
+ <h1>{title}</h1>
45
+ {dek && <p class="t-lead doc-dek">{dek}</p>}
46
+ {updated && <span class="doc-updated">Updated {updated}</span>}
47
+ </div>
48
+
49
+ <div class:list={['doc-grid', { 'doc-grid--no-toc': !showToc }]}>
50
+ {showToc && (
51
+ <nav class="toc-rail" aria-label="On this page">
52
+ <p class="toc-label">On this page</p>
53
+ <ul class="toc-list">
54
+ {sections.map((s) => (
55
+ <li><a href={`#${s.id}`} data-toc-link={s.id}>{s.label}</a></li>
56
+ ))}
57
+ </ul>
58
+ </nav>
59
+ )}
60
+
61
+ <div class="doc-body">
62
+ <slot name="lead" />
63
+ {(noteLabel || noteText) && (
64
+ <div class="key-point">
65
+ <span class="key-point__icon" aria-hidden="true">
66
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M2 12h20"></path></svg>
67
+ </span>
68
+ <div>
69
+ {noteLabel && <p class="key-point__label">{noteLabel}</p>}
70
+ {noteText && <p class="key-point__text">{noteText}</p>}
71
+ </div>
72
+ </div>
73
+ )}
74
+ <slot />
75
+ {showToc && (
76
+ <a class="back-to-top" href="#top">
77
+ <span class="back-to-top__arrow" aria-hidden="true">&uarr;</span> Back to top
78
+ </a>
79
+ )}
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </Base>
84
+
85
+ <style>
86
+ .doc-page { display: flex; flex-direction: column; }
87
+
88
+ .doc-head { max-width: 46rem; }
89
+ .doc-head h1 { text-align: left; }
90
+ .doc-dek { color: var(--muted); max-width: 56ch; margin: 0.5rem 0 1rem; }
91
+
92
+ .doc-updated {
93
+ display: inline-flex;
94
+ align-items: center;
95
+ font-family: 'Poppins', sans-serif;
96
+ font-size: 0.65rem;
97
+ font-weight: 600;
98
+ text-transform: uppercase;
99
+ letter-spacing: 0.08em;
100
+ color: var(--brown);
101
+ background: rgba(180, 145, 70, 0.22);
102
+ border-radius: 4px;
103
+ padding: 0.2rem 0.5rem;
104
+ line-height: 1.2;
105
+ }
106
+ :global(html[data-theme='dark']) .doc-updated {
107
+ color: var(--text-light);
108
+ background: rgba(255, 255, 255, 0.12);
109
+ }
110
+
111
+ .key-point {
112
+ display: flex;
113
+ gap: 0.85rem;
114
+ padding: 1rem 1.15rem;
115
+ background: rgba(200, 151, 58, 0.1);
116
+ border: 1px solid rgba(200, 151, 58, 0.35);
117
+ border-radius: 10px;
118
+ }
119
+ :global(html[data-theme='dark']) .key-point {
120
+ background: rgba(255, 255, 255, 0.045);
121
+ border-color: rgba(232, 168, 74, 0.35);
122
+ }
123
+ .key-point__icon {
124
+ flex-shrink: 0;
125
+ width: 28px;
126
+ height: 28px;
127
+ border-radius: 8px;
128
+ background: var(--gold);
129
+ color: var(--bg-surface);
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ }
134
+ .key-point__label {
135
+ font-family: 'Poppins', sans-serif;
136
+ font-size: 0.7rem;
137
+ font-weight: 700;
138
+ text-transform: uppercase;
139
+ letter-spacing: 0.08em;
140
+ color: var(--brown);
141
+ margin: 0 0 0.25rem;
142
+ }
143
+ :global(html[data-theme='dark']) .key-point__label { color: var(--text-light); }
144
+ .key-point__text { margin: 0; }
145
+
146
+ .doc-grid {
147
+ display: grid;
148
+ grid-template-columns: 1fr;
149
+ gap: 2.25rem;
150
+ align-items: start;
151
+ }
152
+ @media (min-width: 62em) {
153
+ .doc-grid:not(.doc-grid--no-toc) {
154
+ grid-template-columns: 200px minmax(0, 46rem);
155
+ }
156
+ }
157
+
158
+ .toc-rail { position: static; }
159
+ @media (min-width: 62em) {
160
+ .toc-rail { position: sticky; top: 5.5rem; }
161
+ }
162
+ .toc-label {
163
+ font-family: 'Poppins', sans-serif;
164
+ font-size: 0.68rem;
165
+ font-weight: 700;
166
+ text-transform: uppercase;
167
+ letter-spacing: 0.1em;
168
+ color: var(--text-subtle);
169
+ margin: 0 0 0.7rem;
170
+ }
171
+ .toc-list {
172
+ list-style: none;
173
+ margin: 0;
174
+ padding: 0;
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 0.15rem;
178
+ border-left: 2px solid var(--border);
179
+ }
180
+ .toc-list a {
181
+ display: block;
182
+ padding: 0.4rem 0 0.4rem 0.85rem;
183
+ margin-left: -2px;
184
+ border-left: 2px solid transparent;
185
+ font-size: 0.85rem;
186
+ font-weight: 500;
187
+ color: var(--muted);
188
+ text-decoration: none;
189
+ }
190
+ .toc-list a:hover { color: var(--brown); }
191
+ :global(html[data-theme='dark']) .toc-list a:hover { color: var(--gold); }
192
+ .toc-list a.is-active {
193
+ color: var(--brown);
194
+ border-left-color: var(--gold);
195
+ font-weight: 700;
196
+ }
197
+ :global(html[data-theme='dark']) .toc-list a.is-active { color: var(--gold); }
198
+
199
+ /* Layout does the spacing here (matches .post-detail's technique in
200
+ posts/[...slug].astro) rather than per-element margins, since the
201
+ lead slot, the key-point box, and each <section> are siblings of
202
+ varying content whose own p/li margins are zeroed globally by
203
+ Base.astro — margin-based spacing between them silently collapsed
204
+ to nothing without this. */
205
+ .doc-body {
206
+ min-width: 0;
207
+ display: flex;
208
+ flex-direction: column;
209
+ gap: clamp(1.1rem, 2vw, 1.5rem);
210
+ }
211
+ .doc-body :global(section) {
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: clamp(0.85rem, 1.5vw, 1.1rem);
215
+ scroll-margin-top: 5rem;
216
+ }
217
+ .doc-body :global(section + section) {
218
+ margin-top: 1.1rem;
219
+ padding-top: 2.25rem;
220
+ border-top: 1px solid var(--border);
221
+ }
222
+ .doc-body :global(h2) { margin: 0; text-align: left; }
223
+ .doc-body :global(ol),
224
+ .doc-body :global(ul) { margin: 0; padding-left: 1.25rem; }
225
+ .doc-body :global(ol li),
226
+ .doc-body :global(ul li) { margin-bottom: 0.75rem; }
227
+ .doc-body :global(ol li:last-child),
228
+ .doc-body :global(ul li:last-child) { margin-bottom: 0; }
229
+ .doc-body :global(li::marker) { color: var(--gold); font-weight: 700; }
230
+ .doc-body :global(p) { margin: 0; }
231
+ .doc-body :global(.notice-strap) {
232
+ font-weight: 700;
233
+ color: var(--accent);
234
+ background: var(--gold-light);
235
+ border-left: 3px solid var(--gold);
236
+ padding: 0.7rem 0.9rem;
237
+ border-radius: 0 6px 6px 0;
238
+ font-size: 0.92rem;
239
+ }
240
+ :global(html[data-theme='dark']) .doc-body :global(.notice-strap) { color: var(--text-light); }
241
+
242
+ .back-to-top {
243
+ display: inline-flex;
244
+ align-items: center;
245
+ gap: 0.35rem;
246
+ margin-top: 1.25rem;
247
+ font-family: 'Poppins', sans-serif;
248
+ font-size: 0.78rem;
249
+ font-weight: 700;
250
+ color: var(--brown);
251
+ text-transform: uppercase;
252
+ letter-spacing: 0.06em;
253
+ transition: color 0.2s ease;
254
+ }
255
+ .back-to-top:hover { color: var(--gold); }
256
+ :global(html[data-theme='dark']) .back-to-top { color: var(--text-light); }
257
+ :global(html[data-theme='dark']) .back-to-top:hover { color: var(--gold); }
258
+ .back-to-top__arrow {
259
+ display: inline-block;
260
+ transition: transform 0.2s ease;
261
+ }
262
+ .back-to-top:hover .back-to-top__arrow { transform: translateY(-3px); }
263
+ @media (prefers-reduced-motion: reduce) {
264
+ .back-to-top__arrow { transition: none; }
265
+ .back-to-top:hover .back-to-top__arrow { transform: none; }
266
+ }
267
+ </style>
268
+
269
+ {showToc && (
270
+ <script>
271
+ const links = document.querySelectorAll<HTMLAnchorElement>('[data-toc-link]');
272
+ if (links.length) {
273
+ const map = new Map<string, HTMLAnchorElement>();
274
+ links.forEach((l) => {
275
+ const key = l.dataset.tocLink;
276
+ if (key) map.set(key, l);
277
+ });
278
+ const observer = new IntersectionObserver(
279
+ (entries) => {
280
+ entries.forEach((entry) => {
281
+ const link = map.get(entry.target.id);
282
+ if (!link || !entry.isIntersecting) return;
283
+ links.forEach((l) => l.classList.remove('is-active'));
284
+ link.classList.add('is-active');
285
+ });
286
+ },
287
+ { rootMargin: '-15% 0px -70% 0px' }
288
+ );
289
+ document.querySelectorAll('.doc-body section[id]').forEach((s) => observer.observe(s));
290
+ }
291
+ </script>
292
+ )}
@@ -0,0 +1,164 @@
1
+ ---
2
+ import Card from './Card.astro';
3
+ import { normalizeMailtoHref } from '../lib/mailLinks';
4
+
5
+ interface Props {
6
+ title: string;
7
+ description: string;
8
+ icon?: string;
9
+ contact?: string;
10
+ contactLabel?: string;
11
+ linkHref?: string;
12
+ imageSrc?: string;
13
+ imageAlt?: string;
14
+ }
15
+
16
+ const { title, description, contact, contactLabel, linkHref, imageSrc, imageAlt } = Astro.props;
17
+ const trimmedLink = linkHref?.trim();
18
+ const trimmedContact = contact?.trim();
19
+
20
+ function resolvedGroupHref(): string | undefined {
21
+ if (trimmedLink) {
22
+ if (/^https?:\/\//i.test(trimmedLink)) return trimmedLink;
23
+ if (trimmedLink.startsWith('/') || trimmedLink.startsWith('#')) return trimmedLink;
24
+ const mail = normalizeMailtoHref(trimmedLink);
25
+ return mail || undefined;
26
+ }
27
+ if (trimmedContact) {
28
+ const mail = normalizeMailtoHref(trimmedContact);
29
+ return mail || undefined;
30
+ }
31
+ }
32
+
33
+ const resolvedHref = resolvedGroupHref();
34
+ const isExternal = resolvedHref ? /^https?:\/\//.test(resolvedHref) : false;
35
+ const isEmail = resolvedHref ? /^mailto:/.test(resolvedHref) : false;
36
+ ---
37
+
38
+ <Card as="article" padding="0" class="group-card">
39
+ {imageSrc ? (
40
+ <img
41
+ src={imageSrc}
42
+ alt={imageAlt || title}
43
+ class="group-card__img"
44
+ loading="lazy"
45
+ decoding="async"
46
+ />
47
+ ) : (
48
+ <div class="group-card__placeholder" aria-hidden="true">
49
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" width="48" height="48" aria-hidden="true">
50
+ <line x1="12" y1="3" x2="12" y2="21"/>
51
+ <line x1="5" y1="8" x2="19" y2="8"/>
52
+ </svg>
53
+ </div>
54
+ )}
55
+ <div class="group-card__content">
56
+ <h3>{title}</h3>
57
+ <p>{description}</p>
58
+ {resolvedHref && (
59
+ <div>
60
+ <a
61
+ href={resolvedHref}
62
+ target={isExternal ? '_blank' : undefined}
63
+ rel={isExternal ? 'noopener noreferrer' : undefined}
64
+ >
65
+ {isEmail && (
66
+ <svg
67
+ class="group-card__link-icon"
68
+ width="14"
69
+ height="14"
70
+ viewBox="0 0 24 24"
71
+ fill="none"
72
+ stroke="currentColor"
73
+ stroke-width="2"
74
+ stroke-linecap="round"
75
+ stroke-linejoin="round"
76
+ aria-hidden="true"
77
+ >
78
+ <rect x="2" y="4" width="20" height="16" rx="2"></rect>
79
+ <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
80
+ </svg>
81
+ )}
82
+ {isExternal && (
83
+ <svg
84
+ class="group-card__link-icon"
85
+ width="14"
86
+ height="14"
87
+ viewBox="0 0 24 24"
88
+ fill="none"
89
+ stroke="currentColor"
90
+ stroke-width="2"
91
+ stroke-linecap="round"
92
+ stroke-linejoin="round"
93
+ aria-hidden="true"
94
+ >
95
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
96
+ <polyline points="15 3 21 3 21 9"></polyline>
97
+ <line x1="10" y1="14" x2="21" y2="3"></line>
98
+ </svg>
99
+ )}
100
+ {contactLabel || contact}
101
+ </a>
102
+ </div>
103
+ )}
104
+ </div>
105
+ </Card>
106
+
107
+ <style>
108
+ article {
109
+ margin: 0;
110
+ height: 100%;
111
+ }
112
+
113
+ .group-card__content {
114
+ display: flex;
115
+ flex-direction: column;
116
+ flex: 1;
117
+ gap: 0.75rem;
118
+ padding: 1rem 1.25rem;
119
+ }
120
+
121
+ .group-card__content h3 {
122
+ margin-block: clamp(0.75rem, 2vw, 1.25rem);
123
+ }
124
+
125
+ .group-card__content > div {
126
+ margin-top: auto;
127
+ display: flex;
128
+ justify-content: flex-end;
129
+ align-items: flex-end;
130
+ }
131
+
132
+ .group-card__content a {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: 0.375rem;
136
+ }
137
+
138
+ .group-card__content a:hover {
139
+ color: #755528;
140
+ }
141
+
142
+ :global([data-theme='dark']) .group-card__content a:hover {
143
+ color: #C8D4C2;
144
+ }
145
+
146
+ img {
147
+ display: block;
148
+ width: 100%;
149
+ aspect-ratio: 16 / 9;
150
+ object-fit: cover;
151
+ object-position: center;
152
+ margin: 0;
153
+ }
154
+
155
+ .group-card__placeholder {
156
+ width: 100%;
157
+ aspect-ratio: 16 / 9;
158
+ background: linear-gradient(135deg, #4A6741 0%, #2C3B2A 100%);
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ color: rgba(255, 255, 255, 0.55);
163
+ }
164
+ </style>