feat(earthen): Wave A theme lane — 49 overrides, 7 templates, seed (WO-TF-016)

Deliver the full conservation-nonprofit theme per OVERRIDE-CONTRACT (Wave A).

- 49 mandatory builtin overrides in templates/overrides/earthen/ (chrome,
  marketing, primitives, commerce, blog family, auth surface, page-suggestions),
  re-skinned from the frozen builtins into botanical/paper-grain chrome with
  all content fields, custom tags, provider vars, HTMX/auth wiring and JS
  preserved verbatim. Stale button/card/image/text/heading overrides fixed to
  the new field contract (link/style/label, media/text via {% img %}).
- 7 page templates: default, full-width, landing, article, blog-index, contact,
  auth.
- Blocks rework: impact_metrics/partner_logos/botanical_divider/footer converted
  to builtin overrides (deleted as theme blocks); donation_cta and field_note
  kept; impact_report added. Persona blocks now 3.
- 5 presets (all 19 tokens; mossbed/sage-morning/ember-loam are dual-mode).
- Bundled Fraunces + Work Sans woff2 (OFL) in assets/fonts/; fonts.json,
  RECOMMENDED_FONTS.md, RECOMMENDED_ICONS.md; required_icon_packs=[lucide].
- Seed: home/about/blog(3 posts)/contact/login as Rootline Trust, contact-form
  formConfig.targetTable=contact_submissions + row_inserted email-admins workflow.
- Themed email-safe wrapper; on-theme master pages; CSS-first botanical motion.

Verified: make (build) exit 0, make archive-check exit 0, check-safety exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-05 11:13:01 +08:00
parent 4aeddfe352
commit 8b8fb4d65d
86 changed files with 1920 additions and 290 deletions

View File

@ -1,47 +1,39 @@
# Earthen — recommended fonts # Earthen — fonts
This theme ships `fonts.json = []` per the wave-1 fonts policy Earthen **bundles** its display and body faces as `source=template` rows via
(see `themes/docs/FONTS.md`). The CSS uses `fonts.json`, so the theme reads correctly on activation with no admin setup.
`var(--font-heading)` / `var(--font-body)` / `var(--font-mono)` with a All CSS references fonts through `var(--font-heading)` / `var(--font-body)` /
fallback stack so the page reads close to the intended aesthetic before `var(--font-mono)` with a fallback stack, so the admin can still override any
the admin assigns fonts. role from the Typography panel.
Configure these in the admin Typography panel to match the spec's ## Bundled faces (ship with the theme)
intended look.
## Display / heading — assign to `Heading` | Role | Family | Weights | File | License |
|------|--------|---------|------|---------|
| Heading | **Fraunces** | 400, 600 | `assets/fonts/fraunces-latin-400.woff2`, `fraunces-latin-600.woff2` | SIL Open Font License 1.1 (`assets/fonts/OFL.txt`) |
| Body | **Work Sans** | 400, 600 | `assets/fonts/worksans-latin-400.woff2`, `worksans-latin-600.woff2` | SIL Open Font License 1.1 (`assets/fonts/OFL.txt`) |
Primary pick: Fraunces gives the organic, slightly wobbly display voice the persona wants;
Work Sans keeps body copy warm but legible for donation and report pages. Both
are OFL, subset to the Latin range to stay inside the marketplace size gate.
- **Fraunces** — Source: `google:Fraunces`. ## Fallback stacks (used before/without admin fonts)
Open the Typography panel, switch to the Google Fonts tab, search
"Fraunces", click Add, then assign to Heading. (OFL, free to host.)
Spec-preferred display face (commercial): - Heading: `var(--font-heading, "Fraunces", "Playfair Display", Georgia, serif)`
- Body: `var(--font-body, "Work Sans", ui-sans-serif, system-ui, sans-serif)`
- Mono: `var(--font-mono, "JetBrains Mono", ui-monospace, monospace)`
- **Recoleta** — Source: `upload:Recoleta`. ## Optional admin choices
Recoleta is a commercial face from Latinotype. Purchase the desktop +
webfont licence, upload the woff2 in the Fonts panel, then assign to
Heading. Until then Fraunces is the fallback used by the theme CSS.
## Body — assign to `Body` - **Mono** is not bundled. If your content uses monospace (data captions, code),
add **JetBrains Mono** (`google:JetBrains Mono`, OFL) from the Typography panel
- **Spectral** — Source: `google:Spectral`. Google Fonts tab and assign it to Mono. Until then the system mono stack applies.
Open the Typography panel, Google Fonts tab, search "Spectral", - **Alegreya** (`google:Alegreya`, OFL) is a good serif alternative for Body if you
click Add, assign to Body. (OFL, free to host.) want a more literary, long-read feel for field journals. Assign it to Body to swap.
## Mono / data captions — assign to `Mono`
- **JetBrains Mono** — Source: `google:JetBrains Mono`.
Open the Typography panel, Google Fonts tab, search "JetBrains Mono",
click Add, assign to Mono. (OFL, free to host.)
## Notes ## Notes
- All three Google-Font picks are part of the curated picker list. - Do not hardcode a `font-family` anywhere. Every family flows through the CSS
No manual upload required for the OFL families. variables so the admin font picker keeps working.
- A future build pass may bundle Recoleta woff2s directly via a - Bundled woff2 files are subset to Latin. Extend the subset only if you serve
populated `fonts.json` once the licence is confirmed. non-Latin content, and keep the total artifact within the registry size gate.
- The CSS fallback stack inside the theme (`"Fraunces", "Playfair Display",
Georgia, serif` for headings; `"Spectral", Georgia, serif` for body)
keeps the moss/clay character visible during the cold-load window.

23
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,23 @@
# Earthen — icon packs
Earthen renders icons through the sprite system only
(`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`, or the `::pack:name:size::`
shorthand in templates). It does **not** inline icon SVGs. The theme's botanical
line-art (fern, seed, root motifs) is decorative illustration, not icons, and is
drawn inline on purpose.
## Required pack
- **Lucide** — declared in `plugin.mod` `required_icon_packs = ["lucide"]`.
Lucide ships bundled with the CMS, so no action is normally needed. If it is
missing on your instance, open `/admin/icons`, install **Lucide**, and enable it.
Block schemas that expose an icon picker default to Lucide names, so the theme
renders correctly out of the box. When the loader begins honoring
`required_icon_packs`, Lucide will be installed automatically on activation.
## Optional
No other packs are required. If you add icons from another pack in block content,
install that pack via `/admin/icons` first, then reference it as `"<pack>:<name>"`
in the block's icon field.

111
assets/fonts/OFL.txt Normal file
View File

@ -0,0 +1,111 @@
Bundled fonts — SIL Open Font License 1.1
=========================================
This directory bundles latin-subset woff2 files for three families, all
licensed under the SIL Open Font License, Version 1.1 (OFL-1.1).
Copyright notices
-----------------
Fraunces: Copyright 2018 The Fraunces Project Authors (https://github.com/undercasetype/Fraunces)
Work Sans: Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans)
Caveat: Copyright 2019 The Caveat Project Authors (https://github.com/googlefonts/caveat)
Files
-----
fraunces-latin-400.woff2, fraunces-latin-600.woff2 — Fraunces (heading)
worksans-latin-400.woff2, worksans-latin-600.woff2 — Work Sans (body)
caveat-latin-400.woff2, caveat-latin-700.woff2 — Caveat (hand-drawn accents)
Full license text (SIL OFL 1.1)
-------------------------------
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -1,5 +1,9 @@
# Earthen fonts # Earthen fonts
Reserved for woff2 bundles in a future build pass. The wave-1 build ships Bundled woff2 faces, served as `source=template` rows via `../../fonts.json`:
`fonts.json = []` per the FONTS.md policy; admins assign Fraunces /
Spectral / JetBrains Mono through the typography picker. - **Fraunces** 400/600 (`fraunces-latin-400.woff2`, `fraunces-latin-600.woff2`) — heading.
- **Work Sans** 400/600 (`worksans-latin-400.woff2`, `worksans-latin-600.woff2`) — body.
Both are SIL Open Font License 1.1 (see `OFL.txt`), subset to Latin. See
`../../RECOMMENDED_FONTS.md` for admin assignment and the mono option.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -102,9 +102,9 @@
color: hsl(var(--muted-foreground)); color: hsl(var(--muted-foreground));
} }
/* Ensure article main column uses the Spectral body fallback. */ /* Ensure article main column uses the Work Sans body fallback. */
.earthen-article { .earthen-article {
font-family: var(--font-body, "Spectral", Georgia, serif); font-family: var(--font-body, "Work Sans", ui-sans-serif, system-ui, sans-serif);
color: hsl(var(--foreground)); color: hsl(var(--foreground));
} }
@ -112,3 +112,69 @@
.earthen-glyph { .earthen-glyph {
color: hsl(var(--primary)); color: hsl(var(--primary));
} }
/* Organic blob soft asymmetric corners for cards, media frames and hero art.
The signature earthen shape: hand-crafted, never a hard rectangle. */
.earthen-blob {
border-radius: 42% 58% 62% 38% / 42% 44% 56% 58%;
}
.earthen-blob-soft {
border-radius: 1.75rem 2.25rem 1.75rem 2.25rem;
}
/* Soft card / panel frame — cream surface with a hairline botanical border. */
.earthen-panel {
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.35);
}
/* Terracotta seedling button — the primary call to action across the theme. */
.earthen-cta {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
border-radius: 0.75rem;
transition: box-shadow 200ms ease, transform 120ms ease;
}
.earthen-cta:hover,
.earthen-cta:focus-visible {
box-shadow: 0 6px 0 -2px hsl(var(--accent) / 0.35);
transform: translateY(-1px);
}
.earthen-cta:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* Sage link — dotted botanical underline that fills on hover. */
.earthen-link {
color: hsl(var(--primary));
text-decoration: none;
border-bottom: 1px dotted hsl(var(--primary) / 0.6);
transition: color 150ms ease, border-color 150ms ease;
}
.earthen-link:hover,
.earthen-link:focus-visible {
color: hsl(var(--accent));
border-bottom-color: hsl(var(--accent));
}
/* Leaf line-draw — a hairline that draws in on scroll where motion is allowed. */
@media (prefers-reduced-motion: no-preference) {
.earthen-draw {
stroke-dasharray: 240;
stroke-dashoffset: 240;
animation: earthen-draw-in 1.4s ease forwards;
}
@keyframes earthen-draw-in {
to { stroke-dashoffset: 0; }
}
.earthen-blob-morph {
animation: earthen-blob-morph 18s ease-in-out infinite;
}
@keyframes earthen-blob-morph {
0%, 100% { border-radius: 42% 58% 62% 38% / 42% 44% 56% 58%; }
50% { border-radius: 58% 42% 38% 62% / 56% 58% 42% 44%; }
}
}

View File

@ -1,42 +1,26 @@
# Earthen theme block definitions (codeless .bnp, WO-WZ-021). # Earthen theme block definitions (codeless .bnp, WO-WZ-021 / WO-TF-016).
# Keys are fully qualified (earthen:<key>) because the codeless loader registers # Only TRUE persona furniture remains as theme blocks. Everything that
# definitions verbatim — there is no PluginBlockRegistry prefixing in the # duplicated a builtin (footer, stats/impact-metrics, logo-strip/partner-logos,
# codeless path (unlike the wasm/templ path). The qualified keys match the # divider/botanical-divider) is now a template OVERRIDE of the builtin key
# hardcoded data-block attributes and the master_pages.json references. # (templates/overrides/earthen/). Keys are fully qualified (earthen:<key>) — the
# codeless loader registers definitions verbatim; the qualified keys match the
# data-block attributes, master_pages.json and seed references.
blocks: blocks:
- key: earthen:donation_cta - key: earthen:donation_cta
title: Donation CTA title: Donation CTA
description: Donation call-to-action with preset amount tiles and custom amount field. description: Donation appeal with preset amount tiles and a custom amount field.
category: content category: content
schema: donation_cta.schema.json schema: donation_cta.schema.json
template: donation_cta.ninjatpl template: donation_cta.ninjatpl
- key: earthen:impact_metrics
title: Impact Metrics
description: Numerical impact metrics with optional botanical illustration.
category: content
schema: impact_metrics.schema.json
template: impact_metrics.ninjatpl
- key: earthen:partner_logos
title: Partner Logos
description: Grayscale partner logo grid with hover-to-color treatment.
category: content
schema: partner_logos.schema.json
template: partner_logos.ninjatpl
- key: earthen:field_note - key: earthen:field_note
title: Field Note title: Field Note
description: Article-style dispatch from the field with byline and location. description: Dispatch from the field with byline, location and a lede image.
category: content category: content
schema: field_note.schema.json schema: field_note.schema.json
template: field_note.ninjatpl template: field_note.ninjatpl
- key: earthen:botanical_divider - key: earthen:impact_report
title: Botanical Divider title: Impact Report
description: Hand-illustrated SVG section break in fern, root or seed motif. description: Narrative impact report with a summary lede and outcome rows.
category: layout category: content
schema: botanical_divider.schema.json schema: impact_report.schema.json
template: botanical_divider.ninjatpl template: impact_report.ninjatpl
- key: earthen:footer
title: Site Footer
description: Cream-on-moss footer with optional newsletter and column links.
category: navigation
schema: footer.schema.json
template: footer.ninjatpl

View File

@ -1,5 +0,0 @@
<div data-block="earthen:botanical_divider"{% if not motif %} data-empty{% endif %} class="earthen-divider flex items-center justify-center gap-4 my-12 px-4" aria-hidden="true">
<span class="earthen-divider-rule flex-1 h-px" style="background-color: hsl(var(--border));"></span>
{% if motif == "root" %}<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" data-motif="root" class="earthen-glyph" style="color: hsl(var(--primary));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M24 4v18"></path><path d="M24 22c-4 4-9 5-13 4M24 22c4 4 9 5 13 4M24 22c-2 7-3 12-5 16M24 22c2 7 3 12 5 16M24 22c0 9-1 15-1 20"></path><circle cx="24" cy="22" r="2"></circle></svg>{% elif motif == "seed" %}<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" data-motif="seed" class="earthen-glyph" style="color: hsl(var(--accent));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="24" cy="24" rx="6" ry="10"></ellipse><path d="M24 14V6M18 18l-6-6M30 18l6-6M24 34v8M18 30l-6 6M30 30l6 6"></path></svg>{% else %}<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" data-motif="fern" class="earthen-glyph" style="color: hsl(var(--primary));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>{% endif %}
<span class="earthen-divider-rule flex-1 h-px" style="background-color: hsl(var(--border));"></span>
</div>

View File

@ -1,16 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Botanical Divider",
"description": "SVG section break with hand-illustrated botanical motif",
"type": "object",
"properties": {
"motif": {
"type": "string",
"title": "Motif",
"description": "Botanical illustration used for the divider",
"x-editor": "select",
"enum": ["fern", "root", "seed"],
"default": "fern"
}
}
}

View File

@ -1,7 +1,7 @@
<section data-block="earthen:donation_cta"{% if not headline and not body and not amounts and not buttonLabel and not processorUrl %} data-empty{% endif %} class="earthen-donation-cta py-16 px-4" style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground));"> <section data-block="earthen:donation_cta"{% if not headline and not body and not amounts and not buttonLabel and not processorUrl %} data-empty{% endif %} class="earthen-donation-cta py-16 px-4" style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground));">
<div class="max-w-3xl mx-auto text-center"> <div class="max-w-3xl mx-auto text-center">
{% if headline %}<h2 class="earthen-display text-3xl md:text-4xl mb-4" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--primary));">{{ headline }}</h2>{% endif %} {% if headline %}<h2 class="earthen-display text-3xl md:text-4xl mb-4" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--primary));">{{ headline }}</h2>{% endif %}
{% if body %}<div class="earthen-body text-lg mb-8 mx-auto max-w-xl" style="font-family: var(--font-body, \000022Spectral\000022, Georgia, serif); color: hsl(var(--foreground));">{{ body|safe }}</div>{% endif %} {% if body %}<div class="earthen-body text-lg mb-8 mx-auto max-w-xl" style="font-family: var(--font-body, \000022Work Sans\000022, ui-sans-serif, system-ui, sans-serif); color: hsl(var(--foreground));">{{ body|safe }}</div>{% endif %}
<form action="{{ processorUrl|default:"#" }}" method="get" class="space-y-6"> <form action="{{ processorUrl|default:"#" }}" method="get" class="space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3" role="radiogroup" aria-label="Preset donation amounts"> <div class="grid grid-cols-1 sm:grid-cols-3 gap-3" role="radiogroup" aria-label="Preset donation amounts">
{% if amounts %}{% for a in amounts %}<label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="{{ a }}" class="sr-only"{% if forloop.First %} checked{% endif %}><span>${{ a }}</span></label>{% endfor %}{% else %}<label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="25" class="sr-only" checked><span>$25</span></label><label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="50" class="sr-only"><span>$50</span></label><label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="100" class="sr-only"><span>$100</span></label>{% endif %} {% if amounts %}{% for a in amounts %}<label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="{{ a }}" class="sr-only"{% if forloop.First %} checked{% endif %}><span>${{ a }}</span></label>{% endfor %}{% else %}<label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="25" class="sr-only" checked><span>$25</span></label><label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="50" class="sr-only"><span>$50</span></label><label data-amount-tile class="earthen-tile flex items-center justify-center cursor-pointer rounded-lg py-4 px-3 text-lg font-semibold transition-colors" style="border: 1px solid hsl(var(--border)); background-color: hsl(var(--background)); color: hsl(var(--foreground));"><input type="radio" name="amount" value="100" class="sr-only"><span>$100</span></label>{% endif %}

View File

@ -3,6 +3,6 @@
<div class="earthen-byline flex flex-wrap gap-x-3 gap-y-1 uppercase tracking-wider text-xs mb-2" style="color: hsl(var(--muted-foreground));">{% if author %}<span class="earthen-byline-author">By {{ author }}</span>{% endif %}{% if location %}<span class="earthen-byline-location">{{ location }}</span>{% endif %}{% if dateline %}<span class="earthen-byline-date">{{ dateline }}</span>{% endif %}</div> <div class="earthen-byline flex flex-wrap gap-x-3 gap-y-1 uppercase tracking-wider text-xs mb-2" style="color: hsl(var(--muted-foreground));">{% if author %}<span class="earthen-byline-author">By {{ author }}</span>{% endif %}{% if location %}<span class="earthen-byline-location">{{ location }}</span>{% endif %}{% if dateline %}<span class="earthen-byline-date">{{ dateline }}</span>{% endif %}</div>
<div class="botanical-rule" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="28" height="28" data-motif="seed" class="earthen-glyph" style="color: hsl(var(--accent));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="24" cy="24" rx="6" ry="10"></ellipse><path d="M24 14V6M18 18l-6-6M30 18l6-6M24 34v8M18 30l-6 6M30 30l6 6"></path></svg></div> <div class="botanical-rule" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="28" height="28" data-motif="seed" class="earthen-glyph" style="color: hsl(var(--accent));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="24" cy="24" rx="6" ry="10"></ellipse><path d="M24 14V6M18 18l-6-6M30 18l6-6M24 34v8M18 30l-6 6M30 30l6 6"></path></svg></div>
</header> </header>
{% if image %}<figure class="my-6 earthen-paper-frame-inner"><img src="{{ image }}" alt="" class="w-full h-auto rounded"></figure>{% endif %} {% if image %}<figure class="my-6 earthen-paper-frame-inner overflow-hidden earthen-blob-soft">{% img image alt="" class="w-full h-auto" %}</figure>{% endif %}
{% if body %}<div class="earthen-body drop-cap prose-base" style="font-family: var(--font-body, \000022Spectral\000022, Georgia, serif); color: hsl(var(--foreground));">{{ body|safe }}</div>{% else %}<p class="earthen-empty" style="color: hsl(var(--muted-foreground));">No dispatch yet — add a body to share what you saw in the field.</p>{% endif %} {% if body %}<div class="earthen-body drop-cap prose-base" style="font-family: var(--font-body, \000022Work Sans\000022, ui-sans-serif, system-ui, sans-serif); color: hsl(var(--foreground));">{{ body|safe }}</div>{% else %}<p class="earthen-empty" style="color: hsl(var(--muted-foreground));">No dispatch yet — add a body to share what you saw in the field.</p>{% endif %}
</article> </article>

