create-avocado-site 0.22.0 → 0.24.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.
@@ -7,13 +7,13 @@ import { DEMO_SITE_CONFIG_JSON } from "./demo-content.js";
7
7
  * It is 300 lines of token values with no interpolation in it, and it has to
8
8
  * stay identical to the one `apps/site` wears or the scaffold drifts into a
9
9
  * near-miss of the demo. `scripts/sync-demo-seed.mjs` writes it here from
10
- * `apps/site/app/theme-night-harvest.css`; like the demo assets beside it,
10
+ * `apps/site/app/theme-meadow.css`; like the demo assets beside it,
11
11
  * this file is generated and must not be edited in place.
12
12
  *
13
13
  * Resolved relative to the package root — two levels up from both
14
14
  * `src/templates` and `dist/templates`.
15
15
  */
16
- const THEME_FILE = fileURLToPath(new URL("../../theme/night-harvest.css", import.meta.url));
16
+ const THEME_FILE = fileURLToPath(new URL("../../theme/meadow.css", import.meta.url));
17
17
  /*
18
18
  * The theme control, on the same terms.
19
19
  *
@@ -645,7 +645,7 @@ main {
645
645
  }
646
646
  function layout(config) {
647
647
  return `import type { ReactNode } from "react"
648
- import { Anton, Archivo } from "next/font/google"
648
+ import { Bricolage_Grotesque, DM_Sans } from "next/font/google"
649
649
  import { SiteThemeToggle } from "./theme-toggle"
650
650
  import "./globals.css"
651
651
 
@@ -655,20 +655,20 @@ import "./globals.css"
655
655
  * \`--font-display\` and \`--font-body\` at these variables; without them the
656
656
  * whole thing falls back to system sans and reads like a different design.
657
657
  *
658
- * Anton has one cut, so asking it for a bold gets a synthesised one — the
659
- * display weight token stays at 400.
658
+ * Both are variable faces; the display weight token is 800, a real cut of
659
+ * Bricolage Grotesque.
660
660
  */
661
- const anton = Anton({ weight: "400", subsets: ["latin"], display: "swap", variable: "--font-anton" })
662
- const archivo = Archivo({ weight: ["400", "500"], subsets: ["latin"], display: "swap", variable: "--font-archivo" })
661
+ const bricolage = Bricolage_Grotesque({ subsets: ["latin"], display: "swap", variable: "--font-bricolage" })
662
+ const dmSans = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-dmsans" })
663
663
 
664
664
  /*
665
665
  * Runs before first paint so the theme never flashes.
666
666
  *
667
- * The query is \`prefers-color-scheme: light\`, not \`: dark\`: this is a dark
668
- * theme with a light half, so dark is the fallback and light is the opt-in. A
669
- * stored value is a pin; no stored value follows the system.
667
+ * The query is \`prefers-color-scheme: dark\`: this is a light theme with a
668
+ * dark half, so light is the fallback and dark is the opt-in. A stored value
669
+ * is a pin; no stored value follows the system.
670
670
  */
