ferst-core 0.1.0 → 0.2.1

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.
Files changed (70) hide show
  1. package/LICENSE +102 -201
  2. package/LICENSE-Apache-2.0 +201 -0
  3. package/NOTICE +24 -5
  4. package/README.md +89 -73
  5. package/bin/check-thin.mjs +74 -0
  6. package/components/BlockRenderer.astro +89 -0
  7. package/components/Button.astro +85 -33
  8. package/components/CalendarEmbed.astro +188 -0
  9. package/components/Card.astro +7 -17
  10. package/components/Icon.astro +35 -0
  11. package/components/PostArticle.astro +133 -0
  12. package/components/PostCard.astro +93 -225
  13. package/components/SiteFooter.astro +2 -2
  14. package/components/SiteHeader.astro +104 -51
  15. package/components/blocks/Badge.astro +29 -0
  16. package/components/blocks/ButtonBlock.astro +13 -0
  17. package/components/blocks/Divider.astro +12 -0
  18. package/components/blocks/Grid.astro +48 -0
  19. package/components/blocks/Heading.astro +42 -0
  20. package/components/blocks/ImageBlock.astro +80 -0
  21. package/components/blocks/List.astro +78 -0
  22. package/components/blocks/Prose.astro +29 -0
  23. package/components/blocks/Quote.astro +32 -0
  24. package/components/blocks/Section.astro +52 -0
  25. package/components/blocks/Spacer.astro +20 -0
  26. package/components/blocks/Stack.astro +30 -0
  27. package/components/blocks/Stat.astro +30 -0
  28. package/components/recipes/Accordion.astro +84 -0
  29. package/components/recipes/Announcement.astro +74 -0
  30. package/components/recipes/Banner.astro +134 -0
  31. package/components/recipes/Bento.astro +138 -0
  32. package/components/recipes/ContactForm.astro +172 -0
  33. package/components/recipes/Cta.astro +83 -0
  34. package/components/recipes/Faq.astro +80 -0
  35. package/components/recipes/FeatureCards.astro +105 -0
  36. package/components/recipes/Gallery.astro +55 -0
  37. package/components/recipes/Hero.astro +191 -0
  38. package/components/recipes/Logos.astro +77 -0
  39. package/components/recipes/Marquee.astro +75 -0
  40. package/components/recipes/Pricing.astro +149 -0
  41. package/components/recipes/Stats.astro +124 -0
  42. package/components/recipes/Steps.astro +120 -0
  43. package/components/recipes/Tabs.astro +133 -0
  44. package/components/recipes/Testimonial.astro +66 -0
  45. package/components/recipes/Tiles.astro +248 -0
  46. package/content/blocks.ts +534 -0
  47. package/content/calendar.ts +40 -0
  48. package/content/collections.ts +25 -14
  49. package/content/posts.ts +89 -0
  50. package/content/schemas.ts +58 -123
  51. package/layouts/Base.astro +30 -56
  52. package/lib/calendar.ts +12 -0
  53. package/lib/icons.ts +64 -0
  54. package/lib/pages.ts +42 -0
  55. package/lib/posts.ts +93 -0
  56. package/lib/siteSettings.ts +18 -36
  57. package/lib/themeTokens.ts +163 -0
  58. package/package.json +61 -48
  59. package/styles/theme.css +42 -0
  60. package/components/DocLayout.astro +0 -292
  61. package/components/GroupCard.astro +0 -164
  62. package/components/LatestPostList.astro +0 -64
  63. package/components/PaginationNav.astro +0 -81
  64. package/components/PostsToolbar.astro +0 -83
  65. package/content/filters.ts +0 -107
  66. package/lib/mailLinks.ts +0 -13
  67. package/lib/mapsLink.ts +0 -7
  68. package/utils/excerpt.ts +0 -56
  69. package/utils/formatDate.ts +0 -22
  70. package/utils/freshness.ts +0 -5
