Compare commits

..

6 Commits

Author SHA1 Message Date
Alex Dunmow
46a45d7160 bump to 0.4.0 2026-07-06 19:26:19 +08:00
Alex Dunmow
ac4ba1523c feat: Wave B gallery screenshots (home/about/article × light+dark)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:25:58 +08:00
Alex Dunmow
7456205750 bump to 0.3.2 2026-07-06 16:30:18 +08:00
Alex Dunmow
c71eebf919 fix(earthen): image block caption below image, not beside it
The figure was a flex ROW (flex justify-*), so a figcaption became a second
flex item sitting to the right of the frame. flex-col + items-* keeps the
alignment semantics and puts the caption underneath.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:30:18 +08:00
Alex Dunmow
bf89cdba6d bump to 0.3.1 2026-07-06 16:24:40 +08:00
Alex Dunmow
c2fc45e7f7 fix(earthen): kill placeholder copy, enrich home, wire stock media, size all SVGs, full-bleed article header
- seed: real Rootline Trust content — no 'example figures' placeholder copy;
  home composed as split hero (hands-soil image) + stats + feature-grid +
  botanical divider + field note + newsletter; About gap filled with fern
  image + impact report between story and pull-quote; first article expanded
  to ~10 field-notes paragraphs with inline forest image; 6 Pixabay images
  seeded via seed/media (id+file+alt)
- article.ninjatpl: header slot now full-bleed (was max-w-3xl wrapped — the
  boxed-header artifact bracketing the nav with vertical rules)
- landing.ninjatpl: main slot full-bleed so composed sections span the page
- SVG geometry guard: every <use>-icon svg now carries explicit width/height
  matching its size class (18 files)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:24:31 +08:00
37 changed files with 485 additions and 89 deletions

View File