671
- const themeScript = \`(function(){try{var t=localStorage.getItem('site-theme-v1');var d=t==='dark'||(t!=='light'&&!matchMedia('(prefers-color-scheme:light)').matches);if(d)document.documentElement.classList.add('dark')}catch(e){document.documentElement.classList.add('dark')}})()\`
671
+ const themeScript = \`(function(){try{var f=window.parent!==window;var t=(f?sessionStorage:localStorage).getItem(f?'site-theme-preview-v1':'site-theme-v1');var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme:dark)').matches);if(d)document.documentElement.classList.add('dark')}catch(e){}})()\`
672
672
 
673
673
  /*
674
674
  * Otherwise deliberately minimal. Every page's own <title> and description come
@@ -681,13 +681,13 @@ export default function RootLayout({ children }: { children: ReactNode }) {
681
681
  <html
682
682
  lang="en"
683
683
  suppressHydrationWarning
684
- className={\`theme-night-harvest \${anton.variable} \${archivo.variable}\`}
685
- style={{ background: "var(--bg-0, #12170f)" }}
684
+ className={\`theme-meadow \${bricolage.variable} \${dmSans.variable}\`}
685
+ style={{ background: "var(--bg-0, #fafcf5)" }}
686
686
  >
687
687
  <head>
688
688
  <script dangerouslySetInnerHTML={{ __html: themeScript }} />
689
689
  </head>
690
- <body style={{ background: "var(--bg-0, #12170f)" }}>
690
+ <body style={{ background: "var(--bg-0, #fafcf5)" }}>
691
691
  {children}
692
692
  {/*
693
693
  One control, in the header's top-right corner — see the note in
@@ -17,7 +17,7 @@
17
17
  * `@avocadostudio-ai/orchestrator-core`, already pinned there; naming it again
18
18
  * here is how a project ends up with two.
19
19
  */
20
- export declare const AVOCADO = "0.22.0";
20
+ export declare const AVOCADO = "0.24.0";
21
21
  /**
22
22
  * Next 15.5.25 rather than 16, because that is the version every example app
23
23
  * and the demo site in this repository build and test against. The SDK
package/dist/versions.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * `@avocadostudio-ai/orchestrator-core`, already pinned there; naming it again
18
18
  * here is how a project ends up with two.
19
19
  */
20
- export const AVOCADO = "0.22.0";
20
+ export const AVOCADO = "0.24.0";
21
21
  /**
22
22
  * Next 15.5.25 rather than 16, because that is the version every example app
23
23
  * and the demo site in this repository build and test against. The SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-avocado-site",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Bootstrap a runnable Avocado Studio demo site, or wire Avocado into an existing Next.js project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,14 +31,14 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@clack/prompts": "^0.9.1",
34
- "@avocadostudio-ai/skills": "^0.22.0"
34
+ "@avocadostudio-ai/skills": "^0.24.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.13.10",
38
38
  "tsx": "^4.19.0",
39
39
  "typescript": "^5.7.3",
40
- "@avocadostudio-ai/site-sdk": "0.22.0",
41
- "@avocadostudio-ai/shared": "0.22.0"
40
+ "@avocadostudio-ai/shared": "0.24.0",
41
+ "@avocadostudio-ai/site-sdk": "0.24.0"
42
42
  },
43
43
  "license": "Apache-2.0",
44
44
  "homepage": "https://docs.avocadostudio.dev",
@@ -0,0 +1,225 @@
1
+ /*
2
+ * Light is the theme, dark is its variant — the other way round from Night
3
+ * Harvest. `html.theme-meadow` is the primary declaration, and the pre-paint
4
+ * script in `layout.tsx` and `SiteThemeToggle` add `dark` only when a visitor
5
+ * pins it or their system prefers dark.
6
+ *
7
+ * `--font-bricolage` and `--font-dmsans` are the `next/font` variables that
8
+ * `layout.tsx` sets on <html>.
9
+ */
10
+
11
+ /*
12
+ * theme-meadow
13
+ * A fresh green alternative (no red, no blue, minimal yellow) to theme-night-harvest for The Avocado Hub.
14
+ * Same token contract as theme-night-harvest: only variables that theme already
15
+ * sets are overridden, so every block (banner, hero, card-grid, stats, two-column,
16
+ * tabs, video, faq, cta, site-top-nav, site-footer) picks it up without markup changes.
17
+ *
18
+ * Activate: <html class="theme-meadow"> (in place of theme-night-harvest).
19
+ * Fonts: add Bricolage Grotesque + DM Sans via next/font and expose them as
20
+ * --font-bricolage and --font-dmsans (same pattern as --font-anton / --font-archivo).
21
+ */
22
+
23
+ html.theme-meadow {
24
+ /* Ground and surfaces */
25
+ --bg-0: #fafcf5;
26
+ --bg-000: #fafcf5;
27
+ --bg-1: #ffffff;
28
+ --bg-100: #ffffff;
29
+ --bg-200: #e3f2e1;
30
+ --bg-300: #cfe6cc;
31
+ --section-bg: #fafcf5;
32
+ --section-bg-panel: #e3f2e1;
33
+ --hero-bg: #fafcf5;
34
+ --surface: #ffffff;
35
+ --surface-border: #17301c;
36
+ --stripe-bg: #e3f2e1;
37
+ --table-header-bg: #e3f2e1;
38
+ --card-bg: #ffffff;
39
+ --cta-bg: #1d4e3a;
40
+ --placeholder-img: #b7d3a8;
41
+ --media-bg: #17301c;
42
+
43
+ /* Text */
44
+ --heading: #17301c;
45
+ --body: #243b28;
46
+ --prose-body: #243b28;
47
+ --body-secondary: #4e6552;
48
+ --lead: #4e6552;
49
+ --caption: #4e6552;
50
+ --text-100: #17301c;
51
+ --text-200: #243b28;
52
+ --text-300: #4e6552;
53
+ --text-400: #4e6552;
54
+ --text-500: #4e6552;
55
+
56
+ /* Brand, links, accent */
57
+ --brand: #1e6e3a;
58
+ --brand-fg: #ffffff;
59
+ --brand-hover: #17301c;
60
+ --brand-subtle: rgba(30, 110, 58, 0.10);
61
+ --link: #1e6e3a;
62
+ --accent: #9ed36a;
63
+ --accent-hover: #8cc657;
64
+ --accent-ink: #1e6e3a;
65
+ --on-accent: #17301c;
66
+ --btn-primary-bg: #9ed36a;
67
+ --btn-primary-fg: #17301c;
68
+ --quote-mark: #b7d3a8;
69
+ --quote-mark-opacity: 1;
70
+
71
+ /* Inverse band (banner--info, cta-section, section--inverse) */
72
+ --band: #1d4e3a;
73
+ --on-band: #b8e986;
74
+ --on-ink: #b8e986;
75
+
76
+ /* Imagery */
77
+ --on-image: #fafcf5;
78
+ --on-image-muted: rgba(250, 252, 245, 0.88);
79
+ --on-image-subtle: #b8e986;
80
+ --photo-treatment: saturate(1.25) contrast(1.06);
81
+ --scrim: linear-gradient(to bottom, rgb(23 48 28 / 0.10) 0%, rgb(23 48 28 / 0.45) 55%, rgb(23 48 28 / 0.85) 100%);
82
+
83
+ /* Rules and borders */
84
+ --border: rgba(23, 48, 28, 0.18);
85
+ --rule: rgba(23, 48, 28, 0.18);
86
+ --nav-bg: rgba(250, 252, 245, 0.94);
87
+ --nav-border: #17301c;
88
+ --code-bg: rgba(30, 110, 58, 0.10);
89
+ --pre-bg: rgba(30, 110, 58, 0.07);
90
+
91
+ /* Footer */
92
+ --footer-bg: #17301c;
93
+ --footer-border: rgba(213, 230, 214, 0.2);
94
+ --footer-heading: #b8e986;
95
+ --footer-text: #d5e6d6;
96
+ --footer-link: #d5e6d6;
97
+ --footer-link-hover: #9ed36a;
98
+
99
+ /* Type */
100
+ --font-display: var(--font-bricolage), "Bricolage Grotesque", Helvetica, sans-serif;
101
+ --font-heading: var(--font-bricolage), "Bricolage Grotesque", Helvetica, sans-serif;
102
+ --font-quote: var(--font-bricolage), "Bricolage Grotesque", Helvetica, sans-serif;
103
+ --font-body: var(--font-dmsans), "DM Sans", Helvetica, sans-serif;
104
+ --font-label: var(--font-dmsans), "DM Sans", Helvetica, sans-serif;
105
+ --font-display-weight: 800;
106
+ --font-display-track: -0.03em;
107
+ --hero-headline-size: clamp(2.75rem, 6vw, 5.25rem);
108
+ --hero-headline-leading: 0.95;
109
+ --hero-headline-width: 12ch;
110
+ --hero-content-anchor: flex-end;
111
+ --hero-content-width: 100%;
112
+ --hero-image-position: 50% 40%;
113
+ --hero-min-h: min(62vh, 600px);
114
+
115
+ /* Shape: pills for actions, soft cards, hard offset "sticker" shadows (unchanged from the first proposal) */
116
+ --radius: 16px;
117
+ --radius-btn: 999px;
118
+ --radius-card: 24px;
119
+ --radius-feature: 28px;
120
+ --radius-input: 14px;
121
+ --radius-dropdown: 16px;
122
+ --radius-logo: 12px;
123
+ --card-shadow: 0 0 0 2px #17301c, 6px 6px 0 #17301c;
124
+ --shadow-btn: 0 0 0 2px #17301c, 3px 3px 0 #17301c;
125
+ --shadow-float: 0 0 0 2px #17301c, 4px 4px 0 #17301c;
126
+ --shadow-dropdown: 0 0 0 2px #17301c, 4px 4px 0 #17301c;
127
+ --shadow-menu: 0 0 0 2px #17301c, 4px 4px 0 #17301c;
128
+
129
+ /* Spacing (kept close to night-harvest so block layouts do not shift) */
130
+ --section-pad-x: 40px;
131
+ --section-pad-x-sm: 16px;
132
+ --section-pad-x-tight-sm: 16px;
133
+ --section-pad-y: 64px;
134
+ --section-pad-y-cta: 72px;
135
+ --section-pad-y-hero: 56px;
136
+ --section-pad-y-hero-sm: 32px;
137
+ --section-pad-y-sm: 40px;
138
+ --section-pad-y-tight: 48px;
139
+ --section-pad-y-tight-sm: 32px;
140
+
141
+ /* Status banners */
142
+ --banner-success-bg: #c9e27a;
143
+ --banner-success-text: #17301c;
144
+ --banner-warning-bg: #efd9a0;
145
+ --banner-warning-text: #17301c;
146
+ }
147
+
148
+ html.theme-meadow.dark {
149
+ --bg-0: #0f1d14;
150
+ --bg-000: #0f1d14;
151
+ --bg-1: #182a1e;
152
+ --bg-100: #182a1e;
153
+ --bg-200: #182a1e;
154
+ --bg-300: #223829;
155
+ --section-bg: #0f1d14;
156
+ --section-bg-panel: #182a1e;
157
+ --hero-bg: #0f1d14;
158
+ --surface: #182a1e;
159
+ --surface-border: #9ed36a;
160
+ --stripe-bg: #182a1e;
161
+ --table-header-bg: #182a1e;
162
+ --card-bg: #182a1e;
163
+ --cta-bg: #1f4a33;
164
+ --media-bg: #0f1d14;
165
+
166
+ --heading: #eef6ee;
167
+ --body: #eef6ee;
168
+ --prose-body: #eef6ee;
169
+ --body-secondary: #a3b8a6;
170
+ --lead: #a3b8a6;
171
+ --caption: #a3b8a6;
172
+ --text-100: #eef6ee;
173
+ --text-200: #eef6ee;
174
+ --text-300: #a3b8a6;
175
+ --text-400: #a3b8a6;
176
+ --text-500: #a3b8a6;
177
+
178
+ --brand: #8fd17a;
179
+ --brand-fg: #0f1d14;
180
+ --brand-hover: #eef6ee;
181
+ --brand-subtle: rgba(143, 209, 122, 0.14);
182
+ --link: #8fd17a;
183
+ --accent: #9ed36a;
184
+ --accent-hover: #b3e08a;
185
+ --accent-ink: #9ed36a;
186
+ --on-accent: #0f1d14;
187
+ --btn-primary-bg: #9ed36a;
188
+ --btn-primary-fg: #0f1d14;
189
+ --quote-mark: #b7d3a8;
190
+
191
+ /* The band and the CTA stay a deep green with lime words, as in light.
192
+ A full lime strip was the loudest thing on a dark page. */
193
+ --band: #1f4a33;
194
+ --on-band: #b8e986;
195
+ --on-ink: #9ed36a;
196
+
197
+ --border: rgba(238, 246, 238, 0.2);
198
+ --rule: rgba(238, 246, 238, 0.2);
199
+ --nav-bg: rgba(15, 29, 20, 0.94);
200
+ --nav-border: #9ed36a;
201
+ --code-bg: rgba(238, 246, 238, 0.12);
202
+ --pre-bg: rgba(238, 246, 238, 0.08);
203
+
204
+ --footer-bg: #0a140e;
205
+ --footer-border: rgba(238, 246, 238, 0.2);
206
+ --footer-heading: #9ed36a;
207
+ --footer-text: #a3b8a6;
208
+ --footer-link: #a3b8a6;
209
+ --footer-link-hover: #9ed36a;
210
+
211
+ --card-shadow: 0 0 0 2px #9ed36a, 6px 6px 0 #9ed36a;
212
+ --shadow-btn: 0 0 0 2px #4f7d34, 3px 3px 0 #4f7d34;
213
+ --shadow-float: 0 0 0 2px #9ed36a, 4px 4px 0 #9ed36a;
214
+ --shadow-dropdown: 0 0 0 2px #9ed36a, 4px 4px 0 #9ed36a;
215
+ --shadow-menu: 0 0 0 2px #9ed36a, 4px 4px 0 #9ed36a;
216
+
217
+ --banner-success-bg: #182a1e;
218
+ --banner-success-text: #9ed36a;
219
+ --banner-warning-bg: #182a1e;
220
+ --banner-warning-text: #efd9a0;
221
+ --error-bg: #182a1e;
222
+ --error-border: rgba(238, 246, 238, 0.2);
223
+ --error-text: #eef6ee;
224
+ --error-detail: #a3b8a6;
225
+ }
@@ -4,6 +4,26 @@ import { useCallback, useEffect, useState } from "react"
4
4
  import { createPortal } from "react-dom"
5
5
 
6
6
  const SITE_THEME_STORAGE_KEY = "site-theme-v1"
7
+ /*
8
+ * Inside the editor's preview frame the choice lives in sessionStorage under
9
+ * its own key. It used to live nowhere — deliberately, so an editor session
10
+ * leaves no pinned preference in the published site's storage — and every
11
+ * link clicked in the preview loaded a page that had forgotten it: the theme
12
+ * reset on each navigation. sessionStorage survives the navigation, dies with
13
+ * the tab, and never touches the site's own key. The pre-paint script in
14
+ * `layout.tsx` reads the same pair.
15
+ */
16
+ const PREVIEW_THEME_STORAGE_KEY = "site-theme-preview-v1"
17
+
18
+ function themeStore(): { storage: Storage; key: string } | null {
19
+ try {
20
+ return window.parent !== window
21
+ ? { storage: window.sessionStorage, key: PREVIEW_THEME_STORAGE_KEY }
22
+ : { storage: window.localStorage, key: SITE_THEME_STORAGE_KEY }
23
+ } catch {
24
+ return null
25
+ }
26
+ }
7
27
 
8
28
  type Choice = "system" | "light" | "dark"
9
29
 
@@ -18,12 +38,11 @@ const LABEL: Record<Choice, string> = { system: "System", light: "Light", dark:
18
38
  * who has never touched the control tracks their OS for as long as the page is
19
39
  * open, and one who has chosen keeps their choice across visits.
20
40
  *
21
- * `prefers-color-scheme: light` and not `: dark`. Night Harvest is a dark
22
- * theme with a light half, so the fallback is dark and light is the opt-in.
23
- * That is the same way round as the token contract itself, where the light
24
- * values sit inside a `prefers-color-scheme: light` query and dark is what
25
- * `:root` says. A browser that expresses no preference gets the theme's
26
- * primary state rather than its secondary one.
41
+ * `prefers-color-scheme: dark`. Meadow is a light theme with a dark
42
+ * half, so the fallback is light and dark is the opt-in — the same way round
43
+ * as the theme file, whose `html.theme-meadow` block is the light one.
44
+ * A browser that expresses no preference gets the theme's primary state
45
+ * rather than its secondary one.
27
46
  *
28
47
  * It writes the `dark` class on `<html>`, not `data-theme`. The brief asks for
29
48
  * the attribute and the attribute would be the nicer contract, but the class
@@ -45,14 +64,17 @@ const THEME_EVENT = "site-theme-change"
45
64
 
46
65
  export function SiteThemeToggle({ portalTo }: { portalTo?: string } = {}) {
47
66
  const [choice, setChoice] = useState<Choice>("system")
48
- const [resolved, setResolved] = useState<"light" | "dark">("dark")
67
+ const [resolved, setResolved] = useState<"light" | "dark">("light")
68
+ /* Until the stored choice has been read, the class the pre-paint script set
69
+ is the truth. Applying the initial "light" first flashed a dark page. */
70
+ const [ready, setReady] = useState(false)
49
71
 
50
72
  /* Kept identical to the pre-paint script in `layout.tsx`. If the two ever
51
73
  disagree the page paints one theme and then visibly switches to the
52
74
  other, which is the flash the script exists to prevent. */
53
75
  const resolve = useCallback((c: Choice): "light" | "dark" => {
54
76
  if (c !== "system") return c
55
- return window.matchMedia?.("(prefers-color-scheme: light)").matches ? "light" : "dark"
77
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light"
56
78
  }, [])
57
79
 
58
80
  const [host, setHost] = useState<Element | null>(null)
@@ -69,10 +91,17 @@ export function SiteThemeToggle({ portalTo }: { portalTo?: string } = {}) {
69
91
  }, [portalTo])
70
92
 
71
93
  useEffect(() => {
72
- const stored = window.localStorage.getItem(SITE_THEME_STORAGE_KEY)
94
+ const store = themeStore()
95
+ let stored: string | null = null
96
+ try {
97
+ stored = store ? store.storage.getItem(store.key) : null
98
+ } catch {
99
+ // Blocked storage reads as "follow the system".
100
+ }
73
101
  const next: Choice = stored === "light" || stored === "dark" ? stored : "system"
74
102
  setChoice(next)
75
103
  setResolved(resolve(next))
104
+ setReady(true)
76
105
  }, [resolve])
77
106
 
78
107
  /* Track the OS for as long as the visitor is on "system". Re-subscribing on
@@ -80,7 +109,7 @@ export function SiteThemeToggle({ portalTo }: { portalTo?: string } = {}) {
80
109
  back by the OS mid-session. */
81
110
  useEffect(() => {
82
111
  if (choice !== "system") return
83
- const mq = window.matchMedia?.("(prefers-color-scheme: light)")
112
+ const mq = window.matchMedia?.("(prefers-color-scheme: dark)")
84
113
  if (!mq) return
85
114
  const onChange = () => setResolved(resolve("system"))
86
115
  mq.addEventListener("change", onChange)
@@ -98,6 +127,7 @@ export function SiteThemeToggle({ portalTo }: { portalTo?: string } = {}) {
98
127
  }, [resolve])
99
128
 
100
129
  useEffect(() => {
130
+ if (!ready) return
101
131
  const root = window.document.documentElement
102
132
  /*
103
133
  * Colour transitions are right for a hover and wrong for a theme change:
@@ -112,24 +142,19 @@ export function SiteThemeToggle({ portalTo }: { portalTo?: string } = {}) {
112
142
  window.requestAnimationFrame(() => root.removeAttribute("data-theme-switching"))
113
143
  )
114
144
  return () => window.cancelAnimationFrame(frame)
115
- }, [resolved])
145
+ }, [resolved, ready])
116
146
 
117
147
  const handleClick = () => {
118
148
  const next = ORDER[(ORDER.indexOf(choice) + 1) % ORDER.length]
119
149
  setChoice(next)
120
150
  setResolved(resolve(next))
121
151
  window.dispatchEvent(new CustomEvent(THEME_EVENT, { detail: next }))
122
- /*
123
- * Not inside the preview iframe: an editor session should not leave a
124
- * pinned preference behind in the published site's own storage.
125
- */
126
- if (window.parent === window) {
127
- try {
128
- if (next === "system") window.localStorage.removeItem(SITE_THEME_STORAGE_KEY)
129
- else window.localStorage.setItem(SITE_THEME_STORAGE_KEY, next)
130
- } catch {
131
- // Blocked storage — the class toggle above still applies for this visit.
132
- }
152
+ const store = themeStore()
153
+ try {
154
+ if (store && next === "system") store.storage.removeItem(store.key)
155
+ else if (store) store.storage.setItem(store.key, next)
156
+ } catch {
157
+ // Blocked storage — the class toggle above still applies for this visit.
133
158
  }
134
159
  }
135
160
 
@@ -1,320 +0,0 @@
1
- /*
2
- * Theme: Night Harvest
3
- *
4
- * Chartreuse on near-black, Anton over Archivo, nothing rounded. A set of
5
- * values for the tokens the blocks package already reads — there is no Night
6
- * Harvest code anywhere, and no block knows this file exists.
7
- *
8
- * Dark is the theme, not a mode of it. `html.theme-night-harvest.dark` is
9
- * therefore the primary declaration and the site's pre-paint script leaves the
10
- * `dark` class on unless a visitor has asked for light. The light half is a
11
- * genuine inversion rather than a lightening: the band flips from chartreuse
12
- * on ink to ink on chartreuse, and the accent stops being legible as text, so
13
- * words in the accent switch to a darker relative of the same hue.
14
- *
15
- * ── The three that are easy to get wrong ──
16
- *
17
- * `--accent` is a fill and `--accent-ink` is the accent as a word. They are
18
- * the same value in dark and different in light: #C5E035 on #F2F3EA is about
19
- * 1.3:1, which is a highlighter, not text. Anything that sets `color` reads
20
- * `--accent-ink`.
21
- *
22
- * `--band` and `--on-band` are the full-bleed strip. The band inverts between
23
- * themes instead of lightening, so it cannot be derived from the accent.
24
- *
25
- * `--bg-100` (the panel) against `--bg-0` (the ground) is roughly 1.1:1 in
26
- * both themes, and that is the intent. A card is separated by the grid gap.
27
- * Do not reach for a border or a shadow to make one "visible".
28
- */
29
-
30
- /* ── Light ──────────────────────────────────────────────────────────────── */
31
-
32
- html.theme-night-harvest {
33
- color-scheme: light;
34
-
35
- /* Ground and panel */
36
- --bg-0: #f2f3ea;
37
- --bg-000: #f2f3ea;
38
- --bg-100: #e4e7d7;
39
- --bg-200: #e4e7d7;
40
- --bg-300: #d5d9c6;
41
- --bg-1: #e4e7d7;
42
- --surface: #e4e7d7;
43
- --card-bg: #e4e7d7;
44
- --section-bg: #f2f3ea;
45
- --hero-bg: #f2f3ea;
46
- --cta-bg: #e4e7d7;
47
- --stripe-bg: #e4e7d7;
48
- --table-header-bg: #e4e7d7;
49
-
50
- /* The alternating ground. Panel is the same #E4E7D7 a card sits on, so a
51
- panel section and the cards inside it read as one surface. */
52
- --section-bg-panel: #e4e7d7;
53
-
54
- /* Ink and muted */
55
- --text-100: #14190f;
56
- --text-200: #14190f;
57
- --text-300: #565c4c;
58
- --text-400: #565c4c;
59
- --text-500: #565c4c;
60
- --heading: #14190f;
61
- --body: #14190f;
62
- --body-secondary: #565c4c;
63
- --caption: #565c4c;
64
- --prose-body: #14190f;
65
- --lead: #565c4c;
66
-
67
- /*
68
- * The accent, in its two jobs. `--brand` is the repo's name for "the colour
69
- * a word is set in", so it takes the text-safe value; the fill lives in
70
- * `--accent` and reaches the primary button through `--btn-primary-bg`.
71
- */
72
- --accent: #c5e035;
73
- --accent-hover: #c5e035;
74
- --accent-ink: #46630d;
75
- --on-accent: #14190f;
76
- --brand: #46630d;
77
- --brand-hover: #14190f;
78
- --brand-fg: #14190f;
79
- --brand-subtle: rgba(197, 224, 53, 0.22);
80
- --link: #46630d;
81
- --btn-primary-bg: #c5e035;
82
- --btn-primary-fg: #14190f;
83
-
84
- /* The band inverts: ink ground, chartreuse type. */
85
- --band: #14190f;
86
- --on-band: #c5e035;
87
- --on-ink: #c5e035;
88
-
89
- /* Hairlines */
90
- --rule: rgba(20, 25, 15, 0.16);
91
- --border: rgba(20, 25, 15, 0.16);
92
- --surface-border: rgba(20, 25, 15, 0.16);
93
- --nav-border: rgba(20, 25, 15, 0.16);
94
- --footer-border: rgba(20, 25, 15, 0.16);
95
- --code-bg: rgba(20, 25, 15, 0.1);
96
- --pre-bg: rgba(20, 25, 15, 0.07);
97
-
98
- /* Chrome */
99
- --nav-bg: rgba(242, 243, 234, 0.94);
100
-
101
- /*
102
- * The footer is a panel, not a slab of ink.
103
- *
104
- * It was #14190F — the same value as the inverse band — so on any page
105
- * ending in a CTA the band ran straight into the footer and the two read as
106
- * one undifferentiated dark block a third of the page tall. The boundary
107
- * between "the last thing we are asking you to do" and "the small print"
108
- * had disappeared.
109
- *
110
- * Making it the panel ground fixes it in both directions: in light the ink
111
- * band now sits against a pale footer, and in dark the chartreuse band sits
112
- * against the same panel. The footer is the same surface as a card in both
113
- * themes, which is what it is.
114
- */
115
- --footer-bg: var(--section-bg-panel);
116
- --footer-text: #565c4c;
117
- --footer-heading: #14190f;
118
- --footer-link: #565c4c;
119
- --footer-link-hover: #46630d;
120
-
121
- /*
122
- * An image position with no image is a flat accent field, not a grey box.
123
- * It is a designed state: on a page whose photography is not all there yet,
124
- * a chartreuse panel reads as a decision and a grey one reads as a bug.
125
- */
126
- --placeholder-img: #c5e035;
127
- --media-bg: #14190f;
128
-
129
- /*
130
- * The hero as a poster.
131
- *
132
- * Most of the fold, the headline at display scale set solid, and the type
133
- * anchored to the bottom of the frame rather than floating in the middle of
134
- * it — which is what separates a poster from a stock hero with words on it.
135
- *
136
- * The scrim sinks the photograph towards the page's own near-black instead
137
- * of neutral black. A warm palette with a neutral scrim over its one
138
- * photograph reads as two designs, and the grey is the one you notice.
139
- */
140
- /*
141
- * A duotone, built from filters rather than an asset, so it applies to
142
- * whatever photograph the content happens to name. Desaturate, lift the
143
- * contrast, drop the exposure, then push the whole thing through the
144
- * palette's own hue. The photograph stops being a warm neutral standing
145
- * beside chartreuse and becomes the dark end of it.
146
- */
147
- --photo-treatment: grayscale(1) contrast(1.15) brightness(0.55) sepia(0.55) hue-rotate(35deg) saturate(2.6);
148
-
149
- /* 58vh, not 78. The frame was taking the whole fold for a photograph that is
150
- mostly empty wall, which is a lot of screen spent on very little. */
151
- /* The fruit sits in the upper half of this photograph, so a centred crop
152
- takes the top off it. */
153
- --hero-image-position: 50% 18%;
154
-
155
- --hero-min-h: min(58vh, 560px);
156
- --hero-headline-size: clamp(2.5rem, 5.2vw, 4.5rem);
157
- --hero-headline-leading: 0.92;
158
- --hero-content-anchor: flex-end;
159
- --hero-content-width: 100%;
160
- /*
161
- * 11ch, not 14. At 14 the headline ran two wide lines straight across the
162
- * middle of the frame and landed on the fruit — the photograph had room for
163
- * type on the left and the type refused to stay in it. A narrow column is
164
- * also simply what a poster does: three short lines stacked hard left read
165
- * as deliberate, where two long ones read as a caption that outgrew its box.
166
- */
167
- --hero-headline-width: 11ch;
168
- --scrim: linear-gradient(to bottom, rgb(18 23 15 / 0.35) 0%, rgb(18 23 15 / 0.62) 55%, rgb(18 23 15 / 0.9) 100%);
169
- --on-image: #ecefe3;
170
- --on-image-muted: rgba(236, 239, 227, 0.82);
171
- --on-image-subtle: #c5e035;
172
-
173
- --quote-mark: #46630d;
174
- --quote-mark-opacity: 1;
175
-
176
- /* Nothing is rounded. */
177
- --radius: 0px;
178
- --radius-btn: 0px;
179
- --radius-card: 0px;
180
- --radius-feature: 0px;
181
- --radius-logo: 0px;
182
- --radius-input: 0px;
183
- --radius-dropdown: 0px;
184
-
185
- /* Flat. The panel/ground relationship does the separating. */
186
- --card-shadow: none;
187
- --shadow-btn: none;
188
- --shadow-float: none;
189
- --shadow-dropdown: 0 0 0 1px var(--rule);
190
- --shadow-menu: 0 0 0 1px var(--rule);
191
-
192
- /*
193
- * Anton has one cut, so the display weight drops to 400 — asking a poster
194
- * face for 800 gets you a synthesised bold that ruins its sidebearings.
195
- */
196
- --font-display: var(--font-anton), Impact, "Haettenschweiler", sans-serif;
197
- --font-heading: var(--font-anton), Impact, "Haettenschweiler", sans-serif;
198
- --font-body: var(--font-archivo), Helvetica, Arial, sans-serif;
199
- --font-label: var(--font-archivo), Helvetica, Arial, sans-serif;
200
- --font-quote: var(--font-anton), Impact, sans-serif;
201
- --font-display-weight: 400;
202
- --font-display-track: -0.005em;
203
-
204
- /*
205
- * ~120px BETWEEN sections, which is 56 a side — not 120 a side.
206
- *
207
- * Sections abut, so each one pays half the gap. Reading "section spacing
208
- * holds at 120px" as 120 of padding put 240px of dead ground between a card
209
- * row and the next heading, which is not a rhythm, it is a hole.
210
- *
211
- * 56 rather than 60 because 60 is not on the scale (4 8 12 16 24 32 40 56 80
212
- * 120) and a step invented to make one sum come out is how a scale stops
213
- * being one. 56 a side gives 112 between, four pixels off the number in the
214
- * brief and indistinguishable from it on a page.
215
- *
216
- * This is the first thing that quietly collapses during an implementation,
217
- * so it is one value: every block reads it, and no block sets its own.
218
- */
219
- --section-pad-y: 56px;
220
- --section-pad-y-tight: 56px;
221
- --section-pad-y-cta: 56px;
222
- --section-pad-y-hero: 56px;
223
- --section-pad-x: 40px;
224
-
225
- --section-pad-y-sm: 32px;
226
- --section-pad-y-tight-sm: 32px;
227
- --section-pad-y-hero-sm: 32px;
228
- --section-pad-x-sm: 16px;
229
- --section-pad-x-tight-sm: 16px;
230
- }
231
-
232
- /* ── Dark, the primary ──────────────────────────────────────────────────── */
233
-
234
- html.theme-night-harvest.dark {
235
- color-scheme: dark;
236
-
237
- --bg-0: #12170f;
238
- --bg-000: #12170f;
239
- --bg-100: #1c2317;
240
- --bg-200: #1c2317;
241
- --bg-300: #262f1f;
242
- --bg-1: #1c2317;
243
- --surface: #1c2317;
244
- --card-bg: #1c2317;
245
- --section-bg: #12170f;
246
- --hero-bg: #12170f;
247
- --cta-bg: #1c2317;
248
- --stripe-bg: #1c2317;
249
- --table-header-bg: #1c2317;
250
- --section-bg-panel: #1c2317;
251
-
252
- --text-100: #ecefe3;
253
- --text-200: #ecefe3;
254
- --text-300: #9aa38c;
255
- --text-400: #9aa38c;
256
- --text-500: #9aa38c;
257
- --heading: #ecefe3;
258
- --body: #ecefe3;
259
- --body-secondary: #9aa38c;
260
- --caption: #9aa38c;
261
- --prose-body: #ecefe3;
262
- --lead: #9aa38c;
263
-
264
- /* Here the fill and the ink are the same value — chartreuse on #12170F is
265
- legible as a word. That coincidence is why `--accent-ink` has to exist:
266
- the light half proves it is a coincidence. */
267
- --accent: #c5e035;
268
- --accent-hover: #c5e035;
269
- --accent-ink: #c5e035;
270
- --on-accent: #12170f;
271
- --brand: #c5e035;
272
- --brand-hover: #ecefe3;
273
- --brand-fg: #12170f;
274
- --brand-subtle: rgba(197, 224, 53, 0.12);
275
- --link: #c5e035;
276
- --btn-primary-bg: #c5e035;
277
- --btn-primary-fg: #12170f;
278
-
279
- --band: #c5e035;
280
- --on-band: #12170f;
281
- --on-ink: #c5e035;
282
-
283
- --rule: rgba(236, 239, 227, 0.2);
284
- --border: rgba(236, 239, 227, 0.2);
285
- --surface-border: rgba(236, 239, 227, 0.2);
286
- --nav-border: rgba(236, 239, 227, 0.2);
287
- --footer-border: rgba(236, 239, 227, 0.2);
288
- --code-bg: rgba(236, 239, 227, 0.12);
289
- --pre-bg: rgba(236, 239, 227, 0.08);
290
-
291
- --nav-bg: rgba(18, 23, 15, 0.94);
292
- --footer-bg: var(--section-bg-panel);
293
- --footer-text: #9aa38c;
294
- --footer-heading: #ecefe3;
295
- --footer-link: #9aa38c;
296
- --footer-link-hover: #c5e035;
297
-
298
- --placeholder-img: #c5e035;
299
- --media-bg: #12170f;
300
-
301
- --quote-mark: #c5e035;
302
- --quote-mark-opacity: 1;
303
-
304
- --card-shadow: none;
305
- --shadow-btn: none;
306
- --shadow-float: none;
307
- --shadow-dropdown: 0 0 0 1px var(--rule);
308
- --shadow-menu: 0 0 0 1px var(--rule);
309
-
310
- /* Status fills, restated so they sit in the palette rather than arriving
311
- from the package's slate-and-emerald defaults. */
312
- --banner-success-bg: #1c2317;
313
- --banner-success-text: #c5e035;
314
- --banner-warning-bg: #1c2317;
315
- --banner-warning-text: #ecefe3;
316
- --error-bg: #1c2317;
317
- --error-border: rgba(236, 239, 227, 0.2);
318
- --error-text: #ecefe3;
319
- --error-detail: #9aa38c;
320
- }