View File

@ -1,17 +0,0 @@
<div data-block="earthen:footer"{% if not showNewsletter and not tagline and not columns %} data-empty{% endif %} class="earthen-footer w-full" style="background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground));">
<div class="max-w-6xl mx-auto px-6 py-16">
<div class="botanical-rule flex items-center gap-4 mb-10" aria-hidden="true">
<span class="flex-1 h-px" style="background-color: hsl(var(--primary-foreground) / 0.3);"></span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" data-motif="fern" class="earthen-glyph" style="color: hsl(var(--primary));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
<span class="flex-1 h-px" style="background-color: hsl(var(--primary-foreground) / 0.3);"></span>
</div>
{% if tagline %}<p class="earthen-display text-2xl md:text-3xl text-center mb-10" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif);">{{ tagline }}</p>{% endif %}
{% if showNewsletter %}<form class="max-w-xl mx-auto mb-12 flex flex-col sm:flex-row gap-3">
<label class="sr-only" for="earthen-footer-email">Email address</label>
<input id="earthen-footer-email" type="email" placeholder="you@example.org" class="flex-1 rounded-lg px-4 py-3 text-base" style="background-color: hsl(var(--primary-foreground) / 0.1); color: hsl(var(--primary-foreground)); border: 1px solid hsl(var(--primary-foreground) / 0.3);">
<button type="submit" class="rounded-lg px-6 py-3 font-semibold" style="background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground));">Subscribe</button>
</form>{% endif %}
{% if columns %}<div class="grid gap-8 mb-10 {% if columns|length == 1 %}grid-cols-1{% elif columns|length == 2 %}grid-cols-1 sm:grid-cols-2{% elif columns|length == 3 %}grid-cols-1 sm:grid-cols-3{% else %}grid-cols-2 lg:grid-cols-4{% endif %}">{% for col in columns %}<div>{% if col.title %}<h4 class="earthen-display text-lg mb-4" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif);">{{ col.title }}</h4>{% endif %}{% if col.links %}<ul class="space-y-2">{% for link in col.links %}<li><a href="{{ link.url|default:"#" }}" class="earthen-footer-link transition-colors" style="color: hsl(var(--primary-foreground) / 0.85);">{{ link.text }}</a></li>{% endfor %}</ul>{% endif %}</div>{% endfor %}</div>{% endif %}
<div class="text-center text-sm pt-6" style="border-top: 1px solid hsl(var(--primary-foreground) / 0.2); color: hsl(var(--primary-foreground) / 0.7);"><span>Rooted in place. Made with care.</span></div>
</div>
</div>

View File

@ -1,57 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Site Footer",
"description": "Cream-on-moss footer with optional newsletter and botanical rule",
"type": "object",
"properties": {
"showNewsletter": {
"type": "boolean",
"title": "Show Newsletter",
"description": "Show the newsletter signup form",
"x-editor": "select",
"default": true
},
"tagline": {
"type": "string",
"title": "Tagline",
"description": "Tagline above the columns",
"x-editor": "text"
},
"columns": {
"type": "array",
"title": "Columns",
"description": "Footer link columns",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Column Title",
"x-editor": "text"
},
"links": {
"type": "array",
"title": "Links",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "Text",
"x-editor": "text"
},
"url": {
"type": "string",
"title": "URL",
"x-editor": "link"
}
}
}
}
}
}
}
}
}

View File

@ -1,7 +0,0 @@
<section data-block="earthen:impact_metrics"{% if not title and not metrics and not illustration %} data-empty{% endif %} class="earthen-impact-metrics py-16 px-4" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
<div class="max-w-5xl mx-auto">
{% if title %}<h2 class="earthen-display text-3xl md:text-4xl text-center mb-12" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--primary));">{{ title }}</h2>{% endif %}
{% if illustration %}<div class="flex justify-center mb-8"><img src="{{ illustration }}" alt="" class="max-h-40 object-contain"></div>{% elif title or metrics %}<div class="flex justify-center mb-8"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="64" height="64" data-motif="fern" class="earthen-glyph" style="color: hsl(var(--primary));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg></div>{% endif %}
{% if metrics %}<div class="grid gap-8 {% if metrics|length == 1 %}grid-cols-1{% elif metrics|length == 2 %}grid-cols-1 sm:grid-cols-2{% elif metrics|length == 3 %}grid-cols-1 sm:grid-cols-3{% else %}grid-cols-2 lg:grid-cols-4{% endif %}">{% for m in metrics %}<div class="text-center px-4"><div class="earthen-display text-5xl font-semibold mb-2" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--accent));">{{ m.value }}{{ m.suffix }}</div><div class="earthen-body uppercase tracking-wider text-sm" style="font-family: var(--font-body, \000022Spectral\000022, Georgia, serif); color: hsl(var(--muted-foreground));">{{ m.label }}</div></div>{% endfor %}</div>{% else %}<p class="text-center" style="color: hsl(var(--muted-foreground));">Add impact metrics to bring this section to life.</p>{% endif %}
</div>
</section>

View File

@ -1,49 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Impact Metrics",
"description": "Numerical impact metrics with optional illustration",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Section Title",
"description": "Heading above the metric tiles",
"x-editor": "text"
},
"metrics": {
"type": "array",
"title": "Metrics",
"description": "List of impact metrics",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string",
"title": "Value",
"description": "Numeric value (e.g. '12,000')",
"x-editor": "text"
},
"label": {
"type": "string",
"title": "Label",
"description": "Description of the metric",
"x-editor": "text"
},
"suffix": {
"type": "string",
"title": "Suffix",
"description": "Optional suffix (e.g. '+', '%')",
"x-editor": "text"
}
}
}
},
"illustration": {
"type": "string",
"title": "Illustration",
"description": "Optional botanical illustration shown alongside metrics",
"x-editor": "media"
}
}
}

View File

@ -0,0 +1,14 @@
<section data-block="earthen:impact_report"{% if not title and not summary and not outcomes and not period %} data-empty{% endif %} class="earthen-impact-report bg-paper-grain py-16 px-4" style="color: hsl(var(--foreground));">
<div class="earthen-panel earthen-blob-soft mx-auto max-w-4xl p-8 md:p-12">
<div class="mb-8 flex items-start gap-4">
<span class="mt-1 shrink-0" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="40" height="40" data-motif="fern" class="earthen-glyph" style="color: hsl(var(--primary));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg></span>
<div>
{% if period %}<p class="mb-1 text-xs uppercase tracking-widest" style="color: hsl(var(--accent)); font-family: var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ period }}</p>{% endif %}
{% if title %}<h2 class="crayon-underline text-3xl md:text-4xl" style="font-family: var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif); color: hsl(var(--primary));">{{ title }}</h2>{% endif %}
</div>
</div>
{% if summary %}<div class="mb-10 max-w-2xl text-lg leading-relaxed" style="font-family: var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif); color: hsl(var(--foreground));">{{ summary|safe }}</div>{% endif %}
{% if outcomes %}<dl class="divide-y" style="border-color: hsl(var(--border));">{% for o in outcomes %}<div class="flex flex-col gap-1 py-5 sm:flex-row sm:items-baseline sm:gap-6" style="border-color: hsl(var(--border));"><dt class="shrink-0 text-4xl font-semibold sm:w-48" style="font-family: var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif); color: hsl(var(--accent));">{{ o.value }}</dt><dd class="flex-1"><p class="text-base font-semibold" style="color: hsl(var(--foreground));">{{ o.label }}</p>{% if o.detail %}<p class="mt-1 text-sm" style="color: hsl(var(--muted-foreground));">{{ o.detail }}</p>{% endif %}</dd></div>{% endfor %}</dl>{% else %}<p class="text-center" style="color: hsl(var(--muted-foreground));">Add outcomes to show what this season grew.</p>{% endif %}
{% if sourceNote %}<p class="mt-8 text-xs italic" style="color: hsl(var(--muted-foreground));">{{ sourceNote }}</p>{% endif %}
</div>
</section>

View File

@ -0,0 +1,62 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Impact Report",
"description": "Narrative impact report with a summary lede and a list of outcome rows",
"type": "object",
"properties": {
"period": {
"type": "string",
"title": "Period",
"description": "Reporting period label (e.g. 'Season 2025')",
"x-editor": "text"
},
"title": {
"type": "string",
"title": "Title",
"description": "Report heading",
"x-editor": "text",
"default": "What we grew together"
},
"summary": {
"type": "string",
"title": "Summary",
"description": "Opening paragraph of the report",
"x-editor": "richtext"
},
"outcomes": {
"type": "array",
"title": "Outcomes",
"description": "Reported outcomes shown as rows",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string",
"title": "Value",
"description": "Headline figure or result (e.g. '840 acres')",
"x-editor": "text"
},
"label": {
"type": "string",
"title": "Label",
"description": "What the figure counts",
"x-editor": "text"
},
"detail": {
"type": "string",
"title": "Detail",
"description": "One line of context",
"x-editor": "text"
}
}
}
},
"sourceNote": {
"type": "string",
"title": "Source Note",
"description": "Footnote crediting the source of the figures",
"x-editor": "text"
}
}
}

View File

@ -1,6 +0,0 @@
<section data-block="earthen:partner_logos"{% if not title and not logos %} data-empty{% endif %} class="earthen-partner-logos py-16 px-4" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
<div class="max-w-5xl mx-auto">
{% if title %}<h2 class="earthen-display text-2xl md:text-3xl text-center mb-10" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--primary));">{{ title }}</h2>{% endif %}
{% if logos %}<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-8 items-center">{% for logo in logos %}<div class="earthen-partner-tile flex justify-center items-center">{% if logo.url %}<a href="{{ logo.url }}" class="earthen-partner-link block transition-colors" title="{{ logo.name }}" aria-label="{{ logo.name }}">{% if logo.image %}<img src="{{ logo.image }}" alt="{{ logo.name }}" class="earthen-partner-img max-h-12 object-contain w-full">{% else %}<div class="earthen-partner-fallback w-full h-12 flex items-center justify-center rounded" style="background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground));"><span class="text-sm">{% if logo.name %}{% for w in logo.name|split:" " %}{% if forloop.Counter <= 2 %}{{ w|first }}{% endif %}{% endfor %}{% else %}?{% endif %}</span></div>{% endif %}</a>{% else %}<div class="earthen-partner-link block" title="{{ logo.name }}" aria-label="{{ logo.name }}">{% if logo.image %}<img src="{{ logo.image }}" alt="{{ logo.name }}" class="earthen-partner-img max-h-12 object-contain w-full">{% else %}<div class="earthen-partner-fallback w-full h-12 flex items-center justify-center rounded" style="background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground));"><span class="text-sm">{% if logo.name %}{% for w in logo.name|split:" " %}{% if forloop.Counter <= 2 %}{{ w|first }}{% endif %}{% endfor %}{% else %}?{% endif %}</span></div>{% endif %}</div>{% endif %}</div>{% endfor %}</div>{% else %}<p class="text-center" style="color: hsl(var(--muted-foreground));">Add partner logos to fill this grid.</p>{% endif %}
</div>
</section>

View File

@ -1,40 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Partner Logos",
"description": "Grayscale partner logo grid with hover-to-color treatment",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Section Title",
"description": "Heading above the logo grid",
"x-editor": "text"
},
"logos": {
"type": "array",
"title": "Logos",
"description": "Partner logos",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Partner Name",
"x-editor": "text"
},
"image": {
"type": "string",
"title": "Logo Image",
"x-editor": "media"
},
"url": {
"type": "string",
"title": "Partner URL",
"x-editor": "link"
}
}
}
}
}
}

View File

@ -1 +1,18 @@
[] [
{
"name": "Fraunces",
"family": "Fraunces",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/fraunces-latin-400.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/fraunces-latin-600.woff2" }
]
},
{
"name": "Work Sans",
"family": "Work Sans",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/worksans-latin-400.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/worksans-latin-600.woff2" }
]
}
]

View File

@ -3,6 +3,7 @@
theme_presets: presets.json theme_presets: presets.json
bundled_fonts: fonts.json bundled_fonts: fonts.json
master_pages: master_pages.json master_pages: master_pages.json
required_icon_packs: [lucide]
system_templates: system_templates:
- key: earthen - key: earthen
@ -30,13 +31,79 @@ page_templates:
title: Full Width title: Full Width
description: "Edge-to-edge imagery for reports and galleries" description: "Edge-to-edge imagery for reports and galleries"
slots: [header, main, footer] slots: [header, main, footer]
- system: earthen
key: blog-index
title: Field Journal
description: "Blog index with a botanical masthead, featured and post-grid slots"
slots: [header, featured, main, footer]
- system: earthen
key: contact
title: Get in Touch
description: "Contact layout with an optional aside for hours and location"
slots: [header, main, aside, footer]
- system: earthen
key: auth
title: Members
description: "Centred auth shell with wordmark and a fern crest"
slots: [main, footer]
template_overrides: template_overrides:
# Chrome (4)
- { template: earthen, block: navbar }
- { template: earthen, block: footer }
- { template: earthen, block: utility-bar }
- { template: earthen, block: announcement-bar }
# Marketing (12)
- { template: earthen, block: hero }
- { template: earthen, block: feature-grid }
- { template: earthen, block: rich-section }
- { template: earthen, block: stats }
- { template: earthen, block: testimonial }
- { template: earthen, block: logo-strip }
- { template: earthen, block: pricing }
- { template: earthen, block: team }
- { template: earthen, block: faq }
- { template: earthen, block: timeline }
- { template: earthen, block: cta }
- { template: earthen, block: countdown }
# Primitives (11)
- { template: earthen, block: heading } - { template: earthen, block: heading }
- { template: earthen, block: text } - { template: earthen, block: text }
- { template: earthen, block: button } - { template: earthen, block: rich-text }
- { template: earthen, block: image } - { template: earthen, block: image }
- { template: earthen, block: quote }
- { template: earthen, block: divider }
- { template: earthen, block: button }
- { template: earthen, block: card } - { template: earthen, block: card }
- { template: earthen, block: gallery }
- { template: earthen, block: video-embed }
- { template: earthen, block: audio-embed }
# Commerce / local (6)
- { template: earthen, block: menu-list }
- { template: earthen, block: event-list }
- { template: earthen, block: contact-form }
- { template: earthen, block: contact-card }
- { template: earthen, block: hours-location }
- { template: earthen, block: social-links }
# Blog family (12)
- { template: earthen, block: blog-hero }
- { template: earthen, block: featured-posts }
- { template: earthen, block: related-posts }
- { template: earthen, block: popular-posts }
- { template: earthen, block: category-list }
- { template: earthen, block: post-hero }
- { template: earthen, block: post-metadata }
- { template: earthen, block: author-bio }
- { template: earthen, block: author-bio-hero }
- { template: earthen, block: newsletter-signup }
- { template: earthen, block: breadcrumbs }
- { template: earthen, block: sidebar-nav }
# Auth surface (3)
- { template: earthen, block: auth-form }
- { template: earthen, block: password-reset-form }
- { template: earthen, block: auth-status }
# System (1)
- { template: earthen, block: page-suggestions }
email_wrappers: email_wrappers:
- earthen - earthen

View File

@ -2,24 +2,31 @@
{ {
"key": "earthen:default-master", "key": "earthen:default-master",
"title": "Earthen Default Master", "title": "Earthen Default Master",
"page_templates": ["default", "article"], "page_templates": ["default", "article", "contact", "full-width", "blog-index"],
"blocks": [ "blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 }, { "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }, { "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "earthen:donation_cta", "title": "Donation Rail", "content": { "amounts": [25, 50, 100], "headline": "Plant the next acre." }, "slot": "main", "sort_order": 100 }, { "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showNewsletter": true, "newsletterTitle": "Field notes in your inbox", "companyName": "Rootline Trust", "tagline": "Rooted in place. We restore land with the people who live on it.", "menuName": "main" }, "slot": "footer", "sort_order": 0 }
{ "block_key": "earthen:footer", "title": "Site Footer", "content": { "showNewsletter": true, "tagline": "Rooted in place." }, "slot": "footer", "sort_order": 0 }
] ]
}, },
{ {
"key": "earthen:landing-master", "key": "earthen:landing-master",
"title": "Earthen Landing Master", "title": "Earthen Landing Master",
"page_templates": ["landing", "full-width"], "page_templates": ["landing"],
"blocks": [ "blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "hero", "sort_order": 0 }, { "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "hero", "sort_order": 0 },
{ "block_key": "slot", "title": "Hero Slot", "content": { "slotName": "hero" }, "slot": "hero", "sort_order": 10 }, { "block_key": "slot", "title": "Hero Slot", "content": { "slotName": "hero" }, "slot": "hero", "sort_order": 10 },
{ "block_key": "slot", "title": "Main Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }, { "block_key": "slot", "title": "Main Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "earthen:donation_cta", "title": "Donation CTA", "content": { "amounts": [25, 50, 100, 250] }, "slot": "cta", "sort_order": 0 }, { "block_key": "earthen:donation_cta", "title": "Donation Appeal", "content": { "headline": "Plant the next acre.", "amounts": [25, 50, 100, 250] }, "slot": "cta", "sort_order": 0 },
{ "block_key": "earthen:footer", "title": "Footer", "content": { "showNewsletter": true }, "slot": "footer", "sort_order": 0 } { "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showNewsletter": true, "newsletterTitle": "Field notes in your inbox", "companyName": "Rootline Trust", "tagline": "Rooted in place. We restore land with the people who live on it.", "menuName": "main" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "earthen:auth-master",
"title": "Earthen Auth Master",
"page_templates": ["auth"],
"blocks": [
{ "block_key": "slot", "title": "Auth Form Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }
] ]
} }
] ]

View File

@ -7,3 +7,4 @@ description = "Botanical nonprofit theme in moss, clay, sage and cream — hand-
kind = "theme" kind = "theme"
categories = ["templates"] categories = ["templates"]
tags = ["earth", "nonprofit", "sustainability", "outdoor", "farm", "cause", "botanical", "conservation", "ngo"] tags = ["earth", "nonprofit", "sustainability", "outdoor", "farm", "cause", "botanical", "conservation", "ngo"]
required_icon_packs = ["lucide"]

View File

@ -106,5 +106,105 @@
"ring": "80 35% 55%" "ring": "80 35% 55%"
} }
} }
},
{
"id": "sage-morning",
"name": "Sage Morning",
"description": "Pale sage paper and honey light, misty forest dark — a softer, hopeful reading of the palette.",
"theme": {
"mode": "both",
"lightColors": {
"background": "70 25% 95%",
"foreground": "130 20% 16%",
"card": "70 22% 98%",
"cardForeground": "130 20% 16%",
"popover": "70 22% 98%",
"popoverForeground": "130 20% 16%",
"primary": "140 25% 34%",
"primaryForeground": "70 25% 96%",
"secondary": "75 18% 87%",
"secondaryForeground": "130 22% 20%",
"muted": "70 16% 90%",
"mutedForeground": "110 10% 40%",
"accent": "38 62% 48%",
"accentForeground": "40 30% 97%",
"destructive": "8 70% 45%",
"destructiveForeground": "40 30% 96%",
"border": "90 12% 80%",
"input": "90 12% 80%",
"ring": "140 25% 34%"
},
"darkColors": {
"background": "140 15% 9%",
"foreground": "70 18% 90%",
"card": "140 15% 12%",
"cardForeground": "70 18% 90%",
"popover": "140 15% 12%",
"popoverForeground": "70 18% 90%",
"primary": "95 28% 58%",
"primaryForeground": "140 15% 9%",
"secondary": "140 12% 18%",
"secondaryForeground": "70 18% 90%",
"muted": "140 10% 15%",
"mutedForeground": "90 10% 58%",
"accent": "40 65% 56%",
"accentForeground": "140 15% 9%",
"destructive": "8 70% 50%",
"destructiveForeground": "70 18% 90%",
"border": "140 10% 22%",
"input": "140 10% 22%",
"ring": "95 28% 58%"
}
}
},
{
"id": "ember-loam",
"name": "Ember Loam",
"description": "Terracotta-led warm clay in light, firelight ember over deep loam in dark — moss becomes the accent.",
"theme": {
"mode": "both",
"lightColors": {
"background": "28 38% 95%",
"foreground": "18 40% 15%",
"card": "28 32% 98%",
"cardForeground": "18 40% 15%",
"popover": "28 32% 98%",
"popoverForeground": "18 40% 15%",
"primary": "14 62% 44%",
"primaryForeground": "28 38% 96%",
"secondary": "32 26% 88%",
"secondaryForeground": "18 40% 16%",
"muted": "28 22% 91%",
"mutedForeground": "22 20% 38%",
"accent": "95 24% 34%",
"accentForeground": "28 38% 96%",
"destructive": "6 72% 44%",
"destructiveForeground": "28 38% 96%",
"border": "28 18% 81%",
"input": "28 18% 81%",
"ring": "14 62% 44%"
},
"darkColors": {
"background": "20 22% 7%",
"foreground": "35 25% 90%",
"card": "20 22% 10%",
"cardForeground": "35 25% 90%",
"popover": "20 22% 10%",
"popoverForeground": "35 25% 90%",
"primary": "22 72% 56%",
"primaryForeground": "20 22% 7%",
"secondary": "20 16% 17%",
"secondaryForeground": "35 25% 90%",
"muted": "20 14% 14%",
"mutedForeground": "30 14% 58%",
"accent": "90 30% 52%",
"accentForeground": "20 22% 7%",
"destructive": "6 72% 52%",
"destructiveForeground": "35 25% 90%",
"border": "20 14% 21%",
"input": "20 14% 21%",
"ring": "22 72% 56%"
}
}
} }
] ]