@ -2,7 +2,7 @@
name = "earthen"
display_name = "Earthen"
scope = "@themes"
version = "0.3.0"
version = "0.4.0"
description = "Botanical nonprofit theme in moss, clay, sage and cream — hand-illustrated motifs for cause-driven sites."
kind = "theme"
categories = ["templates"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
seed/media/botanical.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
seed/media/forest.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

BIN
seed/media/hands-soil.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

BIN
seed/media/leaf-veins.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

BIN
seed/media/seedling.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
seed/media/terracotta.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

View File

@ -6,16 +6,74 @@
"tagline": "We restore land with the people who live on it."
}
},
"media": [
{
"id": "41830fd8-442a-4255-bc96-9fb12e428ef1",
"file": "media/hands-soil.jpg",
"alt": "Hands lowering a young seedling into dark soil",
"folder": "earthen"
},
{
"id": "152f8386-ecf1-4f0c-899a-2c947766644d",
"file": "media/seedling.jpg",
"alt": "Cupped hands holding a seedling above turned earth",
"folder": "earthen"
},
{
"id": "c71264ea-0217-4826-a796-baffab1cf041",
"file": "media/forest.jpg",
"alt": "Sunlight through moss-covered trees in regrown forest",
"folder": "earthen"
},
{
"id": "603fdac3-2fa5-4511-8357-aa65550d9a31",
"file": "media/leaf-veins.jpg",
"alt": "Rain-washed leaf showing its veins in deep green",
"folder": "earthen"
},
{
"id": "02a30527-5464-45e2-a1f9-529fd9f46787",
"file": "media/terracotta.jpg",
"alt": "Terracotta pots of native tube stock ready for planting",
"folder": "earthen"
},
{
"id": "a8c164d1-5f0c-496d-bb77-d1e8ba769527",
"file": "media/botanical.jpg",
"alt": "Fern fronds unfurling on the forest floor",
"folder": "earthen"
}
],
"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.",
"description": "Contact-form submissions for Rootline Trust. 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 }
{
"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
}
]
}
],
@ -31,42 +89,122 @@
"slot": "hero",
"sort_order": 1,
"content": {
"template": "centered",
"template": "split",
"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" }
"description": "Rootline Trust plants native cover, mends waterways and hands the care of the land back to local hands. Small work, done every season, in the places that need it most.",
"backgroundImage": "media:41830fd8-442a-4255-bc96-9fb12e428ef1",
"mediaPosition": "right",
"ctaPrimary": {
"text": "Donate",
"url": "/contact"
},
"ctaSecondary": {
"text": "See our work",
"url": "/about"
}
}
},
{
"block_key": "earthen:impact_report",
"title": "Impact Report",
"block_key": "stats",
"title": "Season in numbers",
"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."
"heading": "The last season, in the ground",
"intro": "Every figure here is an acre planted, a bank mended, or a day given by someone who lives beside the work.",
"columns": 4,
"countUp": false,
"items": [
{
"value": "840",
"suffix": " acres",
"label": "Native cover planted"
},
{
"value": "19",
"suffix": " km",
"label": "Creek bank mended"
},
{
"value": "1,200",
"label": "Volunteer days given"
},
{
"value": "12",
"label": "Restoration sites"
}
]
}
},
{
"block_key": "feature-grid",
"title": "How we work",
"slot": "main",
"sort_order": 2,
"content": {
"eyebrow": "How we work",
"heading": "Three steps, every site",
"intro": "Restoration is not one grand act. It is the same patient sequence, repeated season after season, with the people who live on the land.",
"columns": 3,
"layout": "card",
"items": [
{
"icon": "lucide:sprout",
"title": "Plant native cover",
"text": "We raise local tube stock and plant the species that belong on each site — the ones that hold soil, feed pollinators and come back on their own."
},
{
"icon": "lucide:waves",
"title": "Mend the waterways",
"text": "We fence stock out of the creek lines, stabilise the banks and let the water run clean again. A mended creek carries a whole valley with it."
},
{
"icon": "lucide:heart-handshake",
"title": "Hand the land back",
"text": "From the first day we work beside local carers. By the third season we step back, and the people who live there keep it going."
}
]
}
},
{
"block_key": "divider",
"title": "Botanical divider",
"slot": "main",
"sort_order": 3,
"content": {
"variant": "ornament",
"width": "wide",
"icon": "lucide:leaf"
}
},
{
"block_key": "earthen:field_note",
"title": "Field Note",
"slot": "main",
"sort_order": 2,
"sort_order": 4,
"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>"
"image": "media:152f8386-ecf1-4f0c-899a-2c947766644d",
"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 — thousands of small green points, holding the bank against the next flood.</p><p>You plant, you fence, you wait. Restoration keeps its own slow clock. Then one ordinary morning the land answers back, and you understand you were only ever helping it remember what it already knew how to do.</p>"
}
},
{
"block_key": "newsletter-signup",
"title": "Newsletter",
"slot": "main",
"sort_order": 5,
"content": {
"title": "Field notes in your inbox",
"description": "A short dispatch from the sites each season — what we planted, what held, and where we are headed next. No noise.",
"listSlug": "main",
"source": "home",
"submitText": "Subscribe",
"placeholder": "you@example.com",
"layout": "card"
}
}
]
@ -76,10 +214,90 @@
"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" } }
{
"block_key": "heading",
"title": "Heading",
"slot": "main",
"sort_order": 1,
"content": {
"text": "Our work",
"level": 1
}
},
{
"block_key": "image",
"title": "Forest regrowth",
"slot": "main",
"sort_order": 3,
"content": {
"src": "media:a8c164d1-5f0c-496d-bb77-d1e8ba769527",
"alt": "Fern fronds unfurling on the forest floor",
"caption": "Ferns returning to the understorey at Wirra Creek, three seasons after fencing.",
"size": "lg",
"aspectRatio": "wide"
}
},
{
"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 the 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, planted from local stock, and reported honestly to the people who fund it. Nothing here happens in a season — it happens over three, or five, or ten, and it lasts because the people who live there stay long after we have gone.</p><p>We are a small trust on purpose. It keeps us close to the ground, close to the water, and close to the people whose land we are helping to mend.</p>"
}
},
{
"block_key": "earthen:impact_report",
"title": "Impact Report",
"slot": "main",
"sort_order": 4,
"content": {
"period": "Season 2025",
"title": "What we grew together",
"summary": "<p>Every acre here began as a pledge from someone who wanted a piece of degraded land to come back. This is where those pledges went last season — twelve sites, one patient sequence, repeated until the ground took hold.</p>",
"outcomes": [
{
"value": "840 acres",
"label": "Native cover planted",
"detail": "Across twelve restoration sites, raised from local tube stock."
},
{
"value": "19 km",
"label": "Creek bank mended",
"detail": "Fenced, replanted and handed to the carers who live beside it."
},
{
"value": "1,200",
"label": "Volunteer days",
"detail": "Given by people who live within a day's drive of the work."
}
]
}
},
{
"block_key": "divider",
"title": "Divider",
"slot": "main",
"sort_order": 5,
"content": {
"variant": "line",
"weight": "thin",
"width": "wide"
}
},
{
"block_key": "quote",
"title": "Quote",
"slot": "main",
"sort_order": 6,
"content": {
"style": "pull",
"align": "left",
"quote": "You plant, you fence, you wait. Then the land answers back.",
"attribution": "Mara Ellison",
"role": "Field Lead, Wirra Creek"
}
}
]
},
{
@ -87,9 +305,48 @@
"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" } }
{
"block_key": "card",
"title": "Post: First Rain",
"slot": "main",
"sort_order": 1,
"content": {
"layout": "vertical",
"media": "media:603fdac3-2fa5-4511-8357-aa65550d9a31",
"title": "The first rain after planting",
"text": "<p>What a dawn walk along Wirra Creek tells you about a whole 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",
"media": "media:c71264ea-0217-4826-a796-baffab1cf041",
"title": "Why we fence before we plant",
"text": "<p>The least glamorous work on any site is the thing that 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",
"media": "media:02a30527-5464-45e2-a1f9-529fd9f46787",
"title": "Handing the land back",
"text": "<p>Restoration is not finished when we leave. It is finished when the local carers take it on.</p>",
"link": "/blog/handing-the-land-back",
"linkLabel": "Read the dispatch"
}
}
]
},
{
@ -98,8 +355,38 @@
"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>" } }
{
"block_key": "heading",
"title": "Title",
"slot": "main",
"sort_order": 1,
"content": {
"text": "The first rain after planting",
"level": 1
}
},
{
"block_key": "image",
"title": "Creek line at dawn",
"slot": "main",
"sort_order": 2,
"content": {
"src": "media:c71264ea-0217-4826-a796-baffab1cf041",
"alt": "Sunlight through moss-covered trees in regrown forest",
"caption": "The oldest reach at Wirra Creek, three seasons in — canopy closing, bank holding.",
"size": "lg",
"aspectRatio": "wide"
}
},
{
"block_key": "rich-text",
"title": "Body",
"slot": "main",
"sort_order": 3,
"content": {
"html": "<p>We walked the creek line at dawn, before the heat, the way we always do on the morning after the first real rain. The ground was dark and giving underfoot. Along the bank, where six weeks ago there had been bare clay and the stubble of old grazing, the tube stock had held — thousands of small green points, each one a native seedling with its roots now gripping the soil we had loosened for it.</p><p>The first rain after planting is the one that tells you. Everything before it is hope and hard work: you clear, you fence, you dig the holes on contour, you sink the tubes and firm the earth around each one. Then you wait, and you watch the sky, because a young planting that dries out in its first fortnight will not come back no matter how carefully it was put in. This rain came in the second week. It came slow and steady overnight, the kind that soaks in instead of running off, and by dawn the whole site had drunk it.</p><p>Restoration is slow, and it is meant to be. You cannot hurry a root system. You cannot argue a creek bank into stability. What you can do is give the land the conditions it needs and then get out of its way — fence the stock out, keep the weeds off the young growth, and let the seasons do the rest. We plant, we fence, we wait. It is the least heroic sequence imaginable, and it is the only one that works.</p><p>Wirra Creek was one of the worst sites we had taken on. Decades of stock had trodden the banks to bare earth; every flood carried another metre of topsoil downstream, and the water ran the colour of milky tea for days after any rain. When we first walked it with the carers who live along it, nobody pretended it would be quick. We agreed a ten-year horizon out loud, in the paddock, so that no one would mistake a single good season for the finish line.</p><p>The species matter more than people expect. We raise our tube stock from local seed — the sedges and wattles and paperbarks that already know this soil and this rainfall — because a plant that belongs on a site will hold on through the years we are not watching. The nursery work is unglamorous and it is where half the outcome is decided. A good seedling, hardened off properly, will take the first rain and run with it. A soft one, forced under lights and shipped in from far away, will sulk and die at the first hot week.</p><p>By mid-morning the sun was up and the carers had arrived to walk the line with us. There is a particular quiet that comes over people on a morning like that — nobody says much. You crouch by a tube, you feel the soil, you check that the guard has held and the stem is firm, and you move on to the next. Down the line someone laughs, because a seedling that was leaning last week has stood itself upright overnight, the way they do once the roots find water.</p><p>Not everything held. It never does. In the low corner where the flood sits longest we lost a run of tubes to waterlogging, and up on the dry western bank a few had been nosed out by something — a wallaby, most likely, testing the guards. We marked the gaps to replant in autumn. Losing some is part of the arithmetic; you plant knowing a portion will fail, and you plant enough that the survivors still close the canopy in time.</p><p>The fence is what makes all of it possible. Before a single tube goes in, we fence the stock out of the creek line and hold that line for years. It is the least glamorous work we do and it is the work that protects everything else. A good fence buys a young planting three quiet years to establish, and three years is usually all a native system needs to look after itself.</p><p>We finished the walk at the top of the reach, where the oldest planting — three seasons in now — has grown past head height and thrown enough shade that the bank beneath it stays damp and dark. Frogs have come back to that stretch. You can hear them after rain. It is the plainest possible proof that the work is working: you make a place liveable again, and the living things move back in without being asked.</p><p>That is the whole of it, really. You plant, you fence, you wait. Restoration keeps its own slow clock and there is no cheating it. Then one ordinary morning after rain the land answers back — a bank that holds, a creek that runs clear, a chorus of frogs where there was bare clay — and you understand that you were only ever helping the land remember what it already knew how to do.</p>"
}
}
]
},
{
@ -108,8 +395,25 @@
"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>" } }
{
"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. A single mob of cattle can undo a season's planting in an afternoon, and they always seem to know exactly where the softest new leaves are. So we fence first. Every time. It is the least glamorous work we do and it is the thing that protects everything that comes after.</p><p>A good fence buys a young planting three years of quiet — long enough for the roots to reach down, the canopy to start closing, and the site to stop needing us. Three years is usually all a native system asks for. Get the fence right and hold the line, and the rest of restoration is mostly patience.</p><p>People ask why we do not just plant more to make up for what gets eaten. The answer is that a fence is cheaper than a failure, and far cheaper than the heartbreak of walking a chewed-out site the morning after. We would rather spend the first budget on posts and wire than replant the same bank three times over.</p>"
}
}
]
},
{
@ -118,8 +422,25 @@
"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>" } }
{
"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 the local carers take it on. We plant with them from the first day — not as spectators, but with hands in the soil — so the knowledge stays on the land instead of leaving in the back of our ute.</p><p>By the third season we start to step back. The watering slows, the replanting tapers off, and the walks along the line become the carers' walks, not ours. The site keeps going because the people beside it keep going, and they keep going because it was theirs the whole way through.</p><p>We measure our success by how little we are needed at the end. A site we can walk away from is the goal. It sounds strange for an organisation to aim at its own irrelevance, but that is exactly the point: land that can look after itself, in the care of people who will be there long after the last of our fences has weathered grey.</p>"
}
}
]
},
{
@ -138,12 +459,40 @@
"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" }
{
"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" }
"formConfig": {
"enabled": true,
"captchaEnabled": false,
"targetTable": "contact_submissions"
}
}
},
{
@ -154,10 +503,22 @@
"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 }
{
"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",
@ -171,15 +532,52 @@
"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>" } }
{
"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 }
{
"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
}
]
}
}

