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.
- 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 +93 -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
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.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
package/components/Button.astro
CHANGED
|
@@ -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:
|
|
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
|
|
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:
|
|
104
|
-
color:
|
|
124
|
+
background: var(--gold);
|
|
125
|
+
color: var(--text-light);
|
|
105
126
|
border: none;
|
|
106
127
|
border-radius: 8px;
|
|
107
|
-
font-family:
|
|
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: #
|
|
127
|
-
color:
|
|
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: #
|
|
131
|
-
color:
|
|
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:
|
|
143
|
-
color:
|
|
163
|
+
background: var(--gold);
|
|
164
|
+
color: var(--text-light);
|
|
144
165
|
border: none;
|
|
145
166
|
border-radius: 8px;
|
|
146
|
-
font-family:
|
|
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: #
|
|
166
|
-
color:
|
|
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: #
|
|
170
|
-
color:
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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>
|