185
seed/seed.json Normal file
View File

@ -0,0 +1,185 @@
{
"demo": true,
"settings": {
"merge": {
"companyName": "Rootline Trust",
"tagline": "We restore land with the people who live on it."
}
},
"data_tables": [
{
"key": "contact_submissions",
"name": "Contact Enquiries",
"description": "Seeded contact-form submissions for the Rootline Trust demo. A submission becomes a row here and emails the site admins.",
"columns": [
{ "key": "name", "label": "Name", "type": "text", "required": true },
{ "key": "email", "label": "Email", "type": "email", "required": true },
{ "key": "region", "label": "Region", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"pages": [
{
"slug": "/",
"title": "Rootline Trust",
"template_key": "landing",
"blocks": [
{
"block_key": "hero",
"title": "Hero",
"slot": "hero",
"sort_order": 1,
"content": {
"template": "centered",
"eyebrow": "Land restoration",
"headline": "Bring the land back to life.",
"subheadline": "We restore land with the people who live on it.",
"description": "Rootline Trust plants native cover, mends waterways and hands care of the land back to local hands. Small work, done every season, in places that need it.",
"ctaPrimary": { "text": "Donate", "url": "/contact" },
"ctaSecondary": { "text": "Read a field note", "url": "/blog" }
}
},
{
"block_key": "earthen:impact_report",
"title": "Impact Report",
"slot": "main",
"sort_order": 1,
"content": {
"period": "Season 2025",
"title": "What we grew together",
"summary": "<p>Every acre here started as a pledge. This is where those pledges went last season. The figures below are example figures for this demo site.</p>",
"outcomes": [
{ "value": "840 acres", "label": "Native cover planted", "detail": "Across twelve restoration sites." },
{ "value": "19 km", "label": "Creek bank mended", "detail": "Fenced, replanted and handed to local carers." },
{ "value": "1,200", "label": "Volunteer days", "detail": "Given by people who live near the work." }
],
"sourceNote": "Example figures for demonstration."
}
},
{
"block_key": "earthen:field_note",
"title": "Field Note",
"slot": "main",
"sort_order": 2,
"content": {
"author": "Mara Ellison",
"location": "Wirra Creek",
"dateline": "March 2025",
"body": "<p>The first rain after planting is the one that tells you. We walked the creek line at dawn and the tube stock had held. Small green points, thousands of them, holding the bank against the next flood.</p><p>Restoration is slow. You plant, you fence, you wait. Then one morning the land answers back.</p>"
}
}
]
},
{
"slug": "/about",
"title": "Our work",
"template_key": "default",
"blocks": [
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "Our work", "level": 1 } },
{ "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "<p>Rootline Trust began with one degraded paddock and a handful of neighbours who wanted it back. We plant native cover, mend waterways and fence stock out of creek lines. Then we hand the care of the land to the people who live beside it.</p><p>We work slowly and in public. Every site is chosen with local carers. Every season is reported to the people who fund it.</p>" } },
{ "block_key": "divider", "title": "Divider", "slot": "main", "sort_order": 3, "content": { "variant": "ornament", "icon": "lucide:sprout" } },
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 4, "content": { "style": "block", "align": "center", "quote": "You plant, you fence, you wait. Then the land answers back.", "attribution": "Mara Ellison, Field Lead" } }
]
},
{
"slug": "/blog",
"title": "Field Journal",
"template_key": "blog-index",
"blocks": [
{ "block_key": "card", "title": "Post: First Rain", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "The first rain after planting", "text": "<p>What a dawn walk along Wirra Creek tells you about a season of restoration.</p>", "link": "/blog/the-first-rain-after-planting", "linkLabel": "Read the dispatch" } },
{ "block_key": "card", "title": "Post: Fencing", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "Why we fence before we plant", "text": "<p>The least glamorous work protects everything that comes after it.</p>", "link": "/blog/why-we-fence-before-we-plant", "linkLabel": "Read the dispatch" } },
{ "block_key": "card", "title": "Post: Handover", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Handing the land back", "text": "<p>Restoration is not finished when we leave. It is finished when local carers take it on.</p>", "link": "/blog/handing-the-land-back", "linkLabel": "Read the dispatch" } }
]
},
{
"slug": "/blog/the-first-rain-after-planting",
"title": "The first rain after planting",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "The first rain after planting", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>We walked the creek line at dawn and the tube stock had held. Small green points, thousands of them, holding the bank against the next flood.</p><p>The first rain after planting is the one that tells you whether the season will take. This one took. The soil was dark and the roots had gripped.</p><p>Restoration is slow. You plant, you fence, you wait. Then one morning the land answers back.</p>" } }
]
},
{
"slug": "/blog/why-we-fence-before-we-plant",
"title": "Why we fence before we plant",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Why we fence before we plant", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Stock will find new growth before it has a chance to root. So we fence first. It is the least glamorous work we do and it protects everything that comes after.</p><p>A good fence buys a young planting three years of quiet. That is usually enough.</p>" } }
]
},
{
"slug": "/blog/handing-the-land-back",
"title": "Handing the land back",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Handing the land back", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Restoration is not finished when we leave. It is finished when local carers take it on. We plant with them from the first day so the knowledge stays on the land.</p><p>By the third season we step back. The site keeps going because the people beside it keep going.</p>" } }
]
},
{
"slug": "/contact",
"title": "Get in touch",
"template_key": "contact",
"blocks": [
{
"block_key": "contact-form",
"title": "Contact Enquiry",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "Work with us",
"description": "Tell us about your land, your region or how you would like to help. We read every message.",
"submitLabel": "Send message",
"successMessage": "Thank you. Your message has been received. We will reply by email.",
"fields": [
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
{ "name": "region", "label": "Region", "type": "text", "required": false, "width": "half" },
{ "name": "message", "label": "How can we help?", "type": "textarea", "required": true, "width": "full" }
],
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
}
},
{
"block_key": "hours-location",
"title": "Where to find us",
"slot": "aside",
"sort_order": 1,
"content": {
"heading": "Office hours",
"hours": [
{ "day": "Monday to Thursday", "hours": "9am to 5pm" },
{ "day": "Friday", "hours": "9am to 3pm" },
{ "day": "Saturday", "hours": "Field days only" },
{ "day": "Sunday", "closed": true }
],
"specialHoursNote": "Planting days run most weekends in autumn. Check the field journal.",
"address": "3 Wattle Road\nWirra Creek",
"phone": "+1 555 0142"
}
}
]
},
{
"slug": "/login",
"title": "Members",
"template_key": "auth",
"blocks": [
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } },
{ "block_key": "text", "title": "Foot", "slot": "footer", "sort_order": 1, "content": { "text": "<p>Members track pledges and see each site's season report first.</p>" } }
]
}
],
"menu_items": [
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
{ "menu": "main", "label": "Our work", "page_slug": "/about", "sort_order": 2 },
{ "menu": "main", "label": "Field Journal", "page_slug": "/blog", "sort_order": 3 },
{ "menu": "main", "label": "Get in touch", "page_slug": "/contact", "sort_order": 4 }
]
}

19
seed/workflows.json Normal file
View File