View File

@ -3,9 +3,7 @@
{{ 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 }}
<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>
</header>
<header class="w-full">{{ slots.header|safe }}</header>
{% if slots.lede %}<section class="w-full max-w-3xl mx-auto px-4 pt-12">{{ slots.lede|safe }}</section>{% endif %}
<div class="flex-grow w-full grid grid-cols-1 lg:grid-cols-[1fr,260px] gap-12 max-w-5xl mx-auto px-4 py-12">
<main>{% if slots.main %}<article class="earthen-article">{{ slots.main|safe }}</article>{% else %}<p style="color: hsl(var(--muted-foreground));">No story body yet.</p>{% endif %}</main>

View File

@ -4,7 +4,7 @@
<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 }}
<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 w-full">{% if slots.main %}{{ slots.main|safe }}{% endif %}</main>
<section class="w-full">{{ slots.cta|safe }}</section>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}

View File

@ -4,7 +4,7 @@
<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 %}
{% 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 width="16" height="16" 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>

View File

@ -4,7 +4,7 @@
<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>
<svg width="16" height="16" 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>

View File

@ -12,10 +12,10 @@
{% 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 %}
{% 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 width="20" height="20" 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 width="20" height="20" 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 width="20" height="20" 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 width="20" height="20" class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>

View File

@ -5,7 +5,7 @@
{% 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 showHome or not forloop.First %}{% if arrows %}<svg width="14" height="14" 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 %}

View File

@ -6,7 +6,7 @@
<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 %}
{% 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 width="20" height="20" 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 %}