package/README.md CHANGED
@@ -1,73 +1,89 @@
1
- # ferst-core
2
-
3
- The shared, brand-agnostic client-site system behind [Ferst](https://ferst.co.uk).
4
- It ships the Astro components, layouts, content schemas, the layered config
5
- resolver, and a neutral styling architecture that every client site is built
6
- on. Update the core → every client benefits; brand and bespoke changes live in
7
- the client's own repo.
8
-
9
- This is the open, source-available **core**. It is deliberately generic — there
10
- are no verticals baked in (a "parish" or a "school" site is a *preset* of
11
- generic capabilities and content, never core code). Premium capability modules
12
- and the hosting platform live elsewhere.
13
-
14
- ## Install
15
-
16
- ```sh
17
- npm install ferst-core astro
18
- ```
19
-
20
- `astro` is a peer dependency (Astro 5+). The package ships raw `.astro` / `.ts`
21
- source and is compiled by your own Astro build.
22
-
23
- ## Usage
24
-
25
- A client site holds only its content, brand, and config. The core owns the
26
- schemas, the resolver, and the chrome.
27
-
28
- **1. Re-export the content collections** (Astro requires the config to live in
29
- the consumer — this one line is the whole contract):
30
-
31
- ```ts
32
- // src/content/config.ts
33
- export { collections } from 'ferst-core/content/collections';
34
- ```
35
-
36
- **2. Render pages through the core layout:**
37
-
38
- ```astro
39
- ---
40
- import Base from 'ferst-core/layouts/Base.astro';
41
- import Button from 'ferst-core/components/Button.astro';
42
- ---
43
- <Base title="Home">
44
- <h1>Welcome</h1>
45
- <Button href="/contact/">Get in touch</Button>
46
- </Base>
47
- ```
48
-
49
- `<Base>` loads the site's settings (identity, navigation, logos, calendar,
50
- footer) from the consumer's content collections and composes the header,
51
- footer, and cookie controls automatically.
52
-
53
- ## Brand colours are yours
54
-
55
- The core ships a neutral slate + indigo default palette. Override any design
56
- token per-site via the `themeSettings` collection — no code change, and the
57
- colours live in your repo (or, later, the CMS):
58
-
59
- ```json
60
- // src/content/themeSettings/index.json
61
- {
62
- "light": { "--gold": "#EA580C", "--gold-light": "#FFEDD5" },
63
- "dark": { "--gold": "#FB923C", "--gold-light": "#431407" }
64
- }
65
- ```
66
-
67
- `light` applies at `:root`, `dark` under `[data-theme="dark"]`; only the tokens
68
- you set are overridden, the rest fall through to the neutral defaults.
69
-
70
- ## Licence
71
-
72
- [Apache-2.0](./LICENSE). Please retain the [NOTICE](./NOTICE) attribution when
73
- redistributing. © 2026 MyAI4 Ltd.
1
+ # ferst-core
2
+
3
+ The shared, brand-agnostic client-site system behind [Ferst](https://ferst.co.uk).
4
+ It ships the Astro components, layouts, content schemas, the layered config
5
+ resolver, and a neutral styling architecture that every client site is built
6
+ on. Update the core → every client benefits; brand and bespoke changes live in
7
+ the client's own repo.
8
+
9
+ This is the open, source-available **core**. It is deliberately generic — there
10
+ are no verticals baked in (a "parish" or a "school" site is a *preset* of
11
+ generic capabilities and content, never core code). Premium capability modules
12
+ and the hosting platform live elsewhere.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm install ferst-core astro
18
+ ```
19
+
20
+ `astro` is a peer dependency (Astro 5+). The package ships raw `.astro` / `.ts`
21
+ source and is compiled by your own Astro build.
22
+
23
+ ## Usage
24
+
25
+ A client site holds only its content, brand, and config. The core owns the
26
+ schemas, the resolver, and the chrome.
27
+
28
+ **1. Re-export the content collections** (Astro requires the config to live in
29
+ the consumer — this one line is the whole contract):
30
+
31
+ ```ts
32
+ // src/content/config.ts
33
+ export { collections } from 'ferst-core/content/collections';
34
+ ```
35
+
36
+ **2. Render pages through the core layout:**
37
+
38
+ ```astro
39
+ ---
40
+ import Base from 'ferst-core/layouts/Base.astro';
41
+ import Button from 'ferst-core/components/Button.astro';
42
+ ---
43
+ <Base title="Home">
44
+ <h1>Welcome</h1>
45
+ <Button href="/contact/">Get in touch</Button>
46
+ </Base>
47
+ ```
48
+
49
+ `<Base>` loads the site's settings (identity, navigation, logos, calendar,
50
+ footer) from the consumer's content collections and composes the header,
51
+ footer, and cookie controls automatically.
52
+
53
+ ## Brand colours are yours
54
+
55
+ The core ships a neutral slate + indigo default palette. Design your whole brand
56
+ from **three colours** in the `themeSettings` collection — the core derives the
57
+ ~40 working tokens for you (light via `color-mix`, dark via an OKLCH translation).
58
+ No code change; the colours live in your repo, edited by hand or in the CMS:
59
+
60
+ ```json
61
+ // src/content/themeSettings/index.json
62
+ {
63
+ "brand": "#0E4D4A",
64
+ "ink": "#10201F",
65
+ "surface": "#FBFDFC",
66
+ "fonts": { "heading": "Poppins, sans-serif", "body": "Inter, sans-serif" },
67
+ "corners": "rounded",
68
+ "elevation": "raised",
69
+ "stroke": "on",
70
+ "fields": "boxed"
71
+ }
72
+ ```
73
+
74
+ `brand` drives buttons/links/accents, `ink` text, `surface` backgrounds (cards,
75
+ sections and borders are tinted from it). The four knobs — `corners` /
76
+ `elevation` / `stroke` / `fields` — are set once and apply to every card, tile
77
+ and input. Set no brand and the neutral defaults stand. For the rare token that
78
+ must differ from what the three colours derive, `advanced.light` / `advanced.dark`
79
+ are raw `{ "--token": "value" }` escape hatches layered last.
80
+
81
+ ## Licence
82
+
83
+ [Business Source License 1.1](./LICENSE) from v0.2.0 (v0.1.x was Apache-2.0 and stays
84
+ that way). **You may use this in production** to build, operate and maintain individual
85
+ websites -- your own, or ones you build for a client who controls the site, and any
86
+ developer may take that site over. You may not use it to run a multi-tenant or hosted
87
+ website service for third parties. Each version converts automatically to
88
+ [Apache-2.0](./LICENSE-Apache-2.0) four years after publication. Please retain the
89
+ [NOTICE](./NOTICE) attribution when redistributing.
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ // Ferst single-source guard (manifest §2): a client repo DEPENDS on the core —
3
+ // it never contains a COPY. This fails the build if a client's src/ shadows a
4
+ // core-owned module. Shared code (components/layouts/lib/utils/styles + the
5
+ // content schema/collections/filters) comes from `ferst-core`; the client keeps
6
+ // only content DATA, pages, the content config re-export, and a `custom/`
7
+ // override sandbox. Shipped from core (its own single source) and run in each
8
+ // client's CI/build: `ferst-check-thin [srcDir]` (defaults to ./src).
9
+ import { readdirSync, statSync, existsSync } from 'node:fs';
10
+ import { join, relative } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+
13
+ // Directories directly under src/ that belong to the core, not the client.
14
+ const CORE_OWNED_DIRS = ['components', 'layouts', 'lib', 'utils', 'styles'];
15
+ // Files under src/content/ that are the core's contract, not client data.
16
+ // (Directories like content/posts/ and content/pages/ are client DATA — allowed;
17
+ // these are the single-source contract FILES a client must import, never copy.)
18
+ const CORE_OWNED_CONTENT_FILES = ['schemas.ts', 'collections.ts', 'blocks.ts', 'posts.ts', 'calendar.ts'];
19
+
20
+ const toPosix = (s) => s.replace(/\\/g, '/');
21
+
22
+ function* walk(dir) {
23
+ for (const name of readdirSync(dir)) {
24
+ const p = join(dir, name);
25
+ if (statSync(p).isDirectory()) yield* walk(p);
26
+ else yield p;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Return client `src/` paths (posix-style, relative to srcRoot) that shadow a
32
+ * core module. Pure + synchronous so it is unit-testable against a fixture dir.
33
+ */
34
+ export function findShadowViolations(srcRoot) {
35
+ const violations = [];
36
+ if (!existsSync(srcRoot)) return violations; // no src/ → trivially thin
37
+
38
+ for (const dir of CORE_OWNED_DIRS) {
39
+ const p = join(srcRoot, dir);
40
+ if (existsSync(p) && statSync(p).isDirectory()) {
41
+ for (const f of walk(p)) violations.push(toPosix(relative(srcRoot, f)));
42
+ }
43
+ }
44
+
45
+ const contentDir = join(srcRoot, 'content');
46
+ for (const f of CORE_OWNED_CONTENT_FILES) {
47
+ const p = join(contentDir, f);
48
+ if (existsSync(p)) violations.push(toPosix(relative(srcRoot, p)));
49
+ }
50
+
51
+ return violations.sort();
52
+ }
53
+
54
+ function main() {
55
+ const srcRoot = process.argv[2] ?? 'src';
56
+ const violations = findShadowViolations(srcRoot);
57
+ if (violations.length === 0) {
58
+ console.log(`ferst-check-thin: OK — ${toPosix(srcRoot)}/ contains no copies of core modules.`);
59
+ process.exit(0);
60
+ }
61
+ console.error(
62
+ `ferst-check-thin: FAILED — ${violations.length} file(s) in ${toPosix(srcRoot)}/ shadow ferst-core.\n` +
63
+ `A client repo must DEPEND on the core, never copy it (manifest §2). Import these from\n` +
64
+ `'ferst-core' instead, or move a genuine one-off into src/custom/.\n\n` +
65
+ violations.map((v) => ` ✗ ${v}`).join('\n') +
66
+ '\n'
67
+ );
68
+ process.exit(1);
69
+ }
70
+
71
+ // Run only when invoked directly as a bin (not when imported by a test).
72
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
73
+ main();
74
+ }
@@ -0,0 +1,89 @@
1
+ ---
2
+ /**
3
+ * Renders an ordered list of blocks by mapping each block's `type` to its
4
+ * component via a registry. Disabled blocks are skipped. The registry is typed
5
+ * `Record<BlockType, …>`, so adding a block type to the union without a renderer
6
+ * here is a compile error — no silent `undefined` component at runtime.
7
+ */
8
+ import type { Block, BlockType } from '../content/blocks';
9
+ import Heading from './blocks/Heading.astro';
10
+ import Prose from './blocks/Prose.astro';
11
+ import ImageBlock from './blocks/ImageBlock.astro';
12
+ import ButtonBlock from './blocks/ButtonBlock.astro';
13
+ import Quote from './blocks/Quote.astro';
14
+ import Divider from './blocks/Divider.astro';
15
+ import Spacer from './blocks/Spacer.astro';
16
+ import Badge from './blocks/Badge.astro';
17
+ import Stat from './blocks/Stat.astro';
18
+ import List from './blocks/List.astro';
19
+ import Section from './blocks/Section.astro';
20
+ import Grid from './blocks/Grid.astro';
21
+ import Stack from './blocks/Stack.astro';
22
+ import Hero from './recipes/Hero.astro';
23
+ import FeatureCards from './recipes/FeatureCards.astro';
24
+ import Cta from './recipes/Cta.astro';
25
+ import Faq from './recipes/Faq.astro';
26
+ import Gallery from './recipes/Gallery.astro';
27
+ import Testimonial from './recipes/Testimonial.astro';
28
+ import ContactForm from './recipes/ContactForm.astro';
29
+ import Accordion from './recipes/Accordion.astro';
30
+ import Tiles from './recipes/Tiles.astro';
31
+ import Banner from './recipes/Banner.astro';
32
+ import Stats from './recipes/Stats.astro';
33
+ import Steps from './recipes/Steps.astro';
34
+ import Pricing from './recipes/Pricing.astro';
35
+ import Logos from './recipes/Logos.astro';
36
+ import Tabs from './recipes/Tabs.astro';
37
+ import Announcement from './recipes/Announcement.astro';
38
+ import Bento from './recipes/Bento.astro';
39
+ import Marquee from './recipes/Marquee.astro';
40
+
41
+ interface Props {
42
+ blocks: Block[];
43
+ }
44
+
45
+ const { blocks } = Astro.props;
46
+
47
+ const registry: Record<BlockType, any> = {
48
+ heading: Heading,
49
+ prose: Prose,
50
+ image: ImageBlock,
51
+ button: ButtonBlock,
52
+ quote: Quote,
53
+ divider: Divider,
54
+ spacer: Spacer,
55
+ badge: Badge,
56
+ stat: Stat,
57
+ list: List,
58
+ section: Section,
59
+ grid: Grid,
60
+ stack: Stack,
61
+ hero: Hero,
62
+ featureCards: FeatureCards,
63
+ cta: Cta,
64
+ faq: Faq,
65
+ gallery: Gallery,
66
+ testimonial: Testimonial,
67
+ contactForm: ContactForm,
68
+ accordion: Accordion,
69
+ tiles: Tiles,
70
+ banner: Banner,
71
+ stats: Stats,
72
+ steps: Steps,
73
+ pricing: Pricing,
74
+ logos: Logos,
75
+ tabs: Tabs,
76
+ announcement: Announcement,
77
+ bento: Bento,
78
+ marquee: Marquee,
79
+ };
80
+ ---
81
+
82
+ {
83
+ blocks
84
+ .filter((block) => block.enabled !== false)
85
+ .map((block) => {
86
+ const Component = registry[block.type];
87
+ return <Component {...block} />;
88
+ })
89
+ }
@@ -2,7 +2,7 @@
2
2
  interface Props {
3
3
  href?: string;
4
4
  type?: 'button' | 'submit' | 'reset';
5
- variant?: 'primary' | 'secondary' | 'cta' | 'compact';
5
+ variant?: 'primary' | 'secondary' | 'cta' | 'compact' | 'ghost';
6
6
  size?: 'sm' | 'md';
7
7
  disabled?: boolean;
8
8
  [x: string]: any;
@@ -45,8 +45,9 @@ const {
45
45
  justify-content: center;
46
46
  background: transparent;
47
47
  border: 2px solid var(--fg);
48
+ border-radius: var(--radius-sm);
48
49
  color: var(--fg);
49
- font-family: 'Poppins', sans-serif;
50
+ font-family: var(--font-heading);
50
51
  font-size: 0.75rem;
51
52
  font-weight: 600;
52
53
  text-transform: uppercase;
@@ -88,6 +89,24 @@ const {
88
89
  cursor: not-allowed;
89
90
  }
90
91
 
92
+ /* Role distinction: primary = filled (high emphasis), secondary = the base
93
+ outline (medium). Filled beats the base outline-hover, so override it. */
94
+ .btn--primary {
95
+ background: var(--gold);
96
+ border-color: var(--gold);
97
+ color: var(--text-light);
98
+ }
99
+ .btn--primary:hover,
100
+ .btn--primary:focus-visible {
101
+ background: color-mix(in srgb, var(--gold) 85%, #000);
102
+ border-color: color-mix(in srgb, var(--gold) 85%, #000);
103
+ color: var(--text-light);
104
+ }
105
+ .btn--primary:focus-visible {
106
+ outline: 2px solid var(--gold);
107
+ outline-offset: 2px;
108
+ }
109
+
91
110
  .btn--sm {
92
111
  height: auto;
93
112
  min-height: 40px;
@@ -98,13 +117,15 @@ const {
98
117
  .btn--sm { height: 40px; padding: 0 0.85rem; }
99
118
  }
100
119
 
101
- /* CTA variant - matches the header's Contact button style */
120
+ /* CTA / compact variants a filled button in the brand accent. Token-driven
121
+ (was a hardcoded CTK green) so it recolours with the site palette; hover
122
+ darkens the accent via color-mix, which works for any brand colour. */
102
123
  .btn--cta {
103
- background: #2A3A2A;
104
- color: #C8D4C2;
124
+ background: var(--gold);
125
+ color: var(--text-light);
105
126
  border: none;
106
127
  border-radius: 8px;
107
- font-family: 'Inter', 'Lato', sans-serif;
128
+ font-family: var(--font-body);
108
129
  font-size: 0.75rem;
109
130
  font-weight: 600;
110
131
  letter-spacing: 0.1em;
@@ -123,12 +144,12 @@ const {
123
144
  }
124
145
 
125
146
  .btn--cta:hover {
126
- background: #1A2A1A;
127
- color: #C8D4C2;
147
+ background: color-mix(in srgb, var(--gold) 85%, #000);
148
+ color: var(--text-light);
128
149
  }
129
150
  .btn--cta:focus-visible {
130
- background: #1A2A1A;
131
- color: #C8D4C2;
151
+ background: color-mix(in srgb, var(--gold) 85%, #000);
152
+ color: var(--text-light);
132
153
  outline: 2px solid var(--gold);
133
154
  outline-offset: 2px;
134
155
  }
@@ -139,11 +160,11 @@ const {
139
160
  justify-content: center;
140
161
  margin-top: auto;
141
162
  align-self: flex-start;
142
- background: #2A3A2A;
143
- color: #C8D4C2;
163
+ background: var(--gold);
164
+ color: var(--text-light);
144
165
  border: none;
145
166
  border-radius: 8px;
146
- font-family: 'Inter', 'Lato', sans-serif;
167
+ font-family: var(--font-body);
147
168
  font-size: 0.75rem;
148
169
  font-weight: 600;
149
170
  letter-spacing: 0.1em;
@@ -162,12 +183,33 @@ const {
162
183
  }
163
184
 
164
185
  .btn--compact:hover {
165
- background: #1A2A1A;
166
- color: #C8D4C2;
186
+ background: color-mix(in srgb, var(--gold) 85%, #000);
187
+ color: var(--text-light);
167
188
  }
168
189
  .btn--compact:focus-visible {
169
- background: #1A2A1A;
170
- color: #C8D4C2;
190
+ background: color-mix(in srgb, var(--gold) 85%, #000);
191
+ color: var(--text-light);
192
+ outline: 2px solid var(--gold);
193
+ outline-offset: 2px;
194
+ }
195
+
196
+ /* Ghost — a quiet, borderless text button for tertiary actions. */
197
+ .btn--ghost {
198
+ background: transparent;
199
+ border: 2px solid transparent;
200
+ color: var(--gold);
201
+ min-width: auto;
202
+ padding: 0 0.6rem;
203
+ }
204
+ .btn--ghost:hover {
205
+ color: var(--gold);
206
+ border-color: transparent;
207
+ background: var(--gold-light);
208
+ }
209
+ .btn--ghost:focus-visible {
210
+ color: var(--gold);
211
+ border-color: transparent;
212
+ background: var(--gold-light);
171
213
  outline: 2px solid var(--gold);
172
214
  outline-offset: 2px;
173
215
  }
@@ -178,31 +220,41 @@ const {
178
220
  color: var(--text-light);
179
221
  }
180
222
 
223
+ :global([data-theme="dark"]) .btn--ghost {
224
+ border-color: transparent;
225
+ color: var(--gold);
226
+ }
227
+ :global([data-theme="dark"]) .btn--ghost:hover,
228
+ :global([data-theme="dark"]) .btn--ghost:focus-visible {
229
+ border-color: transparent;
230
+ color: var(--gold);
231
+ background: var(--gold-light);
232
+ }
233
+
181
234
  :global([data-theme="dark"]) .btn:hover,
182
235
  :global([data-theme="dark"]) .btn:focus-visible {
183
236
  border-color: var(--gold);
184
237
  color: var(--gold);
185
238
  }
186
239
 
187
- :global([data-theme="dark"]) .btn--cta {
188
- background: #C8973A;
189
- color: #3D2B1A;
240
+ /* In dark mode --gold is a light accent, so dark text reads best on it, and
241
+ hover lightens (mix toward white) rather than darkens. */
242
+ :global([data-theme="dark"]) .btn--cta,
243
+ :global([data-theme="dark"]) .btn--compact,
244
+ :global([data-theme="dark"]) .btn--primary {
245
+ background: var(--gold);
246
+ border-color: var(--gold);
247
+ color: var(--dark-bg);
190
248
  }
191
249
 
192
250
  :global([data-theme="dark"]) .btn--cta:hover,
193
- :global([data-theme="dark"]) .btn--cta:focus-visible {
194
- background: #B8872A;
195
- color: #3D2B1A;
196
- }
197
-
198
- :global([data-theme="dark"]) .btn--compact {
199
- background: #C8973A;
200
- color: #3D2B1A;
201
- }
202
-
251
+ :global([data-theme="dark"]) .btn--cta:focus-visible,
203
252
  :global([data-theme="dark"]) .btn--compact:hover,
204
- :global([data-theme="dark"]) .btn--compact:focus-visible {
205
- background: #B8872A;
206
- color: #3D2B1A;
253
+ :global([data-theme="dark"]) .btn--compact:focus-visible,
254
+ :global([data-theme="dark"]) .btn--primary:hover,
255
+ :global([data-theme="dark"]) .btn--primary:focus-visible {
256
+ background: color-mix(in srgb, var(--gold) 85%, #fff);
257
+ border-color: color-mix(in srgb, var(--gold) 85%, #fff);
258
+ color: var(--dark-bg);
207
259
  }
208
260
  </style>