@ -0,0 +1,19 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Contact enquiry to admins",
"description": "On a contact-form submission, email the site admins.",
"enabled": true,
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
"steps": [
{
"type": "email",
"name": "Email admins",
"template": "Form Submission Notification",
"recipients": { "mode": "admins", "scope": "all" }
}
]
}
]
}

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="earthen"> <html lang="en" class="earthen">
{{ head_html|safe }} {{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);"> <body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }} {{ admin_banner_html|safe }}
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));"> <header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
<div class="max-w-3xl mx-auto px-4">{{ slots.header|safe }}</div> <div class="max-w-3xl mx-auto px-4">{{ slots.header|safe }}</div>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
<main class="flex-grow w-full flex flex-col items-center justify-center px-4 py-12">
<div class="w-full max-w-md">
<a href="/" class="block text-center" aria-label="Home">
<svg aria-hidden="true" class="mx-auto mb-4 h-11 w-11" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" style="color:hsl(var(--primary));"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
</a>
<div>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p style="color:hsl(var(--muted-foreground));">No form assigned to this page.</p></div>{% endif %}</div>
{% if slots.footer %}<div class="mt-6 text-center text-sm" style="color:hsl(var(--muted-foreground));">{{ slots.footer|safe }}</div>{% endif %}
</div>
</main>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="max-w-6xl mx-auto px-4 py-12 md:py-16">
{% if title %}
<div class="text-center">
<svg aria-hidden="true" class="mx-auto mb-4 h-10 w-10" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" style="color:hsl(var(--primary));"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
<h1 class="crayon-underline mx-auto w-fit text-4xl md:text-5xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ title }}</h1>
{% if page_meta.MetaDescription %}<p class="mt-4 text-lg" style="color:hsl(var(--muted-foreground));">{{ page_meta.MetaDescription }}</p>{% endif %}
</div>
{% endif %}
{% if slots.featured %}<section class="mt-12">{{ slots.featured|safe }}</section>{% endif %}
{% if slots.main %}<section class="mt-12">{{ slots.main|safe }}</section>{% else %}<div class="py-20 text-center"><p style="color:hsl(var(--muted-foreground));">No posts yet. New stories from the field will appear here soon.</p></div>{% endif %}
</div>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en" class="earthen">
{{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="max-w-6xl mx-auto px-4 py-12 md:py-16">
{% if slots.aside %}
<div class="grid gap-12 md:grid-cols-[2fr_1fr]">
<div>{% if slots.main %}{{ slots.main|safe }}{% endif %}</div>
<aside class="w-full md:border-l md:border-border md:pl-10">{{ slots.aside|safe }}</aside>
</div>
{% else %}
<div class="max-w-3xl mx-auto">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p style="color:hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</div>
{% endif %}
</div>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="earthen"> <html lang="en" class="earthen">
{{ head_html|safe }} {{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);"> <body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }} {{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header> <header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full max-w-5xl mx-auto px-4 py-10">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center" style="color: hsl(var(--muted-foreground));"><p>No content blocks assigned to this page.</p></div>{% endif %}</main> <main class="flex-grow w-full max-w-5xl mx-auto px-4 py-10">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center" style="color: hsl(var(--muted-foreground));"><p>No content blocks assigned to this page.</p></div>{% endif %}</main>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="earthen"> <html lang="en" class="earthen">
{{ head_html|safe }} {{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);"> <body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }} {{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header> <header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-4xl mx-auto py-20 px-4 text-center" style="color: hsl(var(--muted-foreground));"><p>No content blocks assigned to this page.</p></div>{% endif %}</main> <main class="flex-grow w-full">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-4xl mx-auto py-20 px-4 text-center" style="color: hsl(var(--muted-foreground));"><p>No content blocks assigned to this page.</p></div>{% endif %}</main>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="earthen"> <html lang="en" class="earthen">
{{ head_html|safe }} {{ head_html|safe }}
<body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Spectral&quot;, Georgia, serif);"> <body class="bg-paper-grain earthen-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground)); font-family: var(--font-body, &quot;Work Sans&quot;, ui-sans-serif, system-ui, sans-serif);">
{{ admin_banner_html|safe }} {{ admin_banner_html|safe }}
<section class="w-full">{{ slots.hero|safe }}</section> <section class="w-full">{{ slots.hero|safe }}</section>
<main class="flex-grow">{% if slots.main %}<div class="max-w-5xl mx-auto px-4 py-16">{{ slots.main|safe }}</div>{% endif %}</main> <main class="flex-grow">{% if slots.main %}<div class="max-w-5xl mx-auto px-4 py-16">{{ slots.main|safe }}</div>{% endif %}</main>

View File

@ -6,4 +6,4 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
</noscript> </noscript>
<![endif]--><title>{{ site_name }}</title></head><body style="background-color: {{ colors.background|default:"#F6F1E6" }}; margin: 0; padding: 0; font-family: &amp;#39;Spectral&amp;#39;, Georgia, serif; color: {{ colors.foreground|default:"#2D3A28" }};">{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: {{ colors.background|default:"#F6F1E6" }};"><tr><td align="center" style="padding: 32px 12px;"><table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#FCF9F2" }}; border: 1px solid {{ colors.border|default:"#D6D1C2" }}; border-radius: 12px; overflow: hidden;"><tr><td align="center" style="padding: 28px 32px; background-color: {{ colors.primary|default:"#3A5A36" }}; color: {{ colors.primaryForeground|default:"#F6F1E6" }};">{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 44px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: &amp;#39;Fraunces&amp;#39;, &amp;#39;Playfair Display&amp;#39;, Georgia, serif; font-size: 26px; font-weight: 600; letter-spacing: -0.01em; color: {{ colors.primaryForeground|default:"#F6F1E6" }};">{{ site_name }}</h1>{% endif %}</td></tr><tr><td style="padding: 36px 40px; font-size: 16px; line-height: 1.7; color: {{ colors.foreground|default:"#2D3A28" }}; font-family: &amp;#39;Spectral&amp;#39;, Georgia, serif;">{{ body|safe }}</td></tr><tr><td style="padding: 24px 40px; background-color: {{ colors.muted|default:"#EFEBDF" }}; border-top: 1px solid {{ colors.border|default:"#D6D1C2" }};"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="center"><p style="margin: 0 0 8px; font-family: &amp;#39;Fraunces&amp;#39;, &amp;#39;Playfair Display&amp;#39;, Georgia, serif; font-size: 14px; font-weight: 600; color: {{ colors.foreground|default:"#2D3A28" }};">{{ site_name }}</p><p style="margin: 0 0 6px; font-size: 11px; color: {{ colors.mutedForeground|default:"#6E7560" }};">Registered charity. We respect your inbox.</p>{% if site_url %}<p style="margin: 0 0 12px; font-size: 12px; color: {{ colors.mutedForeground|default:"#6E7560" }};"><a href="{{ site_url }}" style="color: {{ colors.primary|default:"#3A5A36" }}; text-decoration: none; border-bottom: 1px dotted {{ colors.primary|default:"#3A5A36" }};">{{ site_url }}</a></p>{% endif %}{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px; color: {{ colors.mutedForeground|default:"#6E7560" }};"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#6E7560" }}; text-decoration: underline;">Unsubscribe</a></p>{% else %}<p style="margin: 0; font-size: 11px; color: {{ colors.mutedForeground|default:"#6E7560" }};"><a href="#" style="color: {{ colors.mutedForeground|default:"#6E7560" }}; text-decoration: underline;">Unsubscribe</a></p>{% endif %}</td></tr></table></td></tr></table></td></tr></table></body></html> <![endif]--><title>{{ site_name }}</title></head><body style="background-color: {{ colors.background|default:"#F6F1E6" }}; margin: 0; padding: 0; font-family: &amp;#39;Work Sans&amp;#39;, ui-sans-serif, system-ui, sans-serif; color: {{ colors.foreground|default:"#2D3A28" }};">{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: {{ colors.background|default:"#F6F1E6" }};"><tr><td align="center" style="padding: 32px 12px;"><table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#FCF9F2" }}; border: 1px solid {{ colors.border|default:"#D6D1C2" }}; border-radius: 12px; overflow: hidden;"><tr><td align="center" style="padding: 28px 32px; background-color: {{ colors.primary|default:"#3A5A36" }}; color: {{ colors.primaryForeground|default:"#F6F1E6" }};">{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 44px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: &amp;#39;Fraunces&amp;#39;, &amp;#39;Playfair Display&amp;#39;, Georgia, serif; font-size: 26px; font-weight: 600; letter-spacing: -0.01em; color: {{ colors.primaryForeground|default:"#F6F1E6" }};">{{ site_name }}</h1>{% endif %}</td></tr><tr><td style="padding: 36px 40px; font-size: 16px; line-height: 1.7; color: {{ colors.foreground|default:"#2D3A28" }}; font-family: &amp;#39;Work Sans&amp;#39;, ui-sans-serif, system-ui, sans-serif;">{{ body|safe }}</td></tr><tr><td style="padding: 24px 40px; background-color: {{ colors.muted|default:"#EFEBDF" }}; border-top: 1px solid {{ colors.border|default:"#D6D1C2" }};"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="center"><p style="margin: 0 0 8px; font-family: &amp;#39;Fraunces&amp;#39;, &amp;#39;Playfair Display&amp;#39;, Georgia, serif; font-size: 14px; font-weight: 600; color: {{ colors.foreground|default:"#2D3A28" }};">{{ site_name }}</p><p style="margin: 0 0 6px; font-size: 11px; color: {{ colors.mutedForeground|default:"#6E7560" }};">Registered charity. We respect your inbox.</p>{% if site_url %}<p style="margin: 0 0 12px; font-size: 12px; color: {{ colors.mutedForeground|default:"#6E7560" }};"><a href="{{ site_url }}" style="color: {{ colors.primary|default:"#3A5A36" }}; text-decoration: none; border-bottom: 1px dotted {{ colors.primary|default:"#3A5A36" }};">{{ site_url }}</a></p>{% endif %}{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px; color: {{ colors.mutedForeground|default:"#6E7560" }};"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#6E7560" }}; text-decoration: underline;">Unsubscribe</a></p>{% else %}<p style="margin: 0; font-size: 11px; color: {{ colors.mutedForeground|default:"#6E7560" }};"><a href="#" style="color: {{ colors.mutedForeground|default:"#6E7560" }}; text-decoration: underline;">Unsubscribe</a></p>{% endif %}</td></tr></table></td></tr></table></td></tr></table></body></html>

View File

@ -0,0 +1,21 @@
{# announcement-bar override — earthen banner on moss/clay tones. Dismiss <script>, data-bn-annkey content-hash and localStorage keying preserved verbatim from the builtin; only classes reskinned. Fields mirror builtin: variant, message, linkText, linkUrl, dismissable. #}
{% set anncls = "bg-primary/10 text-foreground" %}
{% if variant == "promo" %}{% set anncls = "bg-primary text-primary-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
<div data-block-override="earthen:announcement-bar" data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full border-b border-border {{ anncls }} {{ class }}">
<div class="max-w-7xl mx-auto px-10 py-2 relative flex items-center justify-center gap-2 text-sm text-center">
<p class="font-medium" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 rounded-lg hover:bg-black/10 transition-colors"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
</div>
</div>
{% if dismissable %}<script>
(function(){
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
bars.forEach(function(bar){
bar.setAttribute('data-bn-ann-wired','1');
var key='bnann:'+bar.getAttribute('data-bn-annkey');
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
var btn=bar.querySelector('[data-bn-ann-dismiss]');
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
});
})();
</script>{% endif %}

View File

@ -0,0 +1,16 @@
{# audio-embed override — cream paper player card. Fields + facade JS mirror builtin verbatim: url, media, artwork, title, artist, download. #}
<figure data-block-override="earthen:audio-embed" class="bn-audio earthen-panel earthen-blob-soft my-8 overflow-hidden{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="earthen-blob-soft h-16 w-16 shrink-0 overflow-hidden bg-muted">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="truncate text-sm font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</p>{% endif %}{% if artist %}<p class="truncate text-sm italic" style="color:hsl(var(--muted-foreground));">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="earthen-cta inline-flex items-center gap-2 px-4 py-1.5 text-sm font-semibold" aria-label="Play{% if title %}: {{ title }}{% endif %}">::lucide:play:sm:: Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="earthen-link inline-flex items-center gap-1 text-sm">::lucide:download:sm:: Download</a>{% endif %}</div></div></div></figure><script>
(function(){
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
e.preventDefault();
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
if(!src)return;
var slot=btn.closest('[data-bn-audio-slot]');
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
btn.remove();slot.insertBefore(a,slot.firstChild);
});
})();
</script>

View File

@ -0,0 +1,33 @@
{# auth-form override — earthen login/register on a paper panel. Wiring is UNTOUCHABLE and reproduced exactly from builtin auth_blocks.go: single shared #auth-message, /api/auth/login (email,password) and /api/auth/register (username,email,password), hx-target #auth-message. Captcha widget injected from _captcha_widget when captchaEnabled (server enforces). Fields: default_tab, show_social (social omitted — needs Go/site config; graceful). Only classes/styles/copy reskinned to earthen. #}
{% if context.isPublicLoggedIn %}
<div data-block-override="earthen:auth-form" class="auth-form-logged-in mx-auto max-w-md text-center py-8">
<p style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">You are signed in as <strong style="color:hsl(var(--foreground));">{{ context.publicUsername }}</strong></p>
<a href="/account" class="earthen-link mt-3 inline-block">My account</a>
</div>
{% else %}
<div data-block-override="earthen:auth-form" class="auth-form earthen-panel mx-auto max-w-md p-8">
<div id="auth-message"></div>
<div class="mb-6 flex border-b border-border">
<button type="button" onclick="document.getElementById('login-panel').classList.remove('hidden');document.getElementById('register-panel').classList.add('hidden');this.setAttribute('data-active','1');this.nextElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab != "register" %}data-active="1"{% endif %} class="flex-1 py-2 text-center text-sm font-medium uppercase tracking-wide border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">Sign in</button>
<button type="button" onclick="document.getElementById('register-panel').classList.remove('hidden');document.getElementById('login-panel').classList.add('hidden');this.setAttribute('data-active','1');this.previousElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab == "register" %}data-active="1"{% endif %} class="flex-1 py-2 text-center text-sm font-medium uppercase tracking-wide border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">Join us</button>
</div>
<div id="login-panel" class="{% if default_tab == "register" %}hidden{% endif %}">
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<div class="mb-4"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="earthen-cta w-full" hx-disabled-elt="this">Sign in</button>
<p class="text-center mt-3 text-sm"><a href="/forgot-password" class="earthen-link">Forgot password?</a></p>
</form>
</div>
<div id="register-panel" class="{% if default_tab != "register" %}hidden{% endif %}">
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<div class="mb-3"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<div class="mb-4"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required minlength="8" class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
<button type="submit" class="earthen-cta w-full" hx-disabled-elt="this">Create account</button>
</form>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,31 @@
{# auth-status override — header login state. Logged-out: Sign in / Join links. Logged-in: moss monogram + dropdown (logout posts /api/auth/logout). Wiring UNTOUCHABLE: context.isPublicLoggedIn / context.publicDisplayName / context.publicUsername reads, #auth-dropdown-menu, onclick toggle, outside-click <script>, hx-post /api/auth/logout. Only classes/styles/copy reskinned to earthen. #}
{% if context.isPublicLoggedIn %}
<div data-block-override="earthen:auth-status" class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="flex items-center gap-2 text-sm font-medium opacity-90 hover:opacity-100 px-3 py-1.5 transition-colors">
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full text-[0.7rem] font-bold" style="background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));">{{ context.publicDisplayName|default:context.publicUsername|first|upper }}</span>
<span>{{ context.publicDisplayName|default:context.publicUsername }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-1 w-48 earthen-panel py-1 z-50">
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-muted">My profile</a>
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-muted">My reviews</a>
<hr class="my-1 border-border"/>
<button hx-post="/api/auth/logout" hx-swap="none" class="block w-full text-left px-4 py-2 text-sm text-destructive hover:bg-muted">Sign out</button>
</div>
<script>
(function(){
document.addEventListener('click', function(e) {
var menu = document.getElementById('auth-dropdown-menu');
var btn = document.querySelector('.auth-status > button');
if (!menu || !btn) return;
if (!btn.contains(e.target) && !menu.contains(e.target)) { menu.classList.add('hidden'); }
});
})();
</script>
</div>
{% else %}
<div data-block-override="earthen:auth-status" class="auth-status flex items-center gap-2">
<a href="/login" class="text-sm px-3 py-1.5 opacity-80 hover:opacity-100 hover:text-primary transition-colors">Sign in</a>
<a href="/login?tab=register" class="earthen-cta text-sm">Join us</a>
</div>
{% endif %}

View File

@ -0,0 +1,22 @@
{# author-bio-hero (earthen) — author-page header from the `authors` provider. Guards, field reads, social icons (via <use>) and image wiring preserved; earthen skin only. #}
{% if author %}
<section data-block-override="earthen:author-bio-hero" class="border-b border-border bg-paper-grain py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
{% if showAvatar and author.avatar_url %}<div class="earthen-blob mb-6 h-24 w-24 overflow-hidden bg-muted">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<svg class="earthen-glyph mb-4 h-8 w-8" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
<h1 class="text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="mt-2 text-sm font-medium text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
{% if author.bio %}<p class="mt-5 max-w-2xl leading-relaxed text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ author.bio }}</p>{% endif %}
<div class="mt-6 flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground">
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
</div>
{% if showSocial %}<div class="mt-6 flex items-center justify-center gap-4">
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Website"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Twitter"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="LinkedIn"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="GitHub"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,13 @@
{# author-bio (earthen) — byline card sourced from the `post` provider's author_* fields. Guards, field reads and image wiring preserved; earthen skin only. #}
{% if post.author_name %}
<aside data-block-override="earthen:author-bio" class="earthen-panel earthen-blob-soft p-6 text-card-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="mb-4 text-xs font-semibold uppercase tracking-wider text-accent">{{ heading }}</p>{% endif %}
<div class="flex items-start gap-4">
{% if showAvatar and post.author_avatar %}<div class="earthen-blob-soft h-14 w-14 flex-shrink-0 overflow-hidden bg-muted">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="text-base font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--card-foreground));">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-primary">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
{% if post.author_bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -0,0 +1,24 @@
{# blog-hero (earthen) — blog-index header + featured posts from the `posts` provider. Wiring, field reads and loops preserved from builtin; earthen skin only. #}
<section data-block-override="earthen:blog-hero" class="bg-paper-grain py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="mx-auto max-w-2xl text-center">
<svg class="earthen-glyph mx-auto mb-4 h-9 w-9" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
{% if title %}<h1 class="crayon-underline inline-block text-4xl md:text-5xl" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="mt-4 text-lg text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ subtitle }}</p>{% endif %}
</div>
{% if posts %}
<div class="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
{% for post in posts %}
<article class="group flex flex-col{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-8{% endif %}">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="earthen-blob-soft mb-4 block overflow-hidden bg-muted{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
<h2 class="{% if forloop.First %}text-2xl{% else %}text-lg{% endif %} font-semibold leading-snug" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h2>
{% if showExcerpt and post.excerpt %}<p class="mt-3 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ post.excerpt }}</p>{% endif %}
<div class="mt-4 flex flex-wrap items-center gap-x-3 text-xs text-muted-foreground">{% if post.author_name %}<span class="font-medium text-accent">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,13 @@
{# breadcrumbs (earthen) — trail from the `breadcrumbs` provider (page ancestry). showHome prepends a Home link; separator is text ("/") or a chevron for the arrows template; the trailing is_current item renders as plain text. Control flow and field reads preserved; earthen skin only. #}
{% set arrows = template == "arrows" %}
<nav aria-label="Breadcrumb" data-block-override="earthen:breadcrumbs" class="py-3{% if class %} {{ class }}{% endif %}">
<ol class="flex flex-wrap items-center gap-1 text-xs">
{% if showHome %}<li class="flex items-center"><a href="/" class="text-muted-foreground transition-colors hover:text-primary">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
{% for item in breadcrumbs %}
<li class="flex items-center">
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-3.5 w-3.5 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>{% else %}<span class="mx-2 text-accent">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
{% if item.is_current %}{% if showCurrent %}<span class="font-medium text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-muted-foreground transition-colors hover:text-primary">{{ item.label }}</a>{% endif %}
</li>
{% endfor %}
</ol>
</nav>

View File

@ -1 +1,10 @@
<a href="{{ url|default:href|default:"#" }}" data-variant="{{ variant|default:"primary" }}" class="earthen-button inline-flex items-center justify-center px-6 py-3 rounded-lg font-semibold transition-colors focus:outline-none" style="{% if variant == "secondary" %}background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border: 1px solid hsl(var(--border));{% elif variant == "ghost" %}background-color: transparent; color: hsl(var(--primary)); border: 1.5px dashed hsl(var(--primary) / 0.6);{% elif variant == "destructive" %}background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground));{% else %}background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground));{% endif %}">{{ label|default:text }}</a> {# button override — terracotta seedling CTA, or a moss botanical-underline link for quiet styles. Fields (new builtin contract, per OVERRIDE-CONTRACT §4): link, style, label, size, icon, iconPosition, newTab, align. #}
{% with align=align|default:"left" %}
<div data-block-override="earthen:button" class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}">
{% if style == "ghost" or style == "link" or style == "secondary" %}
<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="earthen-link inline-flex items-center gap-2 text-sm font-semibold">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>
{% else %}
<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="earthen-cta inline-flex items-center gap-2 px-6 py-3 font-semibold">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>
{% endif %}
</div>
{% endwith %}

View File

@ -1 +1,4 @@
<article class="earthen-card rounded-xl overflow-hidden p-6"{% if not title and not body and not image and not link and not linkLabel %} data-empty{% endif %} style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border)); box-shadow: inset 0 0 0 1px hsl(var(--border) / 0.4);">{% if image %}<div class="earthen-card-image -mx-6 -mt-6 mb-4 overflow-hidden"><img src="{{ image }}" alt="{{ title }}" class="w-full h-auto"></div>{% endif %}{% if title %}<h3 class="earthen-card-title text-2xl mb-3" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--primary));">{{ title }}</h3>{% endif %}{% if body %}<div class="earthen-card-body mb-4" style="font-family: var(--font-body, \000022Spectral\000022, Georgia, serif); color: hsl(var(--foreground));">{{ body|safe }}</div>{% endif %}{% if link %}<a href="{{ link }}" class="earthen-card-link inline-flex items-center gap-2 font-semibold" style="color: hsl(var(--accent));">{% if linkLabel %}<span>{{ linkLabel }}</span>{% else %}<span>Read more</span>{% endif %}<span aria-hidden="true">→</span></a>{% endif %}</article> {# card override — cream paper panel with soft organic corners. Fields mirror builtin: layout, link, media, title, text, linkLabel. Media via {% img %}. #}
{% with layout=layout|default:"vertical" %}
<article data-block-override="earthen:card" class="bn-card earthen-panel earthen-blob-soft overflow-hidden transition-shadow hover:shadow-md {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">{% if link %}<a href="{{ link }}" class="contents">{% endif %}{% if media %}<div class="overflow-hidden bg-muted {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">{% if title %}<h3 class="text-lg font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="mt-2 inline-flex items-center gap-1 text-sm font-semibold" style="color:hsl(var(--accent));">{{ linkLabel }}::lucide:arrow-right:sm::</span>{% endif %}</div>{% if link %}</a>{% endif %}</article>
{% endwith %}

View File

@ -0,0 +1,18 @@
{# category-list (earthen) — from the `categories` provider. list or pills. Control flow and field reads preserved; earthen skin only. #}
{% set pills = style == "pills" %}
<section data-block-override="earthen:category-list" class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="mb-5 text-sm font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading }}</h2>{% endif %}
{% if pills %}
<div class="flex flex-wrap gap-2">
{% for cat in categories %}
<a href="{{ cat.url }}" class="inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-4 py-1.5 text-sm text-card-foreground transition-colors hover:border-primary hover:text-primary">{{ cat.name }}{% if showCount %}<span class="text-xs text-accent">{{ cat.post_count }}</span>{% endif %}</a>
{% endfor %}
</div>
{% else %}
<ul class="space-y-1">
{% for cat in categories %}
<li><a href="{{ cat.url }}" class="flex items-center justify-between rounded-lg px-3 py-2.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-primary"><span>{{ cat.name }}</span>{% if showCount %}<span class="rounded-full border border-border px-2 py-0.5 text-xs text-accent">{{ cat.post_count }}</span>{% endif %}</a></li>
{% endfor %}
</ul>
{% endif %}
</section>

View File

@ -0,0 +1,18 @@
{# contact-card override — cream contact tiles with moss icon chips. Fields mirror builtin: heading, description, columns, channels[].icon/.label/.value/.link. Icons via sprite <use>. Wiring preserved. #}
<section data-block-override="earthen:contact-card" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4 text-center">
{% if heading %}<h2 class="crayon-underline text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading }}</h2>{% endif %}
{% if description %}<p class="mt-4 mx-auto max-w-2xl text-lg" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ description }}</p>{% endif %}
<div class="mt-12 grid grid-cols-1 gap-6 text-left {% if columns == 3 %}sm:grid-cols-2 lg:grid-cols-3{% elif columns == 1 %}{% else %}sm:grid-cols-2{% endif %}">
{% for channel in channels %}
<div class="earthen-panel earthen-blob-soft flex items-start gap-4 p-6">
{% if channel.icon %}<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl" style="background-color:hsl(var(--primary)/0.12);color:hsl(var(--primary));"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"></use></svg></span>{% endif %}
<div class="min-w-0">
{% if channel.label %}<div class="text-xs font-semibold uppercase tracking-[0.14em]" style="color:hsl(var(--muted-foreground));">{{ channel.label }}</div>{% endif %}
{% if channel.value %}<div class="mt-1 break-words text-lg font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{% if channel.link %}<a href="{{ channel.link }}" class="earthen-link">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,43 @@
{# contact-form override — SEED-CRITICAL. ALL form wiring preserved verbatim from builtin: bid, {% form %} action/hx attrs, honeypot input name, field loop, form.submitted/errors, submit target. Only inputs restyled and copy reworded to earthen. #}
{% with bid=_block_id|default:context.blockId %}
<section data-block-override="earthen:contact-form" id="contact-form-{{ bid }}" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-2xl px-4">
{% if form.submitted %}
<div class="earthen-panel earthen-blob-soft p-10 text-center">
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full" style="background-color:hsl(var(--primary)/0.12);color:hsl(var(--primary));">::lucide:check:lg::</div>
<p class="text-lg italic" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ form.message|default:successMessage|default:"Thank you. Your message has been received." }}</p>
</div>
{% else %}
<div class="text-center">
{% if heading %}<h2 class="text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading }}</h2>{% endif %}
{% if description %}<p class="mt-3 text-lg italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ description }}</p>{% endif %}
<div class="mx-auto mt-5 mb-2 flex justify-center" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" style="color:hsl(var(--primary));"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg></div>
</div>
{% if form.errors._form %}<div class="mt-6 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">{{ form.errors._form }}</div>{% endif %}
{% with formAction="/api/blocks/"|add:bid|add:"/submit" formTarget="#contact-form-"|add:bid %}
{% form action=formAction hx_target=formTarget hx_swap="outerHTML" class="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2" %}
{% for field in fields %}
<div class="{% if field.width == "half" %}sm:col-span-1{% else %}sm:col-span-2{% endif %}">
{% if field.label %}<label for="cf-{{ bid }}-{{ field.name }}" class="mb-1.5 block text-xs font-semibold uppercase tracking-wide" style="color:hsl(var(--primary));">{{ field.label }}{% if field.required %} <span class="text-destructive">*</span>{% endif %}</label>{% endif %}
{% if field.type == "textarea" %}
<textarea id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" rows="5"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring"></textarea>
{% elif field.type == "select" %}
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring">
{% if field.placeholder %}<option value="">{{ field.placeholder }}</option>{% endif %}
{% for opt in field.options %}<option value="{{ opt.value }}">{{ opt.label|default:opt.value }}</option>{% endfor %}
</select>
{% else %}
<input id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" type="{{ field.type|default:"text" }}"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring">
{% endif %}
</div>
{% endfor %}
<div class="hidden" aria-hidden="true"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
<div class="sm:col-span-2 flex justify-center">
<button type="submit" class="earthen-cta inline-flex items-center justify-center px-6 py-3 font-semibold">{{ submitLabel|default:"Send message" }}</button>
</div>
{% endform %}
{% endwith %}
{% endif %}
</div>
</section>
{% endwith %}

View File

@ -0,0 +1,49 @@
{# countdown override — earthen: paper ground, soft botanical unit boxes, terracotta digits. Field reads, every data-countdown/data-countdown-timer/data-target/data-unit/data-countdown-expired hook, the <noscript> fallback and the entire tick <script> preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:countdown" class="py-16 md:py-24 bg-paper-grain text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4 text-center">
{% if heading %}<h2 class="text-3xl md:text-4xl font-semibold tracking-tight" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary))">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ intro }}</p>{% endif %}
<div class="mt-10" data-countdown>
<div class="flex items-start justify-center gap-3 sm:gap-6" data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining">
<div class="flex flex-col items-center {% if variant == "boxes" %}earthen-panel earthen-blob-soft min-w-[4.5rem] px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono, 'JetBrains Mono', ui-monospace, monospace);color:hsl(var(--accent));" data-unit="days">--</span><span class="mt-1 text-xs uppercase tracking-wider text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}earthen-panel earthen-blob-soft min-w-[4.5rem] px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono, 'JetBrains Mono', ui-monospace, monospace);color:hsl(var(--accent));" data-unit="hours">--</span><span class="mt-1 text-xs uppercase tracking-wider text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}earthen-panel earthen-blob-soft min-w-[4.5rem] px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono, 'JetBrains Mono', ui-monospace, monospace);color:hsl(var(--accent));" data-unit="minutes">--</span><span class="mt-1 text-xs uppercase tracking-wider text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}earthen-panel earthen-blob-soft min-w-[4.5rem] px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono, 'JetBrains Mono', ui-monospace, monospace);color:hsl(var(--accent));" data-unit="seconds">--</span><span class="mt-1 text-xs uppercase tracking-wider text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
</div>
<div class="mt-8 hidden" data-countdown-expired>
<p class="text-xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif)">{{ expiredMessage|default:"This has started." }}</p>
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="earthen-cta mt-4 inline-flex items-center gap-2 px-5 py-3 font-semibold">{{ expiredLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
</div>
<noscript><p class="mt-6 text-muted-foreground">Counting down to <time datetime="{{ target }}">{{ target }}</time>.</p></noscript>
</div>
</section>
<script>
(function(){
var timers = document.querySelectorAll('[data-countdown-timer]');
timers.forEach(function(el){
var target = new Date(el.getAttribute('data-target')).getTime();
if (isNaN(target)) return;
var wrap = el.closest('[data-countdown]');
var expired = wrap ? wrap.querySelector('[data-countdown-expired]') : null;
function pad(n){ return (n < 10 ? '0' : '') + n; }
function set(unit, value){ var node = el.querySelector('[data-unit="' + unit + '"]'); if (node) node.textContent = value; }
var interval;
function tick(){
var diff = target - Date.now();
if (diff <= 0){
el.style.display = 'none';
if (expired) expired.classList.remove('hidden');
if (interval) clearInterval(interval);
return;
}
set('days', Math.floor(diff / 86400000));
set('hours', pad(Math.floor(diff % 86400000 / 3600000)));
set('minutes', pad(Math.floor(diff % 3600000 / 60000)));
set('seconds', pad(Math.floor(diff % 60000 / 1000)));
}
tick();
interval = setInterval(tick, 1000);
});
})();
</script>

View File

@ -0,0 +1,15 @@
{# cta override — earthen: soft organic panel, moss band / cream card / oat split, terracotta primary button. Field reads, the background control flow and arrow <use> preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:cta" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="{% if background == "band" %}earthen-blob-soft px-6 py-14 text-center md:px-12{% elif background == "card" %}earthen-panel earthen-blob-soft px-6 py-14 text-center md:px-12{% else %}earthen-blob-soft flex flex-col items-center gap-8 px-6 py-12 md:flex-row md:justify-between md:px-12 md:text-left{% endif %}"{% if background == "band" %} style="background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));"{% elif background == "split" %} style="background-color:hsl(var(--muted));color:hsl(var(--foreground));"{% endif %}>
<div class="{% if background != "split" %}mx-auto max-w-2xl{% endif %}">
{% if heading %}<h2 class="text-3xl md:text-4xl font-semibold tracking-tight" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
{% if text %}<p class="mt-3 text-lg {% if background == "band" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ text }}</p>{% endif %}
</div>
<div class="flex flex-wrap items-center justify-center gap-3 {% if background != "split" %}mt-8{% else %}shrink-0{% endif %}">
{% if primaryUrl and primaryLabel %}<a href="{{ primaryUrl }}" class="earthen-cta inline-flex items-center gap-2 px-6 py-3 font-semibold">{{ primaryLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
{% if secondaryUrl and secondaryLabel %}<a href="{{ secondaryUrl }}" class="inline-flex items-center rounded-lg px-6 py-3 font-semibold transition-colors {% if background == "band" %}hover:bg-white/10{% else %}hover:bg-muted{% endif %}" style="{% if background == "band" %}border:1.5px solid hsl(var(--primary-foreground) / 0.4);color:hsl(var(--primary-foreground));{% else %}border:1.5px solid hsl(var(--border));color:hsl(var(--primary));{% endif %}">{{ secondaryLabel }}</a>{% endif %}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,13 @@
{# divider override — carries the botanical divider DNA: a fern glyph flourish between soft hairline rules. Fields mirror builtin: variant (line|ornament|spacer-rule), weight, width, icon. Wiring preserved. #}
{% with variant=variant|default:"line" weight=weight|default:"thin" width=width|default:"full" %}
{% if variant == "ornament" %}
<div data-block-override="earthen:divider" class="bn-divider mx-auto my-10 flex flex-col items-center gap-2 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" style="color:hsl(var(--primary));"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>
<div class="flex w-full items-center gap-4"><span class="h-px flex-1" style="background-color:hsl(var(--border));"></span><span style="color:hsl(var(--accent));">::{{ icon|default:"lucide:leaf" }}:sm::</span><span class="h-px flex-1" style="background-color:hsl(var(--border));"></span></div>
</div>
{% elif variant == "spacer-rule" %}
<div data-block-override="earthen:divider" class="bn-divider mx-auto py-10 flex justify-center {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><span class="w-16 rounded-full {% if weight == "thick" %}h-1{% elif weight == "medium" %}h-0.5{% else %}h-px{% endif %}" style="background-color:hsl(var(--border));"></span></div>
{% else %}
<div data-block-override="earthen:divider" class="bn-divider mx-auto my-8 flex justify-center {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><span class="w-full rounded-full {% if weight == "thick" %}h-1{% elif weight == "medium" %}h-0.5{% else %}h-px{% endif %}" style="background-color:hsl(var(--border));"></span></div>
{% endif %}
{% endwith %}

View File

@ -0,0 +1,41 @@
{# event-list override — cream event cards / rows on paper; terracotta dates, moss titles. Fields mirror builtin: heading, intro, layout, events[].past/.media/.datetime/.start/.end/.timezone/.title/.venue/.description/.link/.linkLabel, hidePast. Wiring preserved. #}
<section data-block-override="earthen:event-list" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4 text-center">
{% if heading %}<h2 class="crayon-underline text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-4 mx-auto max-w-2xl text-lg" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ intro }}</p>{% endif %}
{% if layout == "cards" %}
<div class="mt-12 grid grid-cols-1 gap-8 text-left sm:grid-cols-2 lg:grid-cols-3">
{% for event in events %}{% if not (hidePast and event.past) %}
<article class="earthen-panel earthen-blob-soft flex flex-col overflow-hidden{% if event.past %} opacity-60{% endif %}">
{% if event.media %}<div class="aspect-video overflow-hidden bg-muted">{% img event.media alt=event.title class="h-full w-full object-cover" %}</div>{% endif %}
<div class="flex flex-1 flex-col gap-2 p-6">
<div class="text-xs font-semibold uppercase tracking-[0.14em]" style="color:hsl(var(--accent));"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} &ndash; {{ event.end }}{% endif %}</time>{% if event.timezone %} <span style="color:hsl(var(--muted-foreground));">{{ event.timezone }}</span>{% endif %}</div>
<h3 class="text-xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ event.title }}</h3>
{% if event.venue %}<p class="text-xs uppercase tracking-[0.12em]" style="color:hsl(var(--muted-foreground));">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="text-sm" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ event.description }}</p>{% endif %}
{% if event.link %}<a href="{{ event.link }}" class="earthen-link mt-auto inline-flex items-center gap-2 pt-3 text-sm font-semibold">{{ event.linkLabel|default:"Details" }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
</article>
{% endif %}{% endfor %}
</div>
{% else %}
<ul class="mt-12 divide-y text-left border-y" style="border-color:hsl(var(--border));">
{% for event in events %}{% if not (hidePast and event.past) %}
<li class="flex flex-col gap-3 py-6 sm:flex-row sm:items-center sm:gap-8{% if event.past %} opacity-60{% endif %}">
<div class="shrink-0 sm:w-52">
<div class="text-sm font-semibold uppercase tracking-[0.12em]" style="color:hsl(var(--accent));"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
{% if event.end %}<div class="text-xs" style="color:hsl(var(--muted-foreground));">to {{ event.end }}</div>{% endif %}
{% if event.timezone %}<div class="text-xs" style="color:hsl(var(--muted-foreground));">{{ event.timezone }}</div>{% endif %}
</div>
<div class="min-w-0 flex-1">
<h3 class="text-lg font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ event.title }}</h3>
{% if event.venue %}<p class="text-xs uppercase tracking-[0.12em]" style="color:hsl(var(--muted-foreground));">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="mt-1 text-sm" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ event.description }}</p>{% endif %}
</div>
{% if event.link %}<a href="{{ event.link }}" class="earthen-cta inline-flex shrink-0 items-center gap-2 px-5 py-2.5 text-sm font-semibold">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
</li>
{% endif %}{% endfor %}
</ul>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,18 @@
{# faq override — earthen: paper ground, Fraunces questions, botanical dividers. Field reads, the <details> open/variant control flow, marker-hidden summary, chevron <use> and answer|safe preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:faq" class="py-16 md:py-24 bg-paper-grain text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4">
{% if heading %}<h2 class="text-center text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary))">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ intro }}</p>{% endif %}
<div class="mt-10 divide-y divide-border border-y border-border">
{% for item in items %}
<details class="group py-2"{% if variant == "open-list" %} open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between gap-4 py-3 text-lg font-semibold marker:content-none [&::-webkit-details-marker]:hidden" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground))">
<span>{{ item.question }}</span>
<svg class="h-5 w-5 shrink-0 text-primary transition-transform duration-200 group-open:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
</summary>
<div class="pb-4 text-muted-foreground [&_a]:text-primary [&_a]:underline" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ item.answer|safe }}</div>
</details>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,18 @@
{# feature-grid override — earthen: paper-grain ground, botanical eyebrow rule, Fraunces heading, soft botanical cards. Field reads, columns/layout control flow and icon <use> split preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:feature-grid" class="py-16 md:py-24 bg-paper-grain text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if eyebrow %}<div class="mb-3 flex items-center justify-center gap-3"><span class="h-px w-8" style="background-color:hsl(var(--accent));"></span><p class="text-xs font-semibold uppercase tracking-wider" style="color:hsl(var(--primary));">{{ eyebrow }}</p><span class="h-px w-8" style="background-color:hsl(var(--accent));"></span></div>{% endif %}
{% if heading %}<h2 class="mt-2 text-center text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ intro }}</p>{% endif %}
<div class="mt-12 grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
{% for item in items %}
<div class="{% if layout == "card" %}earthen-panel earthen-blob-soft p-6 transition-shadow{% elif layout == "centered" %}text-center{% endif %}">
{% if item.icon %}<div class="mb-4 inline-flex h-11 w-11 items-center justify-center earthen-blob-soft bg-primary/10 text-primary{% if layout == "centered" %} mx-auto{% endif %}"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg></div>{% endif %}
<h3 class="text-lg font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground))">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" class="transition-colors hover:text-primary">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>
{% if item.text %}<p class="mt-2 text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ item.text }}</p>{% endif %}
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="earthen-link mt-3 inline-flex items-center gap-1 text-sm">{{ item.linkLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,22 @@
{# featured-posts (earthen) — grid of cards from the `posts` provider (params: limit, featured=true). Field reads, loops and image wiring preserved; earthen skin only. #}
<section data-block-override="earthen:featured-posts" class="bg-paper-grain py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<div class="mb-10 text-center"><svg class="earthen-glyph mx-auto mb-3 h-8 w-8" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg><h2 class="crayon-underline inline-block text-2xl md:text-3xl" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading }}</h2></div>{% endif %}
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
{% for post in posts %}
<article class="earthen-panel earthen-blob-soft group flex flex-col overflow-hidden text-card-foreground transition-shadow">
{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-1 flex-col p-6">
<h3 class="text-lg font-semibold leading-snug" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--card-foreground));"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h3>
{% if showExcerpt and post.excerpt %}<p class="mt-3 flex-1 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ post.excerpt }}</p>{% endif %}
<div class="mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
{% if showAuthor and post.author_name %}<span class="font-medium text-accent">{{ post.author_name }}</span>{% endif %}
{% if showDate and post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}
{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
</div>
</div>
</article>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,40 @@
{# footer override — earthen cream-on-moss footer with a botanical fern rule. Layouts columns|minimal|centered, menu columns/legal/social from the footer_menus provider, and the {% signup_form %} core are preserved verbatim from the builtin; only classes/style/fonts/copy reskinned. #}
{% set fl = layout %}
{% if not fl %}{% if template == "centered" %}{% set fl = "centered" %}{% else %}{% set fl = "columns" %}{% endif %}{% endif %}
{% set fbg = backgroundColor|default:"bg-primary text-primary-foreground" %}
{% macro sociallinks(cls) %}{% if social_items %}<div class="flex items-center gap-4 {{ cls }}">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="text-primary-foreground/70 hover:text-accent transition-colors"><span class="sr-only">{{ s.label }}</span>{% if s.icon %}::{{ s.icon }}::{% else %}{{ s.label }}{% endif %}</a>{% endfor %}</div>{% endif %}{% endmacro %}
{% macro newsletter() %}{% if showNewsletter %}<div class="max-w-sm"><h3 class="text-sm font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">{{ newsletterTitle|default:"Stay rooted with us" }}</h3>{% if newsletterDescription %}<p class="mt-1 text-sm text-primary-foreground/70">{{ newsletterDescription }}</p>{% endif %}<div class="mt-3">{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div></div>{% endif %}{% endmacro %}
{% macro legalbar() %}<div class="mt-10 pt-6 flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-primary-foreground/60" style="border-top:1px solid hsl(var(--primary-foreground) / 0.2);"><p>{% if copyright %}&copy; {{ copyright }}{% else %}&copy; {{ companyName }}{% endif %}</p>{% if legal_items %}<nav class="flex flex-wrap items-center gap-x-4 gap-y-1">{% for l in legal_items %}<a href="{{ l.url|default:'#' }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="hover:text-accent transition-colors">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
<footer data-block-override="earthen:footer" class="w-full {{ fbg }} {{ class }}">
<div class="{% if fullWidth %}w-full px-4 sm:px-6 lg:px-8{% else %}max-w-7xl mx-auto px-4 sm:px-6 lg:px-8{% endif %} py-16">
{% if fl == "minimal" %}
<div class="flex flex-col sm:flex-row items-center justify-between gap-6">
<div class="flex items-center gap-3">{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-8 w-auto"/>{% elif companyName %}<span class="text-lg font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="text-sm text-primary-foreground/70 hidden sm:inline">{{ tagline }}</span>{% endif %}</div>
{{ sociallinks("") }}
</div>
{{ legalbar() }}
{% elif fl == "centered" %}
<div class="flex flex-col items-center text-center gap-5">
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-10 w-auto"/>{% elif companyName %}<span class="text-2xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">{{ companyName }}</span>{% endif %}
<div class="flex items-center gap-4 w-56" aria-hidden="true"><span class="flex-1 h-px" style="background-color:hsl(var(--primary-foreground) / 0.3);"></span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="28" height="28" class="earthen-glyph" style="color:hsl(var(--primary-foreground) / 0.85);" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg><span class="flex-1 h-px" style="background-color:hsl(var(--primary-foreground) / 0.3);"></span></div>
{% if tagline %}<p class="max-w-md text-sm text-primary-foreground/70">{{ tagline }}</p>{% endif %}
{% if columns %}<nav class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">{% for col in columns %}{% for item in col.items %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="text-sm text-primary-foreground/80 hover:text-accent transition-colors">{{ item.label }}</a>{% endfor %}{% endfor %}</nav>{% endif %}
{{ newsletter() }}
{{ sociallinks("justify-center") }}
</div>
{{ legalbar() }}
{% else %}
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8">
<div class="col-span-2 lg:col-span-2">
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-9 w-auto"/>{% elif companyName %}<span class="text-xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">{{ companyName }}</span>{% endif %}
{% if tagline %}<p class="mt-3 max-w-xs text-sm text-primary-foreground/70">{{ tagline }}</p>{% endif %}
{% if address or email or phone %}<address class="mt-4 not-italic text-sm text-primary-foreground/70 space-y-1">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="hover:text-accent transition-colors">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="hover:text-accent transition-colors">{{ phone }}</a></div>{% endif %}</address>{% endif %}
{{ sociallinks("mt-5") }}
</div>
{% for col in columns %}<div><h3 class="text-sm font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">{{ col.header }}</h3><ul class="mt-3 space-y-2">{% for item in col.items %}<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="earthen-footer-link text-sm text-primary-foreground/70">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
{% if showNewsletter %}<div class="col-span-2 lg:col-span-1">{{ newsletter() }}</div>{% endif %}
</div>
{{ legalbar() }}
{% endif %}
</div>
</footer>

View File

@ -0,0 +1,52 @@
{# gallery override — soft organic-cornered grid on paper. Fields + lightbox JS mirror builtin verbatim: layout, gap, columns, lightbox, images[].src/.alt/.caption. #}
{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}<div data-block-override="earthen:gallery" class="bn-gallery my-8{% if class %} {{ class }}{% endif %}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}"><div class="{% if layout == "carousel" %}flex snap-x snap-mandatory overflow-x-auto pb-2 {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% elif layout == "masonry" %}columns-2 {% if cols == 4 %}md:columns-4{% elif cols == 2 %}md:columns-2{% else %}md:columns-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% else %}grid grid-cols-2 {% if cols == 4 %}md:grid-cols-4{% elif cols == 2 %}md:grid-cols-2{% else %}md:grid-cols-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% endif %}">{% for image in images %}<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:"" }}" class="earthen-blob-soft group relative block overflow-hidden bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 {% if layout == "carousel" %}w-64 shrink-0 snap-start sm:w-80{% elif layout == "masonry" %}mb-4 w-full break-inside-avoid{% endif %}">{% if layout == "grid" %}{% img image.src alt=image.alt|default:"" class="aspect-square w-full object-cover" %}{% else %}{% img image.src alt=image.alt|default:"" class="h-auto w-full" %}{% endif %}{% if image.caption %}<span class="sr-only">{{ image.caption }}</span>{% endif %}</a>{% endfor %}</div></div>{% if lightbox %}<script>
(function(){
if(window.__bnLightbox)return;window.__bnLightbox=true;
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var ov,imgEl,capEl,items=[],idx=0,lastFocus=null;
function build(){
ov=document.createElement('div');
ov.setAttribute('role','dialog');ov.setAttribute('aria-modal','true');ov.setAttribute('aria-label','Image viewer');ov.tabIndex=-1;
ov.style.cssText='position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.9);padding:2rem;';
var btn='position:absolute;width:2.75rem;height:2.75rem;display:inline-flex;align-items:center;justify-content:center;border-radius:9999px;border:none;background:hsl(var(--background));color:hsl(var(--foreground));cursor:pointer;';
ov.innerHTML=
'<button type="button" data-lb-close aria-label="Close" style="'+btn+'top:1rem;right:1rem;"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#x"/></svg></button>'+
'<button type="button" data-lb-prev aria-label="Previous image" style="'+btn+'left:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-left"/></svg></button>'+
'<figure style="margin:0;max-width:92vw;max-height:92vh;display:flex;flex-direction:column;align-items:center;gap:0.75rem;"><img data-lb-img alt="" style="max-width:92vw;max-height:82vh;object-fit:contain;border-radius:0.5rem;'+(reduce?'':'transition:opacity .2s ease;')+'"><figcaption data-lb-cap style="color:hsl(var(--background));opacity:.85;font-size:.875rem;text-align:center;max-width:60ch;"></figcaption></figure>'+
'<button type="button" data-lb-next aria-label="Next image" style="'+btn+'right:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"/></svg></button>';
document.body.appendChild(ov);
imgEl=ov.querySelector('[data-lb-img]');capEl=ov.querySelector('[data-lb-cap]');
ov.addEventListener('click',function(e){if(e.target===ov||e.target.closest('[data-lb-close]'))close();});
ov.querySelector('[data-lb-prev]').addEventListener('click',function(e){e.stopPropagation();show(idx-1);});
ov.querySelector('[data-lb-next]').addEventListener('click',function(e){e.stopPropagation();show(idx+1);});
}
function show(i){
if(!items.length)return;
idx=(i+items.length)%items.length;
var a=items[idx];var im=a.querySelector('img');
imgEl.src=(im&&(im.currentSrc||im.src))||a.getAttribute('href');
imgEl.alt=im?im.alt:'';
var cap=a.getAttribute('data-caption')||'';capEl.textContent=cap;capEl.style.display=cap?'block':'none';
}
function open(g,link){
if(!ov)build();
items=Array.prototype.slice.call(g.querySelectorAll('[data-bn-gallery-item]'));
lastFocus=document.activeElement;ov.style.display='flex';
var multi=items.length>1;
ov.querySelector('[data-lb-prev]').style.display=multi?'inline-flex':'none';
ov.querySelector('[data-lb-next]').style.display=multi?'inline-flex':'none';
show(items.indexOf(link));ov.focus();
}
function close(){if(ov)ov.style.display='none';if(lastFocus&&lastFocus.focus)lastFocus.focus();}
document.addEventListener('click',function(e){
var link=e.target.closest('[data-bn-gallery][data-lightbox="true"] [data-bn-gallery-item]');
if(!link)return;e.preventDefault();open(link.closest('[data-bn-gallery]'),link);
});
document.addEventListener('keydown',function(e){
if(!ov||ov.style.display==='none')return;
if(e.key==='Escape'){e.preventDefault();close();}
else if(e.key==='ArrowLeft')show(idx-1);
else if(e.key==='ArrowRight')show(idx+1);
});
})();
</script>{% endif %}{% endwith %}

View File

@ -1 +1,7 @@
{% if level|integer == 1 %}<h1 class="earthen-h1 text-5xl md:text-6xl font-semibold tracking-tight leading-tight {{ textClass }}" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--primary));">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="earthen-h3 text-2xl md:text-3xl font-semibold leading-snug {{ textClass }}" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--foreground));">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="earthen-h4 text-xl md:text-2xl font-semibold {{ textClass }}" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--foreground));">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="earthen-h5 text-lg font-semibold {{ textClass }}" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--foreground));">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="earthen-h6 text-base font-semibold uppercase tracking-wider {{ textClass }}" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--muted-foreground));">{{ text }}</h6>{% else %}<h2 class="earthen-h2 text-3xl md:text-4xl font-semibold leading-snug crayon-underline {{ textClass }}" style="font-family: var(--font-heading, \000022Fraunces\000022, \000022Playfair Display\000022, Georgia, serif); color: hsl(var(--primary));">{{ text }}</h2>{% endif %} {# heading override — Fraunces display headings; section h2s carry the hand-drawn crayon underline. Fields mirror builtin: text, level, textClass. Wiring preserved. #}
{% if level == 1 %}<h1 data-block-override="earthen:heading" class="earthen-h1 text-5xl md:text-6xl font-semibold tracking-tight leading-tight flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ text }}</h1>
{% elif level == 3 %}<h3 data-block-override="earthen:heading" class="earthen-h3 text-2xl md:text-3xl font-semibold leading-snug flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h3>
{% elif level == 4 %}<h4 data-block-override="earthen:heading" class="earthen-h4 text-xl md:text-2xl font-semibold flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h4>
{% elif level == 5 %}<h5 data-block-override="earthen:heading" class="earthen-h5 text-lg font-semibold flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h5>
{% elif level == 6 %}<h6 data-block-override="earthen:heading" class="earthen-h6 text-base font-semibold uppercase tracking-wider flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--muted-foreground));">{{ text }}</h6>
{% else %}<h2 data-block-override="earthen:heading" class="earthen-h2 crayon-underline text-3xl md:text-4xl font-semibold leading-snug flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ text }}</h2>{% endif %}

View File

@ -0,0 +1,39 @@
{# hero override — earthen: paper-grain ground, botanical eyebrow rule, Fraunces headline, soft organic media. All {% set %}, {% img %} calls, overlay conditional layers, the herocontent macro and variant branches preserved verbatim from the builtin; only classes/style/fonts/copy reskinned. Styled-text fields accept {text,color} or a bare string. #}
{% set eyebrowText = eyebrow.text|default:eyebrow %}
{% set headlineText = headline.text|default:headline %}
{% set subText = subheadline.text|default:subheadline %}
{% set descText = description.text|default:description %}
{% set pText = ctaPrimary.text %}
{% set pUrl = ctaPrimary.url %}
{% set sText = ctaSecondary.text %}
{% set sUrl = ctaSecondary.url %}
{% set variant = template|default:"centered" %}
{% set hasImage = backgroundImage %}
{% set showOverlay = hasImage and overlayColor != "none" and overlayColor != "" %}
{% macro herocontent(align) %}
{% if eyebrowText %}<div class="mb-5 flex items-center gap-3 {% if align == "center" %}justify-center{% endif %}"><span class="h-px w-8" style="background-color:hsl(var(--accent));"></span><span class="text-xs font-semibold uppercase tracking-wider" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);color:hsl(var(--primary));">{{ eyebrowText }}</span><span class="h-px w-8" style="background-color:hsl(var(--accent));"></span></div>{% endif %}
{% if headlineText %}<h1 class="text-4xl sm:text-5xl lg:text-6xl font-semibold leading-tight" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ headlineText }}</h1>{% endif %}
{% if subText %}<h2 class="mt-5 text-xl sm:text-2xl max-w-2xl{% if align == "center" %} mx-auto{% endif %}" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ subText }}</h2>{% endif %}
{% if descText %}<p class="mt-4 max-w-xl leading-relaxed{% if align == "center" %} mx-auto{% endif %}" style="color:hsl(var(--muted-foreground));">{{ descText }}</p>{% endif %}
{% if pText or sText %}<div class="mt-8 flex flex-col sm:flex-row gap-4{% if align == "center" %} justify-center{% endif %}">
{% if pText and pUrl %}<a href="{{ pUrl }}" class="earthen-cta inline-flex items-center justify-center gap-2 px-6 py-3 font-semibold">{{ pText }}</a>{% endif %}
{% if sText and sUrl %}<a href="{{ sUrl }}" class="inline-flex items-center justify-center rounded-lg px-6 py-3 font-semibold transition-colors hover:bg-muted" style="border:1.5px solid hsl(var(--border));color:hsl(var(--primary));">{{ sText }}</a>{% endif %}
</div>{% endif %}
{% if trustLogos %}<div class="mt-10">
<p class="text-xs uppercase tracking-wider mb-3" style="color:hsl(var(--muted-foreground));">In good company</p>
<div class="flex flex-wrap items-center gap-6{% if align == "center" %} justify-center{% endif %}">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}" target="_blank" rel="noopener noreferrer" class="opacity-60 transition-opacity hover:opacity-100">{% img logo.image alt=logo.alt class="h-8 w-auto" %}</a>{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-60" %}{% endif %}{% endfor %}</div>
</div>{% endif %}
{% endmacro %}
{% if variant == "split" %}
<section data-block-override="earthen:hero" class="hero grid grid-cols-1 lg:grid-cols-2 overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="relative min-h-[320px] bg-muted bg-paper-grain {% if mediaPosition == "left" %}order-1 lg:order-1{% else %}order-1 lg:order-2{% endif %}">{% if hasImage %}{% img backgroundImage alt=headlineText class="absolute inset-0 h-full w-full object-cover" layout="hero" %}{% endif %}</div>
<div class="flex flex-col justify-center px-6 py-16 lg:px-12 {% if mediaPosition == "left" %}order-2 lg:order-2{% else %}order-2 lg:order-1{% endif %}"><div class="max-w-lg text-left">{{ herocontent("left") }}</div></div>
</section>
{% else %}
{% set align = "center" %}
{% if variant == "left-aligned" %}{% set align = "left" %}{% endif %}
<section data-block-override="earthen:hero" class="hero relative flex flex-col overflow-hidden bg-background text-foreground bg-paper-grain {% if minHeight == "screen" %}min-h-screen{% elif minHeight == "50vh" %}min-h-[50vh]{% elif minHeight == "auto" %}min-h-[400px]{% else %}min-h-[75vh]{% endif %} {% if verticalAlign == "top" %}justify-start pt-20{% elif verticalAlign == "bottom" %}justify-end pb-20{% else %}justify-center{% endif %}{% if class %} {{ class }}{% endif %}">
{% if hasImage %}<div class="absolute inset-0 z-0">{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}{% if showOverlay %}<div class="absolute inset-0 {% if overlayColor == "light" %}bg-background/60{% elif overlayColor == "primary" %}bg-primary/50{% elif overlayOpacity <= 30 %}bg-black/30{% elif overlayOpacity <= 50 %}bg-black/50{% elif overlayOpacity <= 70 %}bg-black/70{% else %}bg-black/80{% endif %}"></div>{% endif %}</div>{% endif %}
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 sm:px-6 lg:px-8 {% if variant == "left-aligned" %}text-left{% else %}text-center{% endif %}">{{ herocontent(align) }}</div>
</section>
{% endif %}