View File

@ -12,7 +12,7 @@
</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 %}
{% 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 width="16" height="16" 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>

View File

@ -7,7 +7,7 @@
{% 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 primaryUrl and primaryLabel %}<a href="{{ primaryUrl }}" class="earthen-cta inline-flex items-center gap-2 px-6 py-3 font-semibold">{{ primaryLabel }}<svg width="16" height="16" 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>

View File

@ -13,7 +13,7 @@
<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 %}
{% 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 width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
</article>
{% endif %}{% endfor %}

View File

@ -8,7 +8,7 @@
<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>
<svg width="20" height="20" 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>

View File

@ -7,10 +7,10 @@
<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 %}
{% 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 width="24" height="24" 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 %}
{% 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 width="16" height="16" class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
{% endfor %}
</div>

View File

@ -10,10 +10,10 @@
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>'+
'<button type="button" data-lb-close aria-label="Close" style="'+btn+'top:1rem;right:1rem;"><svg width="20" height="20" 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 width="20" height="20" 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>';
'<button type="button" data-lb-next aria-label="Next image" style="'+btn+'right:1rem;top:50%;transform:translateY(-50%);"><svg width="20" height="20" 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();});

View File

@ -1,6 +1,6 @@
{# 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 %}">
<figure data-block-override="earthen:image" class="my-8 flex-1 flex flex-col {% if align == "left" %}items-start{% elif align == "right" %}items-end{% else %}items-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 %}

View File

@ -10,7 +10,7 @@
{% 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>
<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 width="16" height="16" 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 %}
@ -33,7 +33,7 @@
<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>
<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 width="24" height="24" 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>
@ -41,8 +41,8 @@
<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>
<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 width="24" height="24" 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 width="16" height="16" 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>

View File

@ -15,7 +15,7 @@
</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 %}
{% for feature in tier.features %}<li class="flex items-start gap-2"><svg width="16" height="16" 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>

View File

@ -5,7 +5,7 @@
{% 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 %}
{% 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 width="16" height="16" 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 %}

View File

@ -4,7 +4,7 @@
{% 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>
<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 width="16" height="16" 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>

View File

@ -5,7 +5,7 @@
{% 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 link.network %}<svg width="20" height="20" 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>

View File

@ -12,7 +12,7 @@
{% 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 %}
{% 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 width="20" height="20" 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>

View File

@ -6,11 +6,11 @@
{% 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>
<svg width="16" height="16" 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 width="16" height="16" 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 width="16" height="16" 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 width="16" height="16" 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 width="16" height="16" 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>

View File

@ -7,7 +7,7 @@
{% 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 %}
{% if marker == "numbered" %}{{ forloop.counter }}{% elif marker == "dot" %}<span class="h-2 w-2 rounded-full bg-primary-foreground"></span>{% else %}<svg width="16" height="16" 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 %}