ferst-core 0.7.4 → 0.7.6

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.
@@ -1,270 +1,325 @@
1
- ---
2
- /**
3
- * Site-wide footer: site name, a one-line description, a Facebook /
4
- * Contact link row, then a legal-nav + copyright strip. Extracted from
5
- * Base.astro so changing footer links is a JSON edit, not a layout edit.
6
- *
7
- * Split masthead at 48em+ (768px, matching --bp-tablet): name/description
8
- * left, Facebook/Contact right, one row — below that it's a centred stack.
9
- * Fills the page's width deliberately on desktop instead of sitting as a
10
- * small centred island inside a full-bleed band.
11
- *
12
- * Follows the page's light/dark theme like every other component. Colour
13
- * rule throughout: accent links/labels are `--accent` in light mode and
14
- * `--gold` in dark mode, never the reverse — `--gold` measures only
15
- * ~1.75:1 against the light-mode footer background (fails WCAG AA badly),
16
- * it only reads well on the dark background. Checked with the WCAG 2.1
17
- * relative-luminance formula before writing the CSS below, not after.
18
- */
19
- import type { NavLink } from '../content/schemas';
20
-
21
- interface Props {
22
- facebookUrl: string;
23
- footerLinks: NavLink[];
24
- footerClass?: string;
25
- /** Brand / site name shown in the footer masthead. */
26
- shortName: string;
27
- /** Optional one-line tagline under the brand. Client-provided; omitted if blank. */
28
- tagline?: string;
29
- }
30
-
31
- const { facebookUrl, footerLinks, footerClass, shortName, tagline } = Astro.props;
32
- const year = new Date().getFullYear();
33
- ---
34
-
35
- <footer class:list={['site-footer', footerClass]}>
36
- <div class="footer-top">
37
- <div class="footer-row">
38
- <div class="footer-copyblock">
39
- <p class="footer-brand">{shortName}</p>
40
- {tagline && <p class="footer-blurb">{tagline}</p>}
41
- </div>
42
- <div class="footer-social">
43
- {facebookUrl && (
44
- <>
45
- <a class="footer-social__link" href={facebookUrl} target="_blank" rel="noopener noreferrer">
46
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
47
- <path d="M22 12c0-5.522-4.477-10-10-10S2 6.478 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987H7.898V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"></path>
48
- </svg>
49
- Facebook
50
- </a>
51
- <span class="footer-social__divider" aria-hidden="true"></span>
52
- </>
53
- )}
54
- <a class="footer-social__link" href="/contact/">
55
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
56
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
57
- </svg>
58
- Contact
59
- </a>
60
- </div>
61
- </div>
62
- </div>
63
-
64
- <div class="footer-bottom">
65
- <nav class="footer-nav" aria-label="Footer navigation">
66
- {footerLinks.map((link) => (
67
- <a href={link.href} set:html={link.label} />
68
- ))}
69
- <button type="button" id="footer-cookie-preferences">Cookie preferences</button>
70
- </nav>
71
- <p class="footer-copy">&copy; {year} All rights reserved.</p>
72
- </div>
73
- </footer>
74
-
75
- <script>
76
- document.getElementById('footer-cookie-preferences')?.addEventListener('click', () => {
77
- document.dispatchEvent(new CustomEvent('open-cookie-preferences'));
78
- });
79
- </script>
80
-
81
- <style is:global>
82
- /*
83
- * Every rule below is qualified under `.site-footer` on purpose, even
84
- * where a bare class would normally do. Base.astro's global stylesheet
85
- * carries `[data-theme="dark"] p { color: #C8D4C2 }` and
86
- * `[data-theme="dark"] a { color: #E8A84A }` — both specificity (0,1,1).
87
- * A bare `.footer-brand { color: ... }` is only (0,1,0) and loses that
88
- * fight in dark mode. Prefixing with `.site-footer` (0,2,0/1) wins
89
- * outright instead of depending on stylesheet order.
90
- */
91
- .site-footer {
92
- font-size: 0.85rem;
93
- background: var(--bg-tag);
94
- color: var(--fg);
95
- }
96
- [data-theme="dark"] .site-footer {
97
- background: var(--dark-bg);
98
- color: var(--text-light);
99
- }
100
- .site-footer a {
101
- text-decoration: none;
102
- }
103
- .site-footer a:focus-visible,
104
- .site-footer button:focus-visible {
105
- outline: 2px solid var(--gold);
106
- outline-offset: 2px;
107
- border-radius: 4px;
108
- }
109
-
110
- /* ── Top: title, description, social row ──
111
- Below 48em (768px): centred stack, same as before. At 768px+: a split
112
- masthead — name/description left, Facebook/Contact right — so the
113
- top section uses the page's width instead of sitting as a small
114
- centred island inside it. */
115
- .site-footer .footer-top {
116
- max-width: 72rem;
117
- margin-inline: auto;
118
- padding: clamp(1.75rem, 4vw, 2.5rem) 1.25rem clamp(1.5rem, 3vw, 2rem);
119
- }
120
- .site-footer .footer-row {
121
- display: flex;
122
- flex-direction: column;
123
- align-items: center;
124
- gap: 0.5rem;
125
- text-align: center;
126
- }
127
- .site-footer .footer-copyblock {
128
- display: flex;
129
- flex-direction: column;
130
- gap: 0.5rem;
131
- }
132
- .site-footer .footer-brand {
133
- margin: 0;
134
- font-family: var(--font-heading);
135
- font-weight: 700;
136
- font-size: 1.15rem;
137
- line-height: 1.3;
138
- color: inherit;
139
- }
140
- .site-footer .footer-blurb {
141
- margin: 0;
142
- font-size: 0.85rem;
143
- font-weight: 400;
144
- line-height: 1.5;
145
- color: var(--muted);
146
- }
147
- .site-footer .footer-social {
148
- display: flex;
149
- align-items: center;
150
- gap: 1.25rem;
151
- margin-top: 0.35rem;
152
- }
153
- @media (min-width: 48em) {
154
- .site-footer .footer-row {
155
- flex-direction: row;
156
- justify-content: space-between;
157
- text-align: left;
158
- }
159
- .site-footer .footer-social {
160
- margin-top: 0;
161
- }
162
- }
163
- .site-footer .footer-social__link {
164
- display: inline-flex;
165
- align-items: center;
166
- gap: 0.4rem;
167
- min-height: 44px;
168
- font-size: 0.92rem;
169
- font-weight: 600;
170
- color: var(--accent);
171
- }
172
- [data-theme="dark"] .site-footer .footer-social__link {
173
- color: var(--gold);
174
- }
175
- .site-footer .footer-social__divider {
176
- width: 1px;
177
- height: 1.4rem;
178
- background: var(--border);
179
- }
180
-
181
- /* ── Legal nav + copyright ── */
182
- .site-footer .footer-bottom {
183
- border-top: 1px solid var(--border);
184
- padding: 1.1rem clamp(1.25rem, 4vw, 2.25rem);
185
- max-width: 72rem;
186
- margin-inline: auto;
187
- display: flex;
188
- flex-direction: column;
189
- align-items: center;
190
- text-align: center;
191
- gap: 0.6rem 1.5rem;
192
- }
193
- /* Tight gap: each link carries its own inline padding (below), so the visible
194
- hit target is a clear region *centred on its label* — no wide dead gap between
195
- labels where a hover lands on nothing (and the browser keeps showing the
196
- previous link's URL, which reads as an off-by-one). The negative inline margin
197
- cancels the first/last item's padding so the row still edge-aligns. */
198
- .site-footer .footer-nav {
199
- display: flex;
200
- flex-wrap: wrap;
201
- justify-content: center;
202
- gap: 0.15rem 0.15rem;
203
- margin-inline: -0.55rem;
204
- }
205
- .site-footer .footer-nav a,
206
- .site-footer .footer-nav button {
207
- min-height: 44px;
208
- display: inline-flex;
209
- align-items: center;
210
- font-family: inherit;
211
- font-size: 0.68rem;
212
- font-weight: 400;
213
- letter-spacing: 0.02em;
214
- color: var(--accent);
215
- background: none;
216
- border: none;
217
- border-radius: var(--radius-sm, 6px);
218
- padding: 0 0.55rem;
219
- cursor: pointer;
220
- text-decoration: none;
221
- transition: background 0.15s ease, color 0.15s ease;
222
- }
223
- /* Visible hover/focus affordance: makes each link's boundary unmistakable, so
224
- it's always clear which one you're on. */
225
- .site-footer .footer-nav a:hover,
226
- .site-footer .footer-nav button:hover,
227
- .site-footer .footer-nav a:focus-visible,
228
- .site-footer .footer-nav button:focus-visible {
229
- background: color-mix(in srgb, var(--accent) 12%, transparent);
230
- text-decoration: underline;
231
- text-underline-offset: 3px;
232
- }
233
- [data-theme="dark"] .site-footer .footer-nav a,
234
- [data-theme="dark"] .site-footer .footer-nav button {
235
- color: var(--gold);
236
- }
237
- [data-theme="dark"] .site-footer .footer-nav a:hover,
238
- [data-theme="dark"] .site-footer .footer-nav button:hover,
239
- [data-theme="dark"] .site-footer .footer-nav a:focus-visible,
240
- [data-theme="dark"] .site-footer .footer-nav button:focus-visible {
241
- background: color-mix(in srgb, var(--gold) 16%, transparent);
242
- }
243
- .site-footer .footer-copy {
244
- margin: 0;
245
- font-family: var(--font-body);
246
- font-size: 0.68rem;
247
- font-weight: 400;
248
- color: var(--muted);
249
- }
250
-
251
- /* 48em / 768px, matching --bp-tablet in theme.css — the bottom bar's
252
- row layout (legal nav | copyright, space-between) gets cramped once
253
- the nav wraps below it, so it stacks and centres instead. */
254
- @media (min-width: 48em) {
255
- .site-footer .footer-bottom {
256
- flex-direction: row;
257
- justify-content: space-between;
258
- text-align: left;
259
- }
260
- .site-footer .footer-nav {
261
- justify-content: flex-start;
262
- }
263
- }
264
-
265
- @media (max-width: 22.5em) {
266
- .site-footer .footer-social {
267
- gap: 1rem;
268
- }
269
- }
270
- </style>
1
+ ---
2
+ /**
3
+ * Site-wide footer: site name, a one-line description, a Facebook /
4
+ * Contact link row, then a legal-nav + copyright strip. Extracted from
5
+ * Base.astro so changing footer links is a JSON edit, not a layout edit.
6
+ *
7
+ * Split masthead at 48em+ (768px, matching --bp-tablet): name/description
8
+ * left, Facebook/Contact right, one row — below that it's a centred stack.
9
+ * Fills the page's width deliberately on desktop instead of sitting as a
10
+ * small centred island inside a full-bleed band.
11
+ *
12
+ * Follows the page's light/dark theme like every other component. Colour
13
+ * rule throughout: accent links/labels are `--accent` in light mode and
14
+ * `--gold` in dark mode, never the reverse — `--gold` measures only
15
+ * ~1.75:1 against the light-mode footer background (fails WCAG AA badly),
16
+ * it only reads well on the dark background. Checked with the WCAG 2.1
17
+ * relative-luminance formula before writing the CSS below, not after.
18
+ */
19
+ import type { NavLink } from '../content/schemas';
20
+
21
+ interface Props {
22
+ facebookUrl: string;
23
+ footerLinks: NavLink[];
24
+ footerClass?: string;
25
+ /** Brand / site name shown in the footer masthead. */
26
+ shortName: string;
27
+ /** Optional one-line tagline under the brand. Client-provided; omitted if blank. */
28
+ tagline?: string;
29
+ /** The site's call-to-action (navbarSettings.cta), shown beside Facebook; null hides it. */
30
+ cta?: { label: string; href: string } | null;
31
+ /** Headed columns of reference links (policies, documents, forms). */
32
+ groups?: Array<{ heading: string; links: NavLink[] }>;
33
+ }
34
+
35
+ const { facebookUrl, footerLinks, footerClass, shortName, tagline, cta = { label: 'Contact', href: '/contact/' }, groups = [] } = Astro.props;
36
+ const year = new Date().getFullYear();
37
+ ---
38
+
39
+ <footer class:list={['site-footer', footerClass]}>
40
+ <div class="footer-top">
41
+ <div class="footer-row">
42
+ <div class="footer-copyblock">
43
+ <p class="footer-brand">{shortName}</p>
44
+ {tagline && <p class="footer-blurb">{tagline}</p>}
45
+ </div>
46
+ <div class="footer-social">
47
+ {facebookUrl && (
48
+ <>
49
+ <a class="footer-social__link" href={facebookUrl} target="_blank" rel="noopener noreferrer">
50
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
51
+ <path d="M22 12c0-5.522-4.477-10-10-10S2 6.478 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987H7.898V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"></path>
52
+ </svg>
53
+ Facebook
54
+ </a>
55
+ {cta && <span class="footer-social__divider" aria-hidden="true"></span>}
56
+ </>
57
+ )}
58
+ {cta && (
59
+ <a class="footer-social__link" href={cta.href}>
60
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
61
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
62
+ </svg>
63
+ {cta.label}
64
+ </a>
65
+ )}
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ {groups.length > 0 && (
71
+ <nav class="footer-groups" aria-label="Site information">
72
+ {groups.map((g) => (
73
+ <div class="footer-group">
74
+ <h2 class="footer-group__title">{g.heading}</h2>
75
+ <ul>
76
+ {g.links.map((l) => <li><a href={l.href}>{l.label}</a></li>)}
77
+ </ul>
78
+ </div>
79
+ ))}
80
+ </nav>
81
+ )}
82
+
83
+ <div class="footer-bottom">
84
+ <nav class="footer-nav" aria-label="Footer navigation">
85
+ {footerLinks.map((link) => (
86
+ <a href={link.href} set:html={link.label} />
87
+ ))}
88
+ <button type="button" id="footer-cookie-preferences">Cookie preferences</button>
89
+ </nav>
90
+ <p class="footer-copy">&copy; {year} All rights reserved.</p>
91
+ </div>
92
+ </footer>
93
+
94
+ <script>
95
+ document.getElementById('footer-cookie-preferences')?.addEventListener('click', () => {
96
+ document.dispatchEvent(new CustomEvent('open-cookie-preferences'));
97
+ });
98
+ </script>
99
+
100
+ <style is:global>
101
+ /*
102
+ * Every rule below is qualified under `.site-footer` on purpose, even
103
+ * where a bare class would normally do. Base.astro's global stylesheet
104
+ * carries `[data-theme="dark"] p { color: #C8D4C2 }` and
105
+ * `[data-theme="dark"] a { color: #E8A84A }` — both specificity (0,1,1).
106
+ * A bare `.footer-brand { color: ... }` is only (0,1,0) and loses that
107
+ * fight in dark mode. Prefixing with `.site-footer` (0,2,0/1) wins
108
+ * outright instead of depending on stylesheet order.
109
+ */
110
+ .site-footer {
111
+ font-size: 0.85rem;
112
+ background: var(--bg-tag);
113
+ color: var(--fg);
114
+ }
115
+ [data-theme="dark"] .site-footer {
116
+ background: var(--dark-bg);
117
+ color: var(--text-light);
118
+ }
119
+ .site-footer a {
120
+ text-decoration: none;
121
+ }
122
+ .site-footer a:focus-visible,
123
+ .site-footer button:focus-visible {
124
+ outline: 2px solid var(--gold);
125
+ outline-offset: 2px;
126
+ border-radius: 4px;
127
+ }
128
+
129
+ /* ── Top: title, description, social row ──
130
+ Below 48em (768px): centred stack, same as before. At 768px+: a split
131
+ masthead — name/description left, Facebook/Contact right — so the
132
+ top section uses the page's width instead of sitting as a small
133
+ centred island inside it. */
134
+ .site-footer .footer-top {
135
+ max-width: 72rem;
136
+ margin-inline: auto;
137
+ padding: clamp(1.75rem, 4vw, 2.5rem) 1.25rem clamp(1.5rem, 3vw, 2rem);
138
+ }
139
+ .site-footer .footer-row {
140
+ display: flex;
141
+ flex-direction: column;
142
+ align-items: center;
143
+ gap: 0.5rem;
144
+ text-align: center;
145
+ }
146
+ .site-footer .footer-copyblock {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 0.5rem;
150
+ }
151
+ .site-footer .footer-brand {
152
+ margin: 0;
153
+ font-family: var(--font-heading);
154
+ font-weight: 700;
155
+ font-size: 1.15rem;
156
+ line-height: 1.3;
157
+ color: inherit;
158
+ }
159
+ .site-footer .footer-blurb {
160
+ margin: 0;
161
+ font-size: 0.85rem;
162
+ font-weight: 400;
163
+ line-height: 1.5;
164
+ color: var(--muted);
165
+ }
166
+ .site-footer .footer-social {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 1.25rem;
170
+ margin-top: 0.35rem;
171
+ }
172
+ @media (min-width: 48em) {
173
+ .site-footer .footer-row {
174
+ flex-direction: row;
175
+ justify-content: space-between;
176
+ text-align: left;
177
+ }
178
+ .site-footer .footer-social {
179
+ margin-top: 0;
180
+ }
181
+ }
182
+ .site-footer .footer-social__link {
183
+ display: inline-flex;
184
+ align-items: center;
185
+ gap: 0.4rem;
186
+ min-height: 44px;
187
+ font-size: 0.92rem;
188
+ font-weight: 600;
189
+ color: var(--accent);
190
+ }
191
+ [data-theme="dark"] .site-footer .footer-social__link {
192
+ color: var(--gold);
193
+ }
194
+ .site-footer .footer-social__divider {
195
+ width: 1px;
196
+ height: 1.4rem;
197
+ background: var(--border);
198
+ }
199
+
200
+ /* ── Legal nav + copyright ── */
201
+ .site-footer .footer-bottom {
202
+ border-top: 1px solid var(--border);
203
+ padding: 1.1rem clamp(1.25rem, 4vw, 2.25rem);
204
+ max-width: 72rem;
205
+ margin-inline: auto;
206
+ display: flex;
207
+ flex-direction: column;
208
+ align-items: center;
209
+ text-align: center;
210
+ gap: 0.6rem 1.5rem;
211
+ }
212
+ /* Tight gap: each link carries its own inline padding (below), so the visible
213
+ hit target is a clear region *centred on its label* — no wide dead gap between
214
+ labels where a hover lands on nothing (and the browser keeps showing the
215
+ previous link's URL, which reads as an off-by-one). The negative inline margin
216
+ cancels the first/last item's padding so the row still edge-aligns. */
217
+ .site-footer .footer-nav {
218
+ display: flex;
219
+ flex-wrap: wrap;
220
+ justify-content: center;
221
+ gap: 0.15rem 0.15rem;
222
+ margin-inline: -0.55rem;
223
+ }
224
+ .site-footer .footer-nav a,
225
+ .site-footer .footer-nav button {
226
+ min-height: 44px;
227
+ display: inline-flex;
228
+ align-items: center;
229
+ font-family: inherit;
230
+ font-size: 0.68rem;
231
+ font-weight: 400;
232
+ letter-spacing: 0.02em;
233
+ color: var(--accent);
234
+ background: none;
235
+ border: none;
236
+ border-radius: var(--radius-sm, 6px);
237
+ padding: 0 0.55rem;
238
+ cursor: pointer;
239
+ text-decoration: none;
240
+ transition: background 0.15s ease, color 0.15s ease;
241
+ }
242
+ /* Visible hover/focus affordance: makes each link's boundary unmistakable, so
243
+ it's always clear which one you're on. */
244
+ .site-footer .footer-nav a:hover,
245
+ .site-footer .footer-nav button:hover,
246
+ .site-footer .footer-nav a:focus-visible,
247
+ .site-footer .footer-nav button:focus-visible {
248
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
249
+ text-decoration: underline;
250
+ text-underline-offset: 3px;
251
+ }
252
+ [data-theme="dark"] .site-footer .footer-nav a,
253
+ [data-theme="dark"] .site-footer .footer-nav button {
254
+ color: var(--gold);
255
+ }
256
+ [data-theme="dark"] .site-footer .footer-nav a:hover,
257
+ [data-theme="dark"] .site-footer .footer-nav button:hover,
258
+ [data-theme="dark"] .site-footer .footer-nav a:focus-visible,
259
+ [data-theme="dark"] .site-footer .footer-nav button:focus-visible {
260
+ background: color-mix(in srgb, var(--gold) 16%, transparent);
261
+ }
262
+ .site-footer .footer-copy {
263
+ margin: 0;
264
+ font-family: var(--font-body);
265
+ font-size: 0.68rem;
266
+ font-weight: 400;
267
+ color: var(--muted);
268
+ }
269
+
270
+ /* 48em / 768px, matching --bp-tablet in theme.css — the bottom bar's
271
+ row layout (legal nav | copyright, space-between) gets cramped once
272
+ the nav wraps below it, so it stacks and centres instead. */
273
+ @media (min-width: 48em) {
274
+ .site-footer .footer-bottom {
275
+ flex-direction: row;
276
+ justify-content: space-between;
277
+ text-align: left;
278
+ }
279
+ .site-footer .footer-nav {
280
+ justify-content: flex-start;
281
+ }
282
+ }
283
+
284
+ @media (max-width: 22.5em) {
285
+ .site-footer .footer-social {
286
+ gap: 1rem;
287
+ }
288
+ }
289
+
290
+ /* Headed columns of reference links: policies, documents, forms. */
291
+ .site-footer .footer-groups {
292
+ display: grid;
293
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
294
+ gap: 1.5rem 2rem;
295
+ max-width: 72rem;
296
+ margin: 0 auto;
297
+ padding: 1.75rem clamp(1.25rem, 4vw, 2.5rem) 0.5rem;
298
+ }
299
+ .site-footer .footer-group__title {
300
+ margin: 0 0 0.6rem;
301
+ font-family: var(--font-heading);
302
+ font-size: 0.75rem;
303
+ font-weight: 600;
304
+ letter-spacing: 0.1em;
305
+ text-transform: uppercase;
306
+ color: var(--text-footer);
307
+ opacity: 0.8;
308
+ }
309
+ .site-footer .footer-group ul {
310
+ list-style: none;
311
+ margin: 0;
312
+ padding: 0;
313
+ display: grid;
314
+ gap: 0.45rem;
315
+ }
316
+ .site-footer .footer-group a {
317
+ color: var(--text-footer-link);
318
+ text-decoration: none;
319
+ font-size: 0.9375rem;
320
+ }
321
+ .site-footer .footer-group a:hover,
322
+ .site-footer .footer-group a:focus-visible {
323
+ text-decoration: underline;
324
+ }
325
+ </style>