View File

@ -0,0 +1,31 @@
{# hours-location override — cream hours ledger + soft-cornered map facade. Map facade <button onclick> preserved verbatim. Fields mirror builtin: heading, hours[].day/.hours/.closed, specialHoursNote, address, phone, directionsUrl, mapEmbedUrl, mapButtonLabel. #}
<section data-block-override="earthen:hours-location" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-5xl px-4">
{% if heading %}<h2 class="text-3xl md:text-4xl font-semibold text-center" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading }}</h2>{% endif %}
<div class="mt-10 grid grid-cols-1 gap-10 md:grid-cols-2">
<div class="earthen-panel earthen-blob-soft p-6 md:p-8">
{% if hours %}
<table class="w-full text-sm">
<tbody class="divide-y" style="border-color:hsl(var(--border));">
{% for row in hours %}
<tr style="border-color:hsl(var(--border));">
<th scope="row" class="py-2 pr-4 text-left text-xs font-semibold uppercase tracking-wide" style="color:hsl(var(--foreground));">{{ row.day }}</th>
<td class="py-2 text-right tabular-nums {% if row.closed %}italic{% endif %}" style="{% if row.closed %}color:hsl(var(--muted-foreground));{% else %}color:hsl(var(--accent));{% endif %}">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if specialHoursNote %}<p class="mt-4 border-t pt-3 text-sm italic" style="border-color:hsl(var(--border));color:hsl(var(--muted-foreground));">{{ specialHoursNote }}</p>{% endif %}
{% if address %}<div class="mt-6"><div class="text-xs font-semibold uppercase tracking-wide" style="color:hsl(var(--primary));">Address</div><address class="mt-1 whitespace-pre-line not-italic" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ address }}</address></div>{% endif %}
{% if phone %}<div class="mt-4"><div class="text-xs font-semibold uppercase tracking-wide" style="color:hsl(var(--primary));">Phone</div><a href="tel:{{ phone }}" class="earthen-link mt-1 inline-block">{{ phone }}</a></div>{% endif %}
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener noreferrer" class="earthen-cta mt-6 inline-flex items-center gap-2 px-5 py-2.5 font-semibold">::lucide:navigation:sm:: Get directions</a>{% endif %}
</div>
{% if mapEmbedUrl %}
<div class="earthen-blob-soft aspect-square w-full overflow-hidden bg-muted md:aspect-auto">
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('class','h-full w-full border-0');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" class="flex h-full min-h-[16rem] w-full items-center justify-center gap-2 text-sm font-semibold transition-colors hover:bg-accent hover:text-accent-foreground" style="color:hsl(var(--primary));">::lucide:map-pin:md:: {{ mapButtonLabel|default:"Load map" }}</button>
</div>
{% endif %}
</div>
</div>
</section>

View File

@ -1 +1,11 @@
<figure class="earthen-image my-8 mx-auto" style="max-width: 100%;">{% if src|default:url %}<div class="earthen-paper-frame-inner rounded-lg overflow-hidden" style="border: 1px solid hsl(var(--border)); padding: 6px; background-color: hsl(var(--card));"><img src="{{ src|default:url }}" alt="{{ alt }}" class="w-full h-auto rounded-md"></div>{% else %}<div data-empty="true" class="earthen-image-empty w-full h-48 rounded-lg flex items-center justify-center" style="background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); border: 1.5px dashed hsl(var(--border));"><span class="text-sm">Choose an image to display here</span></div>{% endif %}{% if caption %}<figcaption class="earthen-caption text-center text-sm mt-3" style="color: hsl(var(--muted-foreground)); font-style: italic;">{{ caption }}</figcaption>{% endif %}</figure> {# image override — recycled-paper frame with soft organic corners. Fields mirror builtin: src, alt, caption, size, align, aspectRatio, objectFit. Media resolved via {% img %} (media filter is not registered in the block render path). #}
{% with size=size|default:"full" align=align|default:"center" objectFit=objectFit|default:"cover" %}
<figure data-block-override="earthen:image" class="my-8 flex-1 {% if align == "left" %}flex justify-start{% elif align == "right" %}flex justify-end{% else %}flex justify-center{% endif %}{% if class %} {{ class }}{% endif %}">
<div class="earthen-paper-frame-inner earthen-blob-soft overflow-hidden {% if size == "xs" %}max-w-[200px] w-full{% elif size == "sm" %}max-w-[400px] w-full{% elif size == "md" %}max-w-[600px] w-full{% elif size == "lg" %}max-w-[800px] w-full{% else %}w-full{% endif %}" style="border:1px solid hsl(var(--border));padding:6px;background-color:hsl(var(--card));">
<div class="earthen-blob-soft relative overflow-hidden bg-muted {% if aspectRatio == "square" %}aspect-square{% elif aspectRatio == "video" %}aspect-video{% elif aspectRatio == "portrait" %}aspect-[3/4]{% elif aspectRatio == "wide" %}aspect-[21/9]{% endif %}">
{% if objectFit == "contain" %}{% img src alt=alt class="w-full h-full object-contain" layout=size %}{% elif objectFit == "fill" %}{% img src alt=alt class="w-full h-full object-fill" layout=size %}{% elif objectFit == "none" %}{% img src alt=alt class="w-full h-full object-none" layout=size %}{% else %}{% img src alt=alt class="w-full h-full object-cover" layout=size %}{% endif %}
</div>
</div>
{% if caption %}<figcaption class="mt-3 text-center text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ caption }}</figcaption>{% endif %}
</figure>
{% endwith %}

View File

@ -0,0 +1,18 @@
{# logo-strip override — earthen: grayscale-at-rest, colour-on-hover partner logos (partner_logos DNA via .earthen-partner-* classes). Field reads, the {% if logo.url %} anchor split, the {% if grayscale %} branch, marquee classes and the {% if marquee %}<style> keyframes are preserved verbatim from the builtin; only wrapper classes/style reskinned. #}
<section data-block-override="earthen:logo-strip" class="py-12 md:py-16 bg-paper-grain text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<div class="mb-8 flex items-center justify-center gap-3"><span class="h-px w-8" style="background-color:hsl(var(--accent));"></span><p class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">{{ heading }}</p><span class="h-px w-8" style="background-color:hsl(var(--accent));"></span></div>{% endif %}
<div class="mt-8 {% if marquee %}group relative overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]{% endif %}">
<div class="flex flex-wrap items-center justify-center gap-x-10 gap-y-6 {% if marquee %}flex-nowrap [animation:logo-marquee_30s_linear_infinite] motion-reduce:[animation:none] group-hover:[animation-play-state:paused]{% endif %}">
{% for logo in logos %}
<div class="earthen-partner-tile flex shrink-0 items-center">
{% if logo.url %}<a href="{{ logo.url }}" class="earthen-partner-link block opacity-80 transition hover:opacity-100">{% endif %}
<span class="earthen-partner-img inline-flex h-8 items-center{% if grayscale %} grayscale transition-[filter] hover:grayscale-0{% endif %}">{% img logo.image alt=logo.alt class="h-8 w-auto object-contain" %}</span>
{% if logo.url %}</a>{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</section>
{% if marquee %}<style>@keyframes logo-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}</style>{% endif %}

View File

@ -0,0 +1,30 @@
{# menu-list override — a field-kitchen menu card: Fraunces section titles, dotted leader rules, terracotta prices. Fields mirror builtin: heading, intro, columns, sections[].title/.note/.items[].name/.badges/.description/.price, currency, footnote. #}
<section data-block-override="earthen:menu-list" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-5xl px-4 text-center">
{% if heading %}<h2 class="text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-3 max-w-2xl text-lg italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ intro }}</p>{% endif %}
<div class="mx-auto mt-6 mb-4 flex justify-center" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="30" height="30" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" style="color:hsl(var(--primary));"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg></div>
<div class="mt-6 grid grid-cols-1 gap-x-14 gap-y-12 text-left {% if columns == 2 %}md:grid-cols-2{% endif %}">
{% for section in sections %}
<div class="menu-section">
{% if section.title %}<h3 class="border-b pb-2 text-center text-xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));border-color:hsl(var(--border));">{{ section.title }}</h3>{% endif %}
{% if section.note %}<p class="mt-2 text-center text-sm italic" style="color:hsl(var(--muted-foreground));">{{ section.note }}</p>{% endif %}
<ul class="mt-5 space-y-5">
{% for item in section.items %}
<li>
<div class="flex items-baseline gap-3">
<span class="shrink-0 font-medium" style="color:hsl(var(--foreground));">{{ item.name }}</span>
<span class="min-w-6 flex-1 translate-y-[-2px] border-b border-dotted" style="border-color:hsl(var(--primary)/0.5);" aria-hidden="true"></span>
{% if item.price %}<span class="shrink-0 font-semibold tabular-nums" style="color:hsl(var(--accent));">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
</div>
{% if item.badges %}<div class="mt-1 flex flex-wrap gap-1.5">{% for badge in item.badges %}{% if badge.label %}<span class="inline-block rounded-full border px-2 py-0.5 text-[0.65rem] uppercase tracking-wide" style="color:hsl(var(--muted-foreground));border-color:hsl(var(--border));">{{ badge.label }}</span>{% endif %}{% endfor %}</div>{% endif %}
{% if item.description %}<p class="mt-1 text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ item.description }}</p>{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% if footnote %}<p class="mt-12 text-xs" style="color:hsl(var(--muted-foreground));">{{ footnote }}</p>{% endif %}
</div>
</section>

View File

@ -0,0 +1,104 @@
{# navbar override — earthen botanical masthead. Fixed drawer ids (bn-navbar-toggle/drawer/overlay), focus-trap + scroll-lock JS and all data-bn-* wiring preserved verbatim from the builtin; only classes/style/fonts reskinned. Fields identical: variant/stickyBehavior/megaMenu/logoType/logoText/logo/companyName/cta/menu.items/... #}
{% set nv = variant %}
{% if not nv %}{% if template == "centered" %}{% set nv = "centered-logo" %}{% elif template == "transparent" %}{% set nv = "transparent-over-hero" %}{% else %}{% set nv = "standard" %}{% endif %}{% endif %}
{% set sb = stickyBehavior %}
{% if not sb %}{% if sticky %}{% set sb = "sticky" %}{% else %}{% set sb = "none" %}{% endif %}{% endif %}
{% set use_mega = megaMenu or template == "mega" %}
{% set is_transparent = nv == "transparent-over-hero" %}
{% set cta_text = cta.text|default:ctaLabel %}
{% set cta_url = cta.url|default:ctaUrl %}
{% set bg = backgroundColor|default:"bg-background" %}
{% macro navlink(item) %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} class="px-3 py-2 text-sm font-medium text-foreground/75 hover:text-primary transition-colors {{ item.css_class }}" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ item.label }}</a>{% endmacro %}
{% macro dropdown(item) %}<div class="relative group">
<button type="button" class="px-3 py-2 text-sm font-medium text-foreground/75 hover:text-primary transition-colors inline-flex items-center gap-1" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ item.label }}<svg class="h-4 w-4 transition-transform group-hover:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg></button>
{% if use_mega %}<div class="absolute left-1/2 -translate-x-1/2 mt-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50"><div class="earthen-panel rounded-2xl p-6 grid gap-8" style="grid-template-columns:repeat({{ item.children|length|default:1 }},minmax(10rem,1fr));min-width:min(90vw,42rem)">{% for child in item.children %}<div class="space-y-1">{% if child.item_type == "header" %}<div class="px-2 py-1 text-[0.65rem] font-semibold uppercase tracking-wider text-primary border-b border-border mb-2">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-2 py-2 rounded-lg hover:bg-muted transition-colors {{ child.css_class }}">{% if child.icon %}<span class="mr-1.5 text-primary">::{{ child.icon }}::</span>{% endif %}<span class="text-sm font-medium text-foreground">{{ child.label }}</span>{% if child.description %}<span class="block text-xs text-muted-foreground mt-0.5">{{ child.description }}</span>{% endif %}</a>{% endif %}</div>{% endfor %}</div></div>
{% else %}<div class="absolute left-0 mt-2 w-56 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50"><div class="earthen-panel rounded-2xl py-2">{% for child in item.children %}{% if child.item_type == "header" %}<div class="px-4 py-2 text-[0.65rem] font-semibold uppercase tracking-wider text-primary">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-4 py-2 text-sm text-foreground hover:bg-muted transition-colors {{ child.css_class }}">{{ child.label }}</a>{% endif %}{% endfor %}</div></div>{% endif %}
</div>{% endmacro %}
{% macro brand(size) %}<a href="/" class="flex-shrink-0 flex items-center">{% if logoType == "text" and logoText.text %}<span class="text-xl font-semibold {{ logoText.color }}" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ logoText.text }}</span>{% elif logo %}<img src="{{ logo }}" alt="{{ alt|default:companyName }}" class="{{ size }} w-auto"{% if logoMaxWidth or logoMaxHeight %} style="{% if logoMaxWidth %}max-width:{{ logoMaxWidth }};{% endif %}{% if logoMaxHeight %}max-height:{{ logoMaxHeight }};{% endif %}"{% endif %}/>{% elif companyName %}<span class="text-xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ companyName }}</span>{% endif %}</a>{% endmacro %}
{% macro ctabtn(extra) %}{% if cta_text %}<a href="{{ cta_url|default:'#' }}" class="earthen-cta inline-flex items-center px-5 py-2.5 text-sm font-semibold {{ extra }}">{{ cta_text }}</a>{% endif %}{% endmacro %}
<nav data-block-override="earthen:navbar" data-bn-navbar data-bn-nav-variant="{{ nv }}" data-bn-sticky="{{ sb }}"{% if is_transparent %} data-bn-transparent="1"{% endif %} class="bn-navbar w-full {% if sb == "sticky" or sb == "shrink-on-scroll" %}sticky top-0 z-40 {% endif %}{% if is_transparent %}absolute top-0 left-0 z-40 text-white{% else %}{{ bg }} border-b border-border{% endif %} {{ class }}">
{% if showUtilityBar %}<div class="w-full text-xs {% if not showOnMobile %}hidden sm:block{% endif %} bg-muted text-muted-foreground border-b border-border"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between h-8"><div>{{ utilityBarText|safe }}</div><div class="flex items-center gap-4">{% if utilityBarPhone %}<a href="tel:{{ utilityBarPhone }}" class="hover:text-primary transition-colors">{{ utilityBarPhone }}</a>{% endif %}{% if utilityBarEmail %}<a href="mailto:{{ utilityBarEmail }}" class="hover:text-primary transition-colors">{{ utilityBarEmail }}</a>{% endif %}</div></div></div>{% endif %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bn-navbar-row flex items-center justify-between h-16">
{% if nv == "centered-logo" %}
<div class="hidden md:flex items-center gap-1 flex-1">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}</div>
<div class="flex items-center justify-center">{{ brand("h-9") }}</div>
<div class="hidden md:flex items-center gap-3 flex-1 justify-end">{{ ctabtn("") }}</div>
{% elif nv == "split" %}
<div class="hidden md:flex items-center gap-1 flex-1">{% for item in menu.items %}{% if forloop.Counter0 * 2 < menu.items|length %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endif %}{% endfor %}</div>
<div class="flex items-center justify-center">{{ brand("h-9") }}</div>
<div class="hidden md:flex items-center gap-1 flex-1 justify-end">{% for item in menu.items %}{% if forloop.Counter0 * 2 >= menu.items|length %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}</div>
{% else %}
<div class="flex items-center">{{ brand("h-8") }}</div>
<div class="hidden md:flex items-center gap-1">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}</div>
{% endif %}
<div class="flex items-center md:hidden">
<label for="bn-navbar-toggle" data-bn-nav-open aria-controls="bn-navbar-drawer" aria-expanded="false" class="inline-flex items-center justify-center p-2 rounded-lg cursor-pointer hover:bg-muted transition-colors"><span class="sr-only">Open menu</span><svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></label>
</div>
</div>
</div>
{# --- Mobile off-canvas drawer: pure-CSS checkbox baseline (works with no JS) --- #}
<input type="checkbox" id="bn-navbar-toggle" class="bn-navbar-toggle" aria-hidden="true"/>
<label for="bn-navbar-toggle" class="bn-navbar-overlay fixed inset-0 bg-black/50 z-40 md:hidden" aria-hidden="true"></label>
<div id="bn-navbar-drawer" class="bn-navbar-drawer fixed top-0 right-0 h-full w-80 max-w-[85vw] bg-background text-foreground z-50 shadow-xl md:hidden flex flex-col border-l border-border" role="dialog" aria-modal="true" aria-label="Site menu">
<div class="flex items-center justify-between p-4 border-b border-border">{{ brand("h-8") }}<label for="bn-navbar-toggle" data-bn-nav-close class="p-2 rounded-lg cursor-pointer text-muted-foreground hover:text-primary hover:bg-muted"><span class="sr-only">Close menu</span><svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></label></div>
<nav class="flex-1 overflow-y-auto py-4" aria-label="Mobile">{% for item in menu.items %}{% if item.children %}<details class="group/acc"><summary class="flex items-center justify-between px-6 py-3 text-base font-medium cursor-pointer hover:bg-muted transition-colors list-none">{{ item.label }}<svg class="h-4 w-4 transition-transform group-open/acc:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg></summary><div class="pl-8 pb-2">{% for child in item.children %}{% if child.item_type != "header" %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-2 py-2 text-sm text-muted-foreground hover:text-primary transition-colors">{{ child.label }}</a>{% endif %}{% endfor %}</div></details>{% else %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-6 py-3 text-base font-medium hover:bg-muted transition-colors">{{ item.label }}</a>{% endif %}{% endfor %}</nav>
{% if cta_text %}<div class="p-4 border-t border-border"><a href="{{ cta_url|default:'#' }}" class="earthen-cta block w-full text-center px-4 py-3 text-base font-semibold">{{ cta_text }}</a></div>{% endif %}
</div>
</nav>
<style>
.bn-navbar-toggle{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;}
.bn-navbar-drawer{transform:translateX(100%);transition:transform .3s ease;}
.bn-navbar-overlay{opacity:0;pointer-events:none;transition:opacity .3s ease;}
.bn-navbar-toggle:checked ~ .bn-navbar-drawer{transform:translateX(0);}
.bn-navbar-toggle:checked ~ .bn-navbar-overlay{opacity:1;pointer-events:auto;}
.bn-navbar-drawer details > summary::-webkit-details-marker{display:none;}
[data-bn-sticky="shrink-on-scroll"].bn-navbar-shrunk .bn-navbar-row{height:3rem;}
[data-bn-sticky="shrink-on-scroll"] .bn-navbar-row{transition:height .2s ease;}
[data-bn-transparent].bn-navbar-solid{position:sticky;color:inherit;background:hsl(var(--background));border-bottom:1px solid hsl(var(--border));}
</style>
<script>
(function(){
if(window.__bnNavInit)return;window.__bnNavInit=1;
function ready(fn){if(document.readyState!='loading')fn();else document.addEventListener('DOMContentLoaded',fn);}
ready(function(){
document.querySelectorAll('[data-bn-navbar]').forEach(function(nav){
var toggle=nav.querySelector('.bn-navbar-toggle');
var drawer=nav.querySelector('.bn-navbar-drawer');
var opener=nav.querySelector('[data-bn-nav-open]');
if(toggle&&drawer){
function focusables(){return drawer.querySelectorAll('a[href],button,label[for],input,summary,[tabindex]:not([tabindex="-1"])');}
toggle.addEventListener('change',function(){
var open=toggle.checked;
document.documentElement.style.overflow=open?'hidden':'';
document.body.style.overflow=open?'hidden':'';
if(opener)opener.setAttribute('aria-expanded',open?'true':'false');
if(open){var f=focusables();if(f.length)f[0].focus();}
else if(opener&&opener.focus)opener.focus();
});
drawer.addEventListener('keydown',function(e){
if(e.key==='Escape'){toggle.checked=false;toggle.dispatchEvent(new Event('change'));return;}
if(e.key!=='Tab')return;
var f=focusables();if(!f.length)return;
var first=f[0],last=f[f.length-1];
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}
});
drawer.querySelectorAll('a[href]').forEach(function(a){a.addEventListener('click',function(){toggle.checked=false;toggle.dispatchEvent(new Event('change'));});});
}
var sb=nav.getAttribute('data-bn-sticky');
var transparent=nav.getAttribute('data-bn-transparent')==='1';
if(sb==='shrink-on-scroll'||transparent){
var overHero=false;
if(transparent){var next=nav.nextElementSibling;overHero=!!next&&next.getBoundingClientRect().height>=240;if(!overHero)nav.classList.add('bn-navbar-solid');}
var onScroll=function(){
var y=window.scrollY||window.pageYOffset;
if(sb==='shrink-on-scroll')nav.classList.toggle('bn-navbar-shrunk',y>40);
if(transparent&&overHero)nav.classList.toggle('bn-navbar-solid',y>40);
};
window.addEventListener('scroll',onScroll,{passive:true});onScroll();
}
});
});
})();
</script>

View File

@ -0,0 +1,11 @@
{# newsletter-signup (earthen) — the {% signup_form %} tag carries the untouchable subscribe wiring (/api/lists/subscribe HTMX post + honeypot); this template supplies only the earthen chrome around it. #}
<section data-block-override="earthen:newsletter-signup" class="bg-paper-grain py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4">
<div class="earthen-panel earthen-blob-soft {% if layout == "inline" %}bg-primary px-6 py-12 text-primary-foreground md:px-12{% else %}px-6 py-12 text-card-foreground md:px-10{% endif %} text-center">
<svg class="mx-auto mb-4 h-9 w-9 {% if layout == "inline" %}text-primary-foreground{% else %}earthen-glyph{% endif %}" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><ellipse cx="24" cy="24" rx="6" ry="10"></ellipse><path d="M24 14V6M18 18l-6-6M30 18l6-6M24 34v8M18 30l-6 6M30 30l6 6"></path></svg>
{% if title %}<h2 class="text-2xl md:text-3xl" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">{{ title }}</h2>{% endif %}
{% if description %}<p class="mx-auto mt-4 max-w-xl {% if layout == "inline" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ description }}</p>{% endif %}
<div class="mx-auto mt-6 max-w-md">{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}</div>
</div>
</div>
</section>

View File

@ -0,0 +1,21 @@
{# page-suggestions override — themed 404 suggestions. Wiring UNTOUCHABLE: fields title, showDescription, emptyMessage; suggestions[] from the provider (.slug/.title/.excerpt). Only classes/styles/copy reskinned to earthen. #}
{% if suggestions %}
<div data-block-override="earthen:page-suggestions" class="page-suggestions py-10">
{% if title %}<h2 class="crayon-underline mx-auto w-fit text-xl text-center" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</h2>{% endif %}
<ul class="mt-6 space-y-3 max-w-lg mx-auto">
{% for suggestion in suggestions %}
<li class="flex items-start gap-3">
<span class="mt-0.5 shrink-0" style="color:hsl(var(--primary));">::lucide:chevron-right:sm::</span>
<div>
<a href="{{ suggestion.slug }}" class="earthen-link font-medium">{{ suggestion.title }}</a>
{% if showDescription and suggestion.excerpt %}<p class="mt-1 text-sm" style="color:hsl(var(--muted-foreground));">{{ suggestion.excerpt }}</p>{% endif %}
</div>
</li>
{% endfor %}
</ul>
</div>
{% elif emptyMessage %}
<div data-block-override="earthen:page-suggestions" class="page-suggestions py-10 text-center">
<p style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ emptyMessage }}</p>
</div>
{% endif %}

View File

@ -0,0 +1,14 @@
{# password-reset-form override — request stage only, matching builtin. Wiring UNTOUCHABLE: /api/auth/request-password-reset (email), hx-target #reset-message, #request-reset-panel. Only classes/styles/copy reskinned to earthen. #}
<div data-block-override="earthen:password-reset-form" class="password-reset-form earthen-panel mx-auto max-w-md p-8">
<div id="reset-message"></div>
<div id="request-reset-panel" class="text-center">
<svg aria-hidden="true" class="mx-auto mb-3 h-10 w-10" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" style="color:hsl(var(--primary));"><path d="M24 4v18"></path><path d="M24 22c-4 4-9 5-13 4M24 22c4 4 9 5 13 4M24 22c-2 7-3 12-5 16M24 22c2 7 3 12 5 16M24 22c0 9-1 15-1 20"></path><circle cx="24" cy="22" r="2"></circle></svg>
<h2 class="text-2xl crayon-underline w-fit mx-auto" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">Reset password</h2>
<p class="mt-3 text-sm" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">Enter your email and we will send a link to reset your password.</p>
<form hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML" class="mt-6 text-left">
<div class="mb-4"><label class="block text-xs font-medium mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:border-[hsl(var(--primary))] focus:ring-1 focus:ring-ring" /></div>
<button type="submit" class="earthen-cta w-full" hx-disabled-elt="this">Send reset link</button>
</form>
<p class="text-center mt-4 text-sm"><a href="/login" class="earthen-link">Back to sign in</a></p>
</div>
</div>

View File

@ -0,0 +1,16 @@
{# popular-posts (earthen) — compact ranked list from the `posts` provider. Loops, rank counter and image wiring preserved; earthen skin only. #}
<section data-block-override="earthen:popular-posts" class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="mb-5 text-sm font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading }}</h2>{% endif %}
<ol class="space-y-4">
{% for post in posts %}
<li class="flex items-start gap-3 border-t border-border pt-4">
{% if showRank %}<span class="mt-0.5 text-lg font-bold tabular-nums text-accent" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">{{ forloop.Counter }}</span>{% endif %}
{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="earthen-blob-soft h-12 w-12 flex-shrink-0 overflow-hidden bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover" %}</a>{% endif %}
<div class="min-w-0">
<h3 class="text-sm font-medium leading-snug" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h3>
{% if post.published_at %}<p class="mt-1 text-xs text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
</div>
</li>
{% endfor %}
</ol>
</section>

View File

@ -0,0 +1,22 @@
{# post-hero (earthen) — current post from the `post` provider. Banner when a featured image exists, else a clean text hero. Branches, field reads and image wiring preserved; earthen skin only. #}
{% set center = align != "left" %}
{% if post.featured_image_url %}
<section data-block-override="earthen:post-hero" class="hero relative flex min-h-[50vh] flex-col justify-end overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="absolute inset-0 z-0">{% img post.featured_image_url alt=post.featured_image_alt class="h-full w-full object-cover" layout="hero" %}<div class="absolute inset-0 bg-black/60"></div><div class="bg-paper-grain absolute inset-0 opacity-30"></div></div>
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 py-14 sm:px-6 lg:px-8 {% if center %}text-center{% endif %}">
{% if center %}<svg class="mx-auto mb-4 h-9 w-9 text-white/90" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>{% endif %}
<h1 class="text-3xl leading-tight text-white sm:text-4xl lg:text-5xl" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);">{{ post.title }}</h1>
{% if showExcerpt and post.excerpt %}<p class="mt-5 max-w-2xl text-lg text-white/80{% if center %} mx-auto{% endif %}" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ post.excerpt }}</p>{% endif %}
{% if showMeta %}<div class="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-white/80{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="font-medium text-white">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}</div>{% endif %}
</div>
</section>
{% else %}
<section data-block-override="earthen:post-hero" class="bg-paper-grain py-16 text-foreground md:py-20{% if class %} {{ class }}{% endif %}">
<div class="mx-auto w-full max-w-3xl px-4 {% if center %}text-center{% endif %}">
{% if center %}<svg class="earthen-glyph mx-auto mb-4 h-9 w-9" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg>{% endif %}
<h1 class="{% if center %}crayon-underline inline-block {% endif %}text-3xl leading-tight sm:text-4xl lg:text-5xl" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ post.title }}</h1>
{% if showExcerpt and post.excerpt %}<p class="mt-5 text-lg text-muted-foreground{% if center %} mx-auto max-w-2xl{% endif %}" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ post.excerpt }}</p>{% endif %}
{% if showMeta %}<div class="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="font-medium text-accent">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,9 @@
{# post-metadata (earthen) — author/date/reading-time row from the `post` provider. Guards, field reads, loops and image wiring preserved; earthen skin only. #}
{% if post %}
<div data-block-override="earthen:post-metadata" class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground{% if class %} {{ class }}{% endif %}">
{% if showAuthor and post.author_name %}<span class="flex items-center gap-2">{% if showAvatar and post.author_avatar %}{% img post.author_avatar alt=post.author_name class="earthen-blob-soft h-7 w-7 object-cover" %}{% endif %}<span class="font-medium text-accent">{{ post.author_name }}</span></span>{% endif %}
{% if showDate and post.published_at %}<time datetime="{{ post.published_at|date:"Y-m-d" }}">{{ post.published_at|date:"F j, Y" }}</time>{% endif %}
{% if showReadingTime and post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
{% if post.categories %}<span class="flex flex-wrap gap-1.5">{% for cat in post.categories %}<a href="{{ cat.url|default:'#' }}" class="rounded-full border border-border px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:text-primary">{{ cat.name }}</a>{% endfor %}</span>{% endif %}
</div>
{% endif %}

View File

@ -0,0 +1,26 @@
{# pricing override — earthen: paper ground, soft botanical tier cards, terracotta prices. Field reads, highlighted/tier control flow, features loop and check-icon <use> preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:pricing" class="py-16 md:py-24 bg-paper-grain text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="text-center text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary))">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ intro }}</p>{% endif %}
<div class="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for tier in tiers %}
<div class="relative flex flex-col earthen-panel earthen-blob-soft p-6"{% if tier.highlighted %} style="box-shadow: inset 0 0 0 2px hsl(var(--accent));"{% endif %}>
{% if tier.badge %}<span class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wide" style="background-color:hsl(var(--accent));color:hsl(var(--accent-foreground));">{{ tier.badge }}</span>{% endif %}
<h3 class="text-center text-lg font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground))">{{ tier.name }}</h3>
<div class="mx-auto my-3 h-px w-12" style="background-color:hsl(var(--border));"></div>
<p class="flex items-baseline justify-center gap-1">
<span class="text-4xl font-bold tracking-tight" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--accent))">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>
{% if tier.period %}<span class="text-sm text-muted-foreground">{{ tier.period }}</span>{% endif %}
</p>
{% if tier.description %}<p class="mt-3 text-center text-sm text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ tier.description }}</p>{% endif %}
{% if tier.features %}<ul class="mt-6 flex-1 space-y-3 text-sm">
{% for feature in tier.features %}<li class="flex items-start gap-2"><svg class="mt-0.5 h-4 w-4 shrink-0 text-primary" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg><span>{{ feature }}</span></li>{% endfor %}
</ul>{% endif %}
{% if tier.ctaUrl and tier.ctaLabel %}{% if tier.highlighted %}<a href="{{ tier.ctaUrl }}" class="earthen-cta mt-8 inline-flex w-full items-center justify-center px-5 py-3 font-semibold">{{ tier.ctaLabel }}</a>{% else %}<a href="{{ tier.ctaUrl }}" class="mt-8 inline-flex w-full items-center justify-center rounded-lg px-5 py-3 font-semibold transition-colors hover:bg-muted" style="border:1.5px solid hsl(var(--border));color:hsl(var(--primary));">{{ tier.ctaLabel }}</a>{% endif %}{% endif %}
</div>
{% endfor %}
</div>
{% if billingNote %}<p class="mt-8 text-center text-sm text-muted-foreground">{{ billingNote }}</p>{% endif %}
</div>
</section>

View File

@ -0,0 +1,8 @@
{# quote override — warm pull-quote: large italic Work Sans, moss rail, a small fern flourish when centred. Fields mirror builtin: quote, style, align, cite, attribution, role. Wiring preserved. #}
{% with style=style|default:"block" align=align|default:"left" %}
<figure data-block-override="earthen:quote" class="bn-quote my-10 {% if style == "inline-card" %}earthen-panel earthen-blob-soft p-8 md:p-10{% elif style == "pull" %}border-l-2 border-[hsl(var(--primary))] pl-6 md:pl-8 py-2{% else %}border-l-2 border-[hsl(var(--border))] pl-5 py-1{% endif %}{% if align == "center" %} text-center{% endif %}{% if class %} {{ class }}{% endif %}">
{% if align == "center" %}<div class="mx-auto mb-4 flex justify-center" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" style="color:hsl(var(--primary));"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg></div>{% endif %}
<blockquote{% if cite %} cite="{{ cite }}"{% endif %} class="{% if style == "pull" %}text-2xl md:text-3xl leading-snug{% else %}text-xl md:text-2xl leading-relaxed{% endif %} italic" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);color:hsl(var(--foreground));">{{ quote|safe }}</blockquote>
{% if attribution or role or cite %}<figcaption class="mt-4 text-sm{% if align == "center" %} flex flex-col items-center{% endif %}" style="color:hsl(var(--muted-foreground));">{% if attribution %}<span class="font-semibold" style="color:hsl(var(--primary));">{{ attribution }}</span>{% endif %}{% if role %}<span class="{% if attribution %}block {% endif %}italic">{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" class="earthen-link {% if attribution or role %}mt-1 {% endif %}inline-block text-xs">Source</a>{% endif %}</figcaption>{% endif %}
</figure>
{% endwith %}

View File

@ -0,0 +1,17 @@
{# related-posts (earthen) — grid from the `posts` provider. Guard, loops and image wiring preserved; earthen skin only. #}
{% if posts %}
<section data-block-override="earthen:related-posts" class="border-t border-border bg-paper-grain py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="mb-10 text-center"><svg class="earthen-glyph mx-auto mb-3 h-8 w-8" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg><h2 class="crayon-underline inline-block text-2xl md:text-3xl" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ heading|default:"More field notes" }}</h2></div>
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% else %}lg:grid-cols-3{% endif %}">
{% for post in posts %}
<article class="group flex flex-col text-center">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="earthen-blob-soft mb-4 block aspect-video overflow-hidden bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" %}</a>{% endif %}
<h3 class="text-base font-semibold leading-snug" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h3>
{% if post.published_at %}<p class="mt-2 text-xs text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
</article>
{% endfor %}
</div>
</div>
</section>
{% endif %}

View File

@ -0,0 +1,14 @@
{# rich-section override — earthen: paper ground, botanical eyebrow rule, Fraunces heading with crayon underline, organic media frame. Field reads, background/mediaPosition order control flow, body|safe and the {% img %} call preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:rich-section" class="py-16 md:py-24 {% if background == "muted" %}bg-muted text-foreground{% elif background == "card" %}bg-card text-card-foreground{% else %}bg-background text-foreground{% endif %}{% if class %} {{ class }}{% endif %}">
<div class="mx-auto grid max-w-6xl items-center gap-10 px-4 md:grid-cols-2 md:gap-16">
<div class="{% if mediaPosition == "left" %}md:order-2{% else %}md:order-1{% endif %}">
{% if eyebrow %}<div class="mb-3 flex items-center gap-3"><span class="h-px w-8" style="background-color:hsl(var(--accent));"></span><p class="text-xs font-semibold uppercase tracking-wider" style="color:hsl(var(--primary));">{{ eyebrow }}</p></div>{% endif %}
{% if heading %}<h2 class="mt-2 inline-block crayon-underline text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary))">{{ heading }}</h2>{% endif %}
{% if body %}<div class="mt-4 space-y-4 text-lg text-muted-foreground [&_a]:text-primary [&_a]:underline [&_strong]:text-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ body|safe }}</div>{% endif %}
{% if ctaUrl and ctaLabel %}<a href="{{ ctaUrl }}" class="earthen-cta mt-6 inline-flex items-center gap-2 px-5 py-3 font-semibold">{{ ctaLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
<div class="{% if mediaPosition == "left" %}md:order-1{% else %}md:order-2{% endif %}">
{% if media %}<div class="earthen-blob-soft earthen-paper-frame overflow-hidden">{% img media alt=mediaAlt class="w-full h-auto object-cover" %}</div>{% endif %}
</div>
</div>
</section>

View File

@ -0,0 +1,2 @@
{# rich-text override — long-form Work Sans editorial column, moss links + moss blockquote rail. Field mirrors builtin: html. Wiring preserved. #}
{% if html %}<article data-block-override="earthen:rich-text" class="rich-text prose prose-lg max-w-none [&_a]:text-primary [&_a]:no-underline [&_a]:border-b [&_a]:border-dotted [&_a]:border-[hsl(var(--primary)/0.6)] [&_a:hover]:text-accent [&_blockquote]:border-l-2 [&_blockquote]:border-[hsl(var(--primary))] [&_blockquote]:italic" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);color:hsl(var(--foreground));line-height:1.75;">{{ html|safe }}</article>{% endif %}

View File

@ -0,0 +1,13 @@
{# sidebar-nav (earthen) — vertical menu from the `menus` provider. Variants default | compact. highlight-current carried as a progressive-enhancement script keyed on data-bn-sidebar-link. Control flow, icon <use> and script preserved byte-for-byte; earthen skin only. #}
{% set compact = template == "compact" %}
<nav data-block-override="earthen:sidebar-nav" class="flex-1 {% if compact %}w-56 py-4 pr-3{% else %}w-64 py-6 pr-4{% endif %}{% if showBorder %} border-r border-border{% endif %}{% if class %} {{ class }}{% endif %}">
{% if title %}<h3 class="crayon-underline inline-block {% if compact %}px-2 mb-4 text-xs{% else %}px-3 mb-5 text-sm{% endif %} font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary));">{{ title }}</h3>{% endif %}
<ul class="{% if compact %}space-y-0.5{% else %}space-y-1{% endif %}">
{% for item in menu.items %}
<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link class="flex items-center rounded-lg text-sm text-muted-foreground transition-colors hover:text-primary hover:bg-muted {% if compact %}px-2 py-1.5{% else %}px-3 py-2{% endif %} {{ item.css_class }}">{% if item.icon %}<svg class="mr-2 h-4 w-4 text-accent" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg>{% endif %}{{ item.label }}</a></li>
{% endfor %}
</ul>
</nav>
{% if highlightCurrent %}<script>
(function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.classList.remove('text-muted-foreground');a.classList.add('text-foreground','bg-accent','font-medium');}});})();
</script>{% endif %}

View File

@ -0,0 +1,14 @@
{# social-links override — soft-cornered moss chips that warm to terracotta on hover. Fields mirror builtin: heading, style, links[].url/.label/.network. Icons via sprite <use>. Wiring preserved. #}
<div data-block-override="earthen:social-links" class="social-links{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="mb-4 text-xs font-semibold uppercase tracking-[0.16em]" style="color:hsl(var(--primary));">{{ heading }}</p>{% endif %}
<ul class="flex {% if style == "stack" %}flex-col gap-2{% else %}flex-wrap items-center gap-3{% endif %}">
{% for link in links %}{% if link.url %}
<li>
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" aria-label="{{ link.label|default:link.network }}" class="inline-flex items-center gap-2 rounded-xl border text-foreground transition-colors hover:bg-accent/10 hover:text-accent {% if style == "stack" %}w-full px-4 py-2.5{% else %}p-2.5{% endif %}" style="border-color:hsl(var(--border));">
{% if link.network %}<svg class="h-5 w-5" style="color:hsl(var(--primary));" aria-hidden="true"><use href="/icons/{{ link.network|split:":"|first }}.svg#{{ link.network|split:":"|last }}"></use></svg>{% endif %}
{% if style == "stack" and link.label %}<span class="text-xs font-semibold uppercase tracking-[0.12em]">{{ link.label }}</span>{% endif %}
</a>
</li>
{% endif %}{% endfor %}
</ul>
</div>

View File

@ -0,0 +1,50 @@
{# stats override — earthen: big terracotta numbers with a botanical fern flourish (impact-metrics DNA). Field reads, columns control flow, countUp data-stats/data-countup-value hooks and the entire count-up <script> preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:stats" class="py-16 md:py-24 bg-paper-grain text-foreground{% if class %} {{ class }}{% endif %}"{% if countUp %} data-stats{% endif %}>
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="text-center text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary))">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ intro }}</p>{% endif %}
<div class="mt-8 flex justify-center" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48" class="earthen-glyph" style="color:hsl(var(--primary));" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M24 44V8"></path><path d="M24 12c-4-2-7-2-9 0M24 12c4-2 7-2 9 0"></path><path d="M24 18c-6-2-10-1-12 1M24 18c6-2 10-1 12 1"></path><path d="M24 25c-7-1-12 0-14 3M24 25c7-1 12 0 14 3"></path><path d="M24 33c-7 0-11 2-12 5M24 33c7 0 11 2 12 5"></path></svg></div>
<dl class="mt-8 grid grid-cols-2 gap-8 {% if columns == 2 %}md:grid-cols-2{% elif columns == 3 %}md:grid-cols-3{% else %}md:grid-cols-4{% endif %}">
{% for item in items %}
<div class="px-2 text-center">
<dd class="text-4xl md:text-5xl font-semibold tracking-tight" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--accent))">{% if item.prefix %}{{ item.prefix }}{% endif %}<span{% if countUp %} data-countup-value="{{ item.value }}"{% endif %}>{{ item.value }}</span>{% if item.suffix %}{{ item.suffix }}{% endif %}</dd>
{% if item.label %}<dt class="mt-2 text-xs uppercase tracking-wider text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ item.label }}</dt>{% endif %}
</div>
{% endfor %}
</dl>
</div>
</section>
{% if countUp %}<script>
(function(){
var sections = document.querySelectorAll('[data-stats]');
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function animate(section){
var nodes = section.querySelectorAll('[data-countup-value]');
nodes.forEach(function(node){
var raw = node.getAttribute('data-countup-value') || '';
var target = parseFloat(raw.replace(/[^0-9.\-]/g, ''));
if (isNaN(target)) return;
var decimals = (raw.split('.')[1] || '').length;
var start = null, dur = 1200;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
node.textContent = (target * eased).toFixed(decimals);
if (p < 1) requestAnimationFrame(step);
else node.textContent = target.toFixed(decimals);
}
requestAnimationFrame(step);
});
}
sections.forEach(function(section){
if (reduce || !('IntersectionObserver' in window)) return;
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting){ animate(section); io.disconnect(); }
});
}, { threshold: 0.3 });
io.observe(section);
});
})();
</script>{% endif %}

View File

@ -0,0 +1,22 @@
{# team override — earthen: paper ground, soft botanical cards, organic member portraits. Field reads, layout/columns control flow, the {% img %} portrait call and the social-icon <use> split preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:team" class="py-16 md:py-24 bg-paper-grain text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="text-center text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary))">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ intro }}</p>{% endif %}
<div class="mt-12 {% if layout == "list" %}mx-auto max-w-3xl space-y-6{% elif layout == "single" %}mx-auto max-w-xl{% else %}grid grid-cols-1 gap-8 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}{% endif %}">
{% for member in members %}
<div class="{% if layout == "list" %}earthen-panel earthen-blob-soft flex items-start gap-5 p-5{% else %}text-center{% endif %}">
{% if member.photo %}<span class="earthen-blob {% if layout == "list" %}h-20 w-20{% else %}mx-auto h-28 w-28{% endif %} block shrink-0 overflow-hidden bg-muted">{% img member.photo alt=member.name class="h-full w-full object-cover" %}</span>{% endif %}
<div class="{% if layout != "list" %}mt-4{% endif %}">
{% if member.name %}<h3 class="text-lg font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground))">{{ member.name }}</h3>{% endif %}
{% if member.role %}<p class="text-xs font-semibold uppercase tracking-wider" style="color:hsl(var(--primary))">{{ member.role }}</p>{% endif %}
{% if member.bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ member.bio }}</p>{% endif %}
{% if member.socials %}<div class="mt-3 flex gap-3 {% if layout != "list" %}justify-center{% endif %}">
{% for social in member.socials %}{% if social.url %}<a href="{{ social.url }}" class="text-muted-foreground transition-colors hover:text-primary" aria-label="Social link"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ social.icon|split:":"|first }}.svg#{{ social.icon|split:":"|last }}"></use></svg></a>{% endif %}{% endfor %}
</div>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,28 @@
{# testimonial override — earthen: paper ground, soft botanical cards, organic avatar. Field reads, layout control flow, the five rating-star <use> conditionals and the {% img %} avatar call preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:testimonial" class="py-16 md:py-24 bg-paper-grain text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="text-center text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary))">{{ heading }}</h2>{% endif %}
<div class="mt-12 {% if layout == "single" %}mx-auto max-w-3xl{% elif layout == "carousel" %}flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4{% else %}grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3{% endif %}">
{% for q in quotes %}
<figure class="earthen-panel earthen-blob-soft flex flex-col p-6{% if layout == "carousel" %} min-w-[18rem] max-w-sm shrink-0 snap-start{% endif %}">
{% if q.rating %}<div class="mb-3 flex justify-center gap-0.5" aria-label="Rated {{ q.rating }} out of 5">
<svg class="h-4 w-4 {% if q.rating >= 1 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 2 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 3 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 4 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 5 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
</div>{% endif %}
<blockquote class="flex-1 text-center text-lg leading-relaxed" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{% if q.quote %}&ldquo;{{ q.quote }}&rdquo;{% endif %}</blockquote>
<div class="mx-auto my-4 h-px w-16" style="background-color:hsl(var(--accent));"></div>
<figcaption class="flex items-center justify-center gap-3">
{% if q.avatar %}<span class="earthen-blob h-10 w-10 shrink-0 overflow-hidden bg-muted">{% img q.avatar alt=q.name class="h-full w-full object-cover" %}</span>{% endif %}
<span class="text-center">
{% if q.name %}<span class="block text-sm font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground))">{{ q.name }}</span>{% endif %}
{% if q.role %}<span class="block text-sm text-muted-foreground">{{ q.role }}</span>{% endif %}
</span>
</figcaption>
</figure>
{% endfor %}
</div>
</div>
</section>

View File

@ -1 +1,2 @@
{% if variant == "lede" %}<div class="earthen-text earthen-lede drop-cap-host max-w-none flex-1 {{ class }}" style="font-family: var(--font-body, \000022Spectral\000022, Georgia, serif); color: hsl(var(--foreground));">{{ text|safe }}</div>{% else %}<div class="earthen-text max-w-none flex-1 {{ class }}" style="font-family: var(--font-body, \000022Spectral\000022, Georgia, serif); color: hsl(var(--foreground));">{{ text|safe }}</div>{% endif %} {# text override — Work Sans body prose with moss links that fill to terracotta on hover. Fields mirror builtin: text, proseClass. Wiring preserved. #}
<div data-block-override="earthen:text" class="prose max-w-none flex-1 [&_a]:text-primary [&_a]:no-underline [&_a]:border-b [&_a]:border-dotted [&_a]:border-[hsl(var(--primary)/0.6)] [&_a:hover]:text-accent [&_a:hover]:border-[hsl(var(--accent))] {{ proseClass }}" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);color:hsl(var(--foreground));line-height:1.75;">{{ text|safe }}</div>

View File

@ -0,0 +1,22 @@
{# timeline override — earthen: paper ground, soft botanical step markers, organic media. Field reads, orientation/marker control flow, forloop.counter, marker <use> flag and the {% img %} call preserved verbatim from the builtin; only classes/style/fonts reskinned. #}
<section data-block-override="earthen:timeline" class="py-16 md:py-24 bg-paper-grain text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4">
{% if heading %}<h2 class="text-center text-3xl md:text-4xl font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--primary))">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ intro }}</p>{% endif %}
<ol class="mt-12 {% if orientation == "horizontal" %}grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4{% else %}relative space-y-8 before:absolute before:left-4 before:top-2 before:h-[calc(100%-1rem)] before:w-px before:bg-border{% endif %}">
{% for step in steps %}
<li class="{% if orientation == "horizontal" %}text-center{% else %}relative pl-14{% endif %}">
<span class="flex h-8 w-8 items-center justify-center earthen-blob-soft text-sm font-semibold {% if orientation == "horizontal" %}mx-auto{% else %}absolute left-0 top-0 ring-4 ring-background{% endif %}" style="background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));">
{% if marker == "numbered" %}{{ forloop.counter }}{% elif marker == "dot" %}<span class="h-2 w-2 rounded-full bg-primary-foreground"></span>{% else %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#flag"></use></svg>{% endif %}
</span>
<div class="{% if orientation == "horizontal" %}mt-4{% else %}pt-1{% endif %}">
{% if step.label %}<p class="text-xs font-semibold uppercase tracking-wider" style="color:hsl(var(--primary))">{{ step.label }}</p>{% endif %}
{% if step.title %}<h3 class="mt-1 text-lg font-semibold" style="font-family:var(--font-heading, 'Fraunces', 'Playfair Display', Georgia, serif);color:hsl(var(--foreground))">{{ step.title }}</h3>{% endif %}
{% if step.text %}<p class="mt-1 text-muted-foreground" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif)">{{ step.text }}</p>{% endif %}
{% if step.media %}<div class="earthen-blob-soft mt-3 overflow-hidden earthen-paper-frame">{% img step.media alt=step.title class="w-full h-auto object-cover" %}</div>{% endif %}
</div>
</li>
{% endfor %}
</ol>
</div>
</section>

View File

@ -0,0 +1,10 @@
{# utility-bar override — earthen thin contact strip on warm oat. Fields mirror builtin: variant, showOnMobile, leftItems/rightItems {icon,label,link}; icons via the ::pack:name:: sprite shorthand. Only classes reskinned. #}
{% set ubcls = "bg-muted text-muted-foreground border-b border-border" %}
{% if variant == "primary" %}{% set ubcls = "bg-primary text-primary-foreground" %}{% elif variant == "dark" %}{% set ubcls = "bg-foreground text-background" %}{% endif %}
{% macro ubitem(it) %}{% if it.link %}<a href="{{ it.link }}" class="inline-flex items-center gap-1.5 hover:text-primary transition-colors">{% if it.icon %}::{{ it.icon }}::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</a>{% else %}<span class="inline-flex items-center gap-1.5">{% if it.icon %}::{{ it.icon }}::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</span>{% endif %}{% endmacro %}
<div data-block-override="earthen:utility-bar" class="w-full text-[0.72rem] {% if not showOnMobile %}hidden sm:block{% endif %} {{ ubcls }} {{ class }}" style="font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-9 flex items-center justify-between gap-4">
<div class="flex items-center gap-5 flex-wrap">{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}</div>
<div class="flex items-center gap-5 flex-wrap justify-end">{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
{# video-embed override — paper frame with soft organic corners; play button in a cream medallion. Fields + facade JS mirror builtin verbatim: aspect, url, media, poster, title, caption. #}
{% with aspect=aspect|default:"16:9" %}<figure data-block-override="earthen:video-embed" class="bn-video my-8{% if class %} {{ class }}{% endif %}"><div class="bn-video-frame earthen-blob-soft relative w-full overflow-hidden bg-muted {% if aspect == "4:3" %}aspect-[4/3]{% elif aspect == "1:1" %}aspect-square{% elif aspect == "21:9" %}aspect-[21/9]{% else %}aspect-video{% endif %}" data-bn-video data-video-url="{{ url|default:"" }}" data-video-media="{% if media %}{{ media|mediaURL }}{% endif %}"><button type="button" data-bn-video-play class="group absolute inset-0 flex h-full w-full items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" aria-label="Play video{% if title %}: {{ title }}{% endif %}">{% if poster %}{% img poster alt=title|default:"" class="absolute inset-0 h-full w-full object-cover" %}{% endif %}<span class="absolute inset-0 bg-foreground/10 transition-colors group-hover:bg-foreground/20" aria-hidden="true"></span><span class="relative inline-flex h-16 w-16 items-center justify-center rounded-full bg-background/90 text-accent shadow-md transition-transform group-hover:scale-105 motion-reduce:transform-none">::lucide:play:xl::</span></button></div>{% if caption %}<figcaption class="mt-3 text-center text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Work Sans', ui-sans-serif, system-ui, sans-serif);">{{ caption }}</figcaption>{% endif %}</figure><script>
(function(){
if(window.__bnVideoFacade)return;window.__bnVideoFacade=true;
function yt(u){var m=u.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{6,})/);return m?m[1]:null;}
function vim(u){var m=u.match(/vimeo\.com\/(?:video\/)?(\d+)/);return m?m[1]:null;}
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-video-play]');if(!btn)return;
var frame=btn.closest('[data-bn-video]');if(!frame)return;
e.preventDefault();
var url=frame.getAttribute('data-video-url')||'',media=frame.getAttribute('data-video-media')||'',node;
var vstyle='position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:rgba(0,0,0,1);';
if(media){node=document.createElement('video');node.src=media;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
else{var y=yt(url),v=vim(url),embed;
if(y)embed='https://www.youtube-nocookie.com/embed/'+y+'?autoplay=1&rel=0';
else if(v)embed='https://player.vimeo.com/video/'+v+'?autoplay=1';
if(embed){node=document.createElement('iframe');node.src=embed;node.allow='autoplay; fullscreen; picture-in-picture';node.setAttribute('allowfullscreen','');node.setAttribute('title',btn.getAttribute('aria-label')||'Video');node.style.cssText='position:absolute;inset:0;width:100%;height:100%;border:0;';}
else if(url){node=document.createElement('video');node.src=url;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
}
if(node){btn.remove();frame.appendChild(node);}
});
})();
</script>{% endwith %}