feat(art-deco): Wave A supper-club theme — 49 overrides, 7 templates, seed

WO-TF-011: dress the fine-dining / supper-club persona (gilded symmetry,
ziggurat frames, sunbursts, hairline gold rules; champagne/ivory light,
gold-on-lacquer dark) against the frozen Wave-0 builtin schemas.

Overrides (templates/overrides/art-deco/): all 49 mandatory builtin keys,
each re-skinned from the builtin default with every content field, provider
field, custom tag ({% img %}/{% button %}/{% form %}/{% signup_form %}/
{% auth_form %} wiring) and JS (navbar drawer, gallery lightbox, countdown,
stats count-up, video/audio facade, announcement dismiss) preserved verbatim.
Auth trio reproduces the untouchable /api/auth/* endpoints and field names;
captcha widget injected from _captcha_widget. manifest.yaml declares all 49.

Page templates: added blog-index, contact, auth (now 7 total) in Deco chrome.

Blocks rework (spec D5): footer/hero(marquee)/gallery/divider/pull-quote theme
blocks deleted — folded into overrides of the builtin keys. Kept persona
furniture reservation + menu; added private_dining + chef_signature (4 blocks).

Presets: 4 dual-mode (mode "both"), all 19 tokens, light AND dark tuned.
Fonts: bundle Cinzel + Cormorant Garamond (OFL) as Latin woff2 subsets in
assets/fonts/ (license recorded in OFL.txt); all font-family via var().
Email wrapper themed. required_icon_packs=["lucide"] + RECOMMENDED_ICONS.md.

Seed (demo): home/about/blog(3 posts)/contact/login pages in supper-club voice;
contact-form targets seeded contact_submissions data table; row_inserted
workflow emails admins.

Verified: ninja plugin build (codeless), verify, archive-check, and
check-safety all exit 0. Visual quality UNVERIFIED until Wave B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-05 10:51:02 +08:00
parent a7141e39ae
commit 03c49a9a83
83 changed files with 1808 additions and 307 deletions

View File

@ -1,53 +1,36 @@
# Recommended Fonts — Art Deco
This theme ships `fonts.json = []` (Wave-1 policy, see `themes/docs/FONTS.md`).
The intended typographic identity is achieved by an admin assigning the
following Google Fonts to the theme's typography slots after install.
This theme **bundles** its display and body families as subset woff2 files in
`assets/fonts/` (see `fonts.json`). They install as `source=template` rows and
are pre-selected for the theme's typography slots — no admin action is required
for the intended look. The families below are the fallbacks baked into every
`var(--font-heading|body|mono, …)` call, so the theme still reads correctly if
an admin reassigns a slot.
## Picker workflow
## Bundled (shipped in this theme)
1. Sign in as an admin and open **Settings → Typography**.
2. Switch to the **Google Fonts** tab in the font picker.
3. Search for the family, click **Add**, then assign it to the listed slot.
| Slot | Family | Weights | File(s) | License |
|------|--------|---------|---------|---------|
| Heading | **Cinzel** | 400, 700 | `assets/fonts/cinzel-*.woff2` | SIL OFL 1.1 |
| Body | **Cormorant Garamond** | 400, 400 italic, 600, 700 | `assets/fonts/cormorant-garamond-*.woff2` | SIL OFL 1.1 |
## Display heading — Italiana
License record and copyright attribution: `assets/fonts/OFL.txt`. The woff2s are
Latin subsets of the upstream OFL families (subsetting is permitted by the OFL).
- **Source**: `google:Italiana`
- **Slot**: Heading
- **Why**: The spec calls for stamped Art Deco titles in tight tracking. Italiana
is the closest curated Google Fonts match to the high-contrast 1920s display
cuts the brief describes.
- **Fallback already in CSS**: `var(--font-heading, "Italiana", "Cinzel", Georgia, serif)`.
## Mono slot (not bundled — assign if needed)
### Alternative
The theme uses `var(--font-mono, "JetBrains Mono", ui-monospace, monospace)` for
reservation IDs and tabular figures. To match the intended look, an admin can
assign a mono family:
- **Source**: `google:Cinzel` (also acceptable per spec §3)
Pick this if Italiana feels too thin at small sizes.
1. Sign in as an admin, open **Settings → Typography**.
2. Switch to the **Google Fonts** tab and add **JetBrains Mono**.
3. Assign it to the **Mono** slot.
## Body — Cormorant Garamond
If left unset, the CSS falls back to the system monospace stack.
- **Source**: `google:Cormorant Garamond`
- **Slot**: Body
- **Why**: Classical italic and book weights for long-form prose, exactly the
"Cormorant for long-form prose with classical italics" pairing the spec calls
out.
- **Fallback already in CSS**: `var(--font-body, "Cormorant Garamond", "Cormorant", Georgia, serif)`.
## Alternatives (optional)
## Mono — JetBrains Mono
- **Source**: `google:JetBrains Mono`
- **Slot**: Mono
- **Why**: Spec wants a precise monospace for reservation IDs and room numbers.
JetBrains Mono ships across all curated Google Fonts and is the cleanest
utilitarian option.
- **Fallback already in CSS**: `var(--font-mono, "JetBrains Mono", ui-monospace, monospace)`.
## Notes for Wave-2
- If Italiana proves too brittle on retina at large sizes, commission a
redistributable Art Deco display face and bundle it via `fonts.json`. The
rest of the CSS is already variable-driven and will pick the new family up
without further edits.
- Body italic for the drop-cap relies on the body family having an italic
variant; both Cormorant Garamond and Cormorant ship italics in the curated
Google Fonts list — no extra picker action needed.
- Heading: `google:Italiana` — a thinner, higher-contrast display cut; already
the second fallback in the heading `var()` list.
- Body: the system serif stack (`Georgia, serif`) is the final fallback.

24
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,24 @@
# Recommended Icons — Art Deco
This theme references icons from a single bundled pack. `plugin.mod` declares
it in `required_icon_packs`; until the loader auto-installs declared packs, an
admin should confirm it is enabled before activation.
## Required pack
- **lucide** — ships bundled with the CMS (v0.460). Enabled by default. No
action needed in most installs; if icons render as empty boxes, open
`/admin/icons` and confirm **Lucide** is installed and enabled.
## Icons used by the theme chrome and overrides
All via the sprite pipeline (`<svg><use href="/icons/lucide.svg#<name>"/></svg>`
or the `::lucide:<name>::` shorthand):
`arrow-right`, `check`, `chevron-down`, `chevron-left`, `chevron-right`, `x`,
`play`, `download`, `navigation`, `map-pin`, `diamond`, `star`, `flag`,
`globe`.
Block content that stores an icon (e.g. feature-grid items, social links,
contact channels) may reference any installed pack as a `pack:name` string;
the schemas default to `lucide` so the theme renders without extra setup.

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

@ -0,0 +1,23 @@
Bundled fonts — license record (SIL Open Font License, Version 1.1)
====================================================================
The woff2 files in this directory are subsets (Latin) of the following
open-source families, both distributed under the SIL Open Font License 1.1
(https://openfontlicense.org). Subsetting and format conversion (TTF to
woff2) are expressly permitted by the OFL; the derived subsets remain under
the same license.
Cinzel
cinzel-400.woff2, cinzel-700.woff2
Copyright (c) Natanael Gama (https://ndiscover.com), with Reserved Font
Name "Cinzel".
Cormorant Garamond
cormorant-garamond-400.woff2, cormorant-garamond-400-italic.woff2,
cormorant-garamond-600.woff2, cormorant-garamond-700.woff2
Copyright (c) Christian Thalmann, Catharsis Fonts
(https://catharsisfonts.com), with Reserved Font Name "Cormorant".
The full SIL Open Font License 1.1 text applies to each family and is
available at https://openfontlicense.org. This file records the license and
copyright attribution as required for redistribution under the OFL.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,8 +1,10 @@
# Art Deco theme block definitions (codeless .bnp, WO-WZ-021).
# Keys are fully qualified (art-deco:<key>) because the codeless loader
# registers definitions verbatim — there is no PluginBlockRegistry prefixing
# in the codeless path (unlike the wasm/templ path). The qualified keys match
# the hardcoded data-block attributes and the master_pages.json references.
# Art Deco theme block definitions (codeless .bnp, WO-WZ-021 / WO-TF-011).
# Only TRUE persona furniture remains as theme blocks. Everything that
# duplicated a builtin (footer, hero/marquee, gallery, divider, pull-quote)
# is now a template OVERRIDE of the builtin key (templates/overrides/art-deco/).
# Keys are fully qualified (art-deco:<key>) — the codeless loader registers
# definitions verbatim; the qualified keys match the data-block attributes,
# master_pages.json and seed references.
blocks:
- key: art-deco:reservation
title: Reservation Strip
@ -12,37 +14,19 @@ blocks:
template: reservation.ninjatpl
- key: art-deco:menu
title: Tasting Menu
description: Symmetric two-column menu card listing courses with description and price.
description: Symmetric menu card listing courses with tasting note and price.
category: content
schema: menu.schema.json
template: menu.ninjatpl
- key: art-deco:gallery_fan
title: Fan Gallery
description: Mirrored gallery framed by sunburst — collapses to single column under 640px.
- key: art-deco:private_dining
title: Private Dining
description: Private dining room showcase — framed image with room details and an enquiry CTA.
category: content
schema: gallery_fan.schema.json
template: gallery_fan.ninjatpl
- key: art-deco:divider_fan
title: Fan Divider
description: Pure-CSS gold geometric divider — sunburst, scallop or ziggurat.
category: layout
schema: divider_fan.schema.json
template: divider_fan.ninjatpl
- key: art-deco:footer
title: Footer
description: Centered Art Deco footer with gold rule, address, reservations email and optional social row.
category: layout
schema: footer.schema.json
template: footer.ninjatpl
- key: art-deco:marquee_hero
title: Marquee Hero
description: Wide hero with stepped ziggurat frame and sunburst overlay.
schema: private_dining.schema.json
template: private_dining.ninjatpl
- key: art-deco:chef_signature
title: Chef's Signature
description: Centred signature-dish feature with a chef's note between sunburst crests.
category: content
schema: marquee_hero.schema.json
template: marquee_hero.ninjatpl
- key: art-deco:press_quote
title: Press Quote
description: Italic Cormorant pull-quote between fan dividers, optional star rating.
category: content
schema: press_quote.schema.json
template: press_quote.ninjatpl
schema: chef_signature.schema.json
template: chef_signature.ninjatpl

View File

@ -0,0 +1,11 @@
{# chef_signature persona block — centred signature-dish feature with sunburst crests. Media via {% img %}. #}
<section data-block="art-deco:chef_signature" class="py-16 md:py-24 bg-background text-foreground">
<div class="mx-auto max-w-3xl px-4 text-center">
{% if eyebrow %}<div class="mb-4 flex items-center justify-center gap-4"><span class="deco-rule w-10"></span><span class="deco-caps text-xs" style="color:hsl(var(--primary));">{{ eyebrow }}</span><span class="deco-rule w-10"></span></div>{% endif %}
{% if image %}<div class="deco-step mx-auto mb-8 max-w-md">{% img image alt=dish class="w-full h-auto object-cover" %}</div>{% endif %}
<div class="h-6 w-40 mx-auto deco-sunburst" aria-hidden="true"></div>
{% if dish %}<h2 class="deco-caps mt-6 text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ dish }}</h2>{% endif %}
{% if note %}<div class="mt-4 text-xl italic leading-relaxed" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ note|safe }}</div>{% endif %}
{% if chef %}<p class="deco-caps mt-6 text-xs" style="color:hsl(var(--primary));">{{ chef }}</p>{% endif %}
</div>
</section>

View File

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Chef's Signature",
"description": "Centred signature-dish feature with a chef's note between sunburst crests.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"x-editor": "text",
"default": "Chef's signature"
},
"dish": {
"type": "string",
"title": "Dish name",
"x-editor": "text",
"default": ""
},
"note": {
"type": "string",
"title": "Chef's note",
"description": "A short tasting note or story about the dish.",
"x-editor": "richtext",
"default": ""
},
"chef": {
"type": "string",
"title": "Chef name",
"x-editor": "text",
"default": ""
},
"image": {
"type": "string",
"title": "Dish image",
"x-editor": "media",
"default": ""
}
}
}

View File

@ -1 +0,0 @@
{% if variant == "scallop" %}<div data-block="art-deco:divider_fan" data-variant="scallop" class="w-full py-6 flex justify-center" role="presentation" aria-hidden="true"><div class="h-10 w-full max-w-xl deco-scallop"></div></div>{% elif variant == "ziggurat" %}<div data-block="art-deco:divider_fan" data-variant="ziggurat" class="w-full py-6 flex justify-center" role="presentation" aria-hidden="true"><div class="h-10 w-full max-w-xl deco-ziggurat"></div></div>{% else %}<div data-block="art-deco:divider_fan" data-variant="sunburst" class="w-full py-6 flex justify-center" role="presentation" aria-hidden="true"><div class="h-10 w-full max-w-xl deco-sunburst"></div></div>{% endif %}

View File

@ -1,16 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Fan Divider",
"description": "Pure-CSS geometric gold divider — sunburst, scallop or ziggurat.",
"type": "object",
"properties": {
"variant": {
"type": "string",
"title": "Variant",
"description": "Choose the divider motif.",
"x-editor": "select",
"default": "sunburst",
"enum": ["sunburst", "scallop", "ziggurat"]
}
}
}

View File

@ -1,10 +0,0 @@
<section data-block="art-deco:footer" class="py-12 deco-grain">
<div class="max-w-3xl mx-auto px-4 text-center">
<hr class="deco-rule mb-8">
{% if address %}<address class="not-italic mb-4" style="color: hsl(var(--muted-foreground));">{% for line in address|lines %}<div>{{ line }}</div>{% endfor %}</address>{% endif %}
{% if reservationsEmail %}<p class="mb-4"><a href="mailto:{{ reservationsEmail }}" class="deco-link">{{ reservationsEmail }}</a></p>{% endif %}
{% if menuName %}<nav aria-label="Footer" class="deco-caps text-xs mb-4" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));"><span>{{ menuName }}</span></nav>{% endif %}
{% if showSocial != "false" %}<div class="flex justify-center gap-4 mb-4" style="color: hsl(var(--primary));"><span class="deco-caps text-xs" aria-hidden="true">Instagram</span><span aria-hidden="true">·</span><span class="deco-caps text-xs" aria-hidden="true">Facebook</span><span aria-hidden="true">·</span><span class="deco-caps text-xs" aria-hidden="true">LinkedIn</span></div>{% endif %}
<hr class="deco-rule mt-8">
</div>
</section>

View File

@ -1,37 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Footer",
"description": "Centered Art Deco footer with gold rule, address, reservations email and optional social row.",
"type": "object",
"properties": {
"address": {
"type": "string",
"title": "Address",
"description": "Multi-line postal address.",
"x-editor": "textarea",
"default": ""
},
"reservationsEmail": {
"type": "string",
"title": "Reservations email",
"description": "Public email shown in the footer for reservations enquiries.",
"x-editor": "text",
"default": ""
},
"showSocial": {
"type": "string",
"title": "Show social row",
"description": "Whether to render the social-icon row.",
"x-editor": "select",
"default": "true",
"enum": ["true", "false"]
},
"menuName": {
"type": "string",
"title": "Footer menu",
"description": "Name of the menu to render in the footer.",
"x-editor": "menu-select",
"default": ""
}
}
}

View File

@ -1,12 +0,0 @@
<section data-block="art-deco:gallery_fan" class="py-16">
<div class="max-w-6xl mx-auto px-4">
<div class="relative deco-grain">
<div class="absolute inset-x-0 -top-2 h-12 pointer-events-none deco-sunburst" aria-hidden="true"></div>
<div class="grid gap-4 pt-12 {% if columns|integer == 2 %}grid-cols-1 sm:grid-cols-2{% elif columns|integer == 4 %}grid-cols-1 sm:grid-cols-2 lg:grid-cols-4{% else %}grid-cols-1 sm:grid-cols-2 lg:grid-cols-3{% endif %}">
{% if not images %}<div class="col-span-full text-center text-sm py-12" style="color: hsl(var(--muted-foreground));">Add images to populate the fan gallery.</div>{% endif %}
{% for img in images %}<figure class="deco-step">{% if img.src %}<img src="{{ img.src }}" alt="{{ img.caption }}" class="block w-full h-auto object-cover" style="background-color: hsl(var(--muted));">{% else %}<img src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22%23222%22%2F%3E%3C%2Fsvg%3E" alt="" class="block w-full h-auto" style="background-color: hsl(var(--muted));">{% endif %}{% if img.caption %}<figcaption class="mt-2 text-center italic text-sm" style="color: hsl(var(--muted-foreground));">{{ img.caption }}</figcaption>{% endif %}</figure>{% endfor %}
</div>
<div class="absolute inset-x-0 -bottom-2 h-12 pointer-events-none deco-sunburst rotate-180" aria-hidden="true"></div>
</div>
</div>
</section>

View File

@ -1,38 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Fan Gallery",
"description": "Mirrored gallery framed by sunburst rays; collapses to single column under 640px.",
"type": "object",
"properties": {
"images": {
"type": "array",
"title": "Images",
"description": "Images to mirror across the fan.",
"x-editor": "array",
"default": [],
"items": {
"type": "object",
"properties": {
"src": {
"type": "string",
"title": "Image",
"x-editor": "media"
},
"caption": {
"type": "string",
"title": "Caption",
"x-editor": "text"
}
}
}
},
"columns": {
"type": "integer",
"title": "Columns",
"description": "Columns on desktop (2, 3 or 4).",
"x-editor": "select",
"default": 3,
"enum": [2, 3, 4]
}
}
}

View File

@ -1,12 +0,0 @@
<section data-block="art-deco:marquee_hero" class="relative w-full overflow-hidden deco-grain" style="background-color: hsl(var(--foreground)); color: hsl(var(--background));">
{% if image %}<div class="absolute inset-0 z-0" aria-hidden="true"><img src="{{ image }}" alt="" class="w-full h-full object-cover" style="opacity: 0.5;"><div class="absolute inset-0" style="background-color: hsl(var(--foreground) / 0.55);"></div></div>{% endif %}
<div class="absolute inset-x-0 top-0 h-24 z-10 pointer-events-none deco-sunburst" aria-hidden="true"></div>
<div class="relative z-20 max-w-4xl mx-auto px-4 py-32 text-center">
<div class="deco-frame inline-block px-12 py-10" style="background-color: hsl(var(--foreground) / 0.6); border-color: hsl(var(--primary));">
{% if eyebrow %}<p class="deco-caps mb-4 text-xs" style="color: hsl(var(--primary));">{{ eyebrow }}</p>{% endif %}
{% if title %}<h1 class="deco-caps deco-underline mb-6 text-5xl md:text-6xl lg:text-7xl" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--background));">{{ title }}</h1>{% endif %}
{% if subtitle %}<div class="text-lg md:text-xl mb-8 max-w-2xl mx-auto" style="color: hsl(var(--background) / 0.9);">{{ subtitle|safe }}</div>{% endif %}
{% if ctaLabel and ctaHref %}<a href="{{ ctaHref }}" class="deco-pill" style="background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary));">{{ ctaLabel }}</a>{% endif %}
</div>
</div>
</section>

View File

@ -1,47 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Marquee Hero",
"description": "Wide hero with stepped ziggurat frame and sunburst overlay.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small caps line above the title.",
"x-editor": "text",
"default": ""
},
"title": {
"type": "string",
"title": "Title",
"description": "Display headline in Italiana caps.",
"x-editor": "text",
"default": ""
},
"subtitle": {
"type": "string",
"title": "Subtitle",
"description": "Long-form supporting copy (rich text).",
"x-editor": "richtext",
"default": ""
},
"ctaLabel": {
"type": "string",
"title": "CTA label",
"x-editor": "text",
"default": ""
},
"ctaHref": {
"type": "string",
"title": "CTA URL",
"x-editor": "link",
"default": ""
},
"image": {
"type": "string",
"title": "Background image",
"x-editor": "media",
"default": ""
}
}
}

View File

@ -1,9 +0,0 @@
<section data-block="art-deco:press_quote" class="py-16">
<div class="max-w-3xl mx-auto px-4 text-center">
<div class="h-6 mb-6 deco-sunburst" aria-hidden="true"></div>
<blockquote class="italic text-2xl md:text-3xl leading-snug" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif); font-style: italic; color: hsl(var(--foreground));">{% if quote %}{{ quote|safe }}{% else %}<span style="color: hsl(var(--muted-foreground));">Add a press quote to populate this block.</span>{% endif %}</blockquote>
{% if stars|integer > 0 %}<div class="mt-4 flex justify-center gap-1" aria-label="Star rating" data-stars="{{ stars|integer }}">{% if stars|integer >= 1 %}<span data-deco-star="filled" aria-hidden="true" style="color: hsl(var(--primary));">★</span>{% else %}<span data-deco-star="empty" aria-hidden="true" style="color: hsl(var(--muted-foreground));">☆</span>{% endif %}{% if stars|integer >= 2 %}<span data-deco-star="filled" aria-hidden="true" style="color: hsl(var(--primary));">★</span>{% else %}<span data-deco-star="empty" aria-hidden="true" style="color: hsl(var(--muted-foreground));">☆</span>{% endif %}{% if stars|integer >= 3 %}<span data-deco-star="filled" aria-hidden="true" style="color: hsl(var(--primary));">★</span>{% else %}<span data-deco-star="empty" aria-hidden="true" style="color: hsl(var(--muted-foreground));">☆</span>{% endif %}{% if stars|integer >= 4 %}<span data-deco-star="filled" aria-hidden="true" style="color: hsl(var(--primary));">★</span>{% else %}<span data-deco-star="empty" aria-hidden="true" style="color: hsl(var(--muted-foreground));">☆</span>{% endif %}{% if stars|integer >= 5 %}<span data-deco-star="filled" aria-hidden="true" style="color: hsl(var(--primary));">★</span>{% else %}<span data-deco-star="empty" aria-hidden="true" style="color: hsl(var(--muted-foreground));">☆</span>{% endif %}</div>{% endif %}
{% if source %}<p class="mt-6 deco-caps text-xs" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--muted-foreground));">{{ source }}</p>{% endif %}
<div class="h-6 mt-6 deco-scallop" aria-hidden="true"></div>
</div>
</section>

View File

@ -1,31 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Press Quote",
"description": "Italic Cormorant pull-quote between fan dividers.",
"type": "object",
"properties": {
"quote": {
"type": "string",
"title": "Quote",
"description": "The press quotation (rich text).",
"x-editor": "richtext",
"default": ""
},
"source": {
"type": "string",
"title": "Source",
"description": "Attribution line — e.g. 'The Daily, March 2024'.",
"x-editor": "text",
"default": ""
},
"stars": {
"type": "integer",
"title": "Stars",
"description": "Filled-star count, clamped to 05.",
"x-editor": "number",
"default": 0,
"minimum": 0,
"maximum": 5
}
}
}

View File

@ -0,0 +1,15 @@
{# private_dining persona block — framed room image + gilded details panel. Media via {% img %}. #}
<section data-block="art-deco:private_dining" class="py-16 md:py-24 bg-background text-foreground">
<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 == "right" %}md:order-2{% else %}md:order-1{% endif %}">
{% if image %}<div class="deco-step">{% img image alt=title class="w-full h-auto object-cover" %}</div>{% endif %}
</div>
<div class="{% if mediaPosition == "right" %}md:order-1{% else %}md:order-2{% endif %}">
{% if eyebrow %}<div class="mb-3 flex items-center gap-4"><span class="deco-rule w-10"></span><span class="deco-caps text-xs" style="color:hsl(var(--primary));">{{ eyebrow }}</span></div>{% endif %}
{% if title %}<h2 class="deco-caps text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</h2>{% endif %}
{% if description %}<div class="mt-4 space-y-4 text-lg italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ description|safe }}</div>{% endif %}
{% if seats or minimum %}<dl class="mt-6 flex flex-wrap gap-x-10 gap-y-3">{% if seats %}<div><dt class="deco-caps text-xs" style="color:hsl(var(--primary));">Capacity</dt><dd class="mt-1">{{ seats }}</dd></div>{% endif %}{% if minimum %}<div><dt class="deco-caps text-xs" style="color:hsl(var(--primary));">Minimum</dt><dd class="mt-1">{{ minimum }}</dd></div>{% endif %}</dl>{% endif %}
{% if enquireUrl %}<a href="{{ enquireUrl }}" class="deco-pill mt-8 inline-flex items-center gap-2">{{ enquireLabel|default:"Enquire" }}</a>{% endif %}
</div>
</div>
</section>

View File

@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Private Dining",
"description": "Private dining room showcase — framed image with room details and an enquiry CTA.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small caps label above the heading.",
"x-editor": "text",
"default": "Private dining"
},
"title": {
"type": "string",
"title": "Room name",
"x-editor": "text",
"default": ""
},
"description": {
"type": "string",
"title": "Description",
"description": "Room description and what the experience includes.",
"x-editor": "richtext",
"default": ""
},
"image": {
"type": "string",
"title": "Room image",
"x-editor": "media",
"default": ""
},
"seats": {
"type": "string",
"title": "Seats",
"description": "e.g. 'Seats 2 to 14'.",
"x-editor": "text",
"default": ""
},
"minimum": {
"type": "string",
"title": "Minimum spend",
"description": "e.g. 'From $2,400'.",
"x-editor": "text",
"default": ""
},
"enquireLabel": {
"type": "string",
"title": "Enquire label",
"x-editor": "text",
"default": "Enquire"
},
"enquireUrl": {
"type": "string",
"title": "Enquire URL",
"x-editor": "link",
"default": ""
},
"mediaPosition": {
"type": "string",
"title": "Image side",
"enum": ["left", "right"],
"x-editor": "select",
"default": "left"
}
}
}

View File

@ -1 +1,20 @@
[]
[
{
"name": "Cinzel",
"family": "Cinzel",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/cinzel-400.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/cinzel-700.woff2" }
]
},
{
"name": "Cormorant Garamond",
"family": "Cormorant Garamond",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/cormorant-garamond-400.woff2" },
{ "weight": "400", "style": "italic", "file": "fonts/cormorant-garamond-400-italic.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/cormorant-garamond-600.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/cormorant-garamond-700.woff2" }
]
}
]

View File

@ -30,12 +30,79 @@ page_templates:
title: Grand Ballroom
description: "Edge-to-edge gallery layout for venues and lookbooks."
slots: [header, main, footer]
- system: art-deco
key: blog-index
title: Editorial Index
description: "Blog index with gilded masthead, featured and post-grid slots."
slots: [header, featured, main, footer]
- system: art-deco
key: contact
title: Reservations
description: "Contact layout with optional aside for hours and location."
slots: [header, main, aside, footer]
- system: art-deco
key: auth
title: Members
description: "Centred auth shell with wordmark and sunburst crest."
slots: [main, footer]
template_overrides:
# Chrome
- { template: art-deco, block: navbar }
- { template: art-deco, block: footer }
- { template: art-deco, block: utility-bar }
- { template: art-deco, block: announcement-bar }
# Marketing
- { template: art-deco, block: hero }
- { template: art-deco, block: feature-grid }
- { template: art-deco, block: rich-section }
- { template: art-deco, block: stats }
- { template: art-deco, block: testimonial }
- { template: art-deco, block: logo-strip }
- { template: art-deco, block: pricing }
- { template: art-deco, block: team }
- { template: art-deco, block: faq }
- { template: art-deco, block: timeline }
- { template: art-deco, block: cta }
- { template: art-deco, block: countdown }
# Primitives
- { template: art-deco, block: heading }
- { template: art-deco, block: text }
- { template: art-deco, block: button }
- { template: art-deco, block: rich-text }
- { template: art-deco, block: image }
- { template: art-deco, block: quote }
- { template: art-deco, block: divider }
- { template: art-deco, block: button }
- { template: art-deco, block: card }
- { template: art-deco, block: gallery }
- { template: art-deco, block: video-embed }
- { template: art-deco, block: audio-embed }
# Commerce / local
- { template: art-deco, block: menu-list }
- { template: art-deco, block: event-list }
- { template: art-deco, block: contact-form }
- { template: art-deco, block: contact-card }
- { template: art-deco, block: hours-location }
- { template: art-deco, block: social-links }
# Blog family
- { template: art-deco, block: blog-hero }
- { template: art-deco, block: featured-posts }
- { template: art-deco, block: related-posts }
- { template: art-deco, block: popular-posts }
- { template: art-deco, block: category-list }
- { template: art-deco, block: post-hero }
- { template: art-deco, block: post-metadata }
- { template: art-deco, block: author-bio }
- { template: art-deco, block: author-bio-hero }
- { template: art-deco, block: newsletter-signup }
- { template: art-deco, block: breadcrumbs }
- { template: art-deco, block: sidebar-nav }
# Auth surface
- { template: art-deco, block: auth-form }
- { template: art-deco, block: password-reset-form }
- { template: art-deco, block: auth-status }
# System
- { template: art-deco, block: page-suggestions }
email_wrappers:
- art-deco

View File

@ -2,12 +2,12 @@
{
"key": "art-deco:default-master",
"title": "Art Deco Default Master",
"page_templates": ["default", "article"],
"page_templates": ["default", "article", "blog-index", "contact"],
"blocks": [
{ "block_key": "navbar", "title": "Brass Navigation", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 },
{ "block_key": "art-deco:divider_fan", "title": "Header Fan Divider", "content": { "variant": "sunburst" }, "slot": "header", "sort_order": 1 },
{ "block_key": "navbar", "title": "Gilded Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "Maison Doré" } }, "slot": "header", "sort_order": 0 },
{ "block_key": "divider", "title": "Header Fan Divider", "content": { "variant": "ornament", "icon": "lucide:diamond" }, "slot": "header", "sort_order": 1 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
{ "block_key": "art-deco:footer", "title": "Site Footer", "content": { "address": "", "reservationsEmail": "", "showSocial": "true" }, "slot": "footer", "sort_order": 0 }
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "centered", "companyName": "Maison Doré", "tagline": "A supper club in the grand manner." }, "slot": "footer", "sort_order": 0 }
]
},
{
@ -15,11 +15,11 @@
"title": "Art Deco Marquee Master",
"page_templates": ["landing", "full-width"],
"blocks": [
{ "block_key": "navbar", "title": "Brass Navigation", "content": { "menuName": "main" }, "slot": "marquee", "sort_order": 0 },
{ "block_key": "art-deco:reservation", "title": "Reservation Strip", "content": { "phone": "", "openTable": "" }, "slot": "marquee", "sort_order": 1 },
{ "block_key": "navbar", "title": "Gilded Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "Maison Doré" } }, "slot": "marquee", "sort_order": 0 },
{ "block_key": "art-deco:reservation", "title": "Reservation Strip", "content": { "phone": "", "openTable": "", "hours": [] }, "slot": "marquee", "sort_order": 1 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "art-deco:divider_fan", "title": "CTA Fan Divider", "content": { "variant": "scallop" }, "slot": "cta", "sort_order": 0 },
{ "block_key": "art-deco:footer", "title": "Site Footer", "content": { "showSocial": "true" }, "slot": "footer", "sort_order": 0 }
{ "block_key": "divider", "title": "CTA Fan Divider", "content": { "variant": "ornament", "icon": "lucide:diamond" }, "slot": "cta", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "centered", "companyName": "Maison Doré", "tagline": "A supper club in the grand manner." }, "slot": "footer", "sort_order": 0 }
]
}
]

View File

@ -7,3 +7,4 @@ description = "Luxury Art Deco theme with champagne gold gradients, jet black, i
kind = "theme"
categories = ["templates"]
tags = ["luxury", "gold", "hospitality", "hotel", "wedding", "jewellery", "vintage", "glam", "fine-dining"]
required_icon_packs = ["lucide"]

View File

@ -52,9 +52,9 @@
{
"id": "ivory-rose",
"name": "Ivory & Rose Gold",
"description": "Soft ivory with rose-gold flourishes for weddings and jewellers.",
"description": "Soft ivory by day, warm ember by night, with rose-gold flourishes for weddings and jewellers.",
"theme": {
"mode": "light",
"mode": "both",
"lightColors": {
"background": "36 40% 97%",
"foreground": "20 15% 14%",
@ -75,15 +75,57 @@
"border": "25 30% 82%",
"input": "25 25% 86%",
"ring": "18 55% 58%"
},
"darkColors": {
"background": "20 18% 8%",
"foreground": "30 30% 92%",
"card": "20 18% 12%",
"cardForeground": "30 30% 92%",
"popover": "20 18% 12%",
"popoverForeground": "30 30% 92%",
"primary": "18 60% 62%",
"primaryForeground": "20 20% 8%",
"secondary": "20 15% 16%",
"secondaryForeground": "30 25% 88%",
"muted": "20 12% 14%",
"mutedForeground": "25 15% 62%",
"accent": "15 62% 62%",
"accentForeground": "20 20% 8%",
"destructive": "0 72% 52%",
"destructiveForeground": "30 30% 94%",
"border": "20 20% 24%",
"input": "20 18% 20%",
"ring": "18 60% 62%"
}
}
},
{
"id": "velvet-onyx",
"name": "Velvet Onyx",
"description": "Deep velvet onyx with gold and rose stage-lights for theatre and after-dark dining.",
"description": "Velvet-tinted ivory by day, deep onyx and gold stage-light by night, for theatre and after-dark dining.",
"theme": {
"mode": "dark",
"mode": "both",
"lightColors": {
"background": "280 15% 96%",
"foreground": "260 20% 12%",
"card": "280 12% 99%",
"cardForeground": "260 20% 12%",
"popover": "280 12% 99%",
"popoverForeground": "260 20% 12%",
"primary": "42 60% 48%",
"primaryForeground": "280 20% 8%",
"secondary": "280 15% 92%",
"secondaryForeground": "260 20% 12%",
"muted": "280 12% 93%",
"mutedForeground": "260 12% 40%",
"accent": "320 35% 52%",
"accentForeground": "280 20% 96%",
"destructive": "0 70% 45%",
"destructiveForeground": "280 15% 96%",
"border": "280 15% 82%",
"input": "280 12% 86%",
"ring": "42 60% 48%"
},
"darkColors": {
"background": "260 20% 6%",
"foreground": "42 40% 92%",
@ -106,5 +148,55 @@
"ring": "42 70% 60%"
}
}
},
{
"id": "emerald-gilt",
"name": "Emerald & Gilt",
"description": "Deco emerald and gilt — parchment and jade by day, emerald lacquer and gold by night.",
"theme": {
"mode": "both",
"lightColors": {
"background": "45 35% 95%",
"foreground": "160 25% 12%",
"card": "45 30% 98%",
"cardForeground": "160 25% 12%",
"popover": "45 30% 98%",
"popoverForeground": "160 25% 12%",
"primary": "42 55% 48%",
"primaryForeground": "160 25% 8%",
"secondary": "150 15% 88%",
"secondaryForeground": "160 25% 12%",
"muted": "150 12% 91%",
"mutedForeground": "160 12% 38%",
"accent": "160 40% 30%",
"accentForeground": "45 35% 95%",
"destructive": "0 70% 45%",
"destructiveForeground": "45 35% 95%",
"border": "150 20% 78%",
"input": "150 18% 83%",
"ring": "42 55% 48%"
},
"darkColors": {
"background": "162 25% 6%",
"foreground": "45 35% 92%",
"card": "162 22% 9%",
"cardForeground": "45 35% 92%",
"popover": "162 22% 9%",
"popoverForeground": "45 35% 92%",
"primary": "42 68% 58%",
"primaryForeground": "162 25% 8%",
"secondary": "162 18% 13%",
"secondaryForeground": "45 30% 88%",
"muted": "162 15% 11%",
"mutedForeground": "45 15% 62%",
"accent": "158 45% 45%",
"accentForeground": "162 25% 6%",
"destructive": "0 72% 52%",
"destructiveForeground": "45 35% 95%",
"border": "162 20% 20%",
"input": "162 18% 16%",
"ring": "42 68% 58%"
}
}
}
]

200
seed/seed.json Normal file
View File

@ -0,0 +1,200 @@
{
"demo": true,
"settings": {
"merge": {
"companyName": "Maison Doré",
"tagline": "A supper club in the grand manner."
}
},
"data_tables": [
{
"key": "contact_submissions",
"name": "Reservation Enquiries",
"description": "Seeded contact-form submissions for the Maison Doré 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": "party", "label": "Party size", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"pages": [
{
"slug": "/",
"title": "Maison Doré",
"template_key": "landing",
"blocks": [
{
"block_key": "hero",
"title": "Hero",
"slot": "main",
"sort_order": 1,
"content": {
"template": "centered",
"eyebrow": "Established 1926",
"headline": "Maison Doré",
"subheadline": "A supper club in the grand manner.",
"description": "Dinner and late suppers, served under gilded ceilings. A short tasting menu, a long cellar, and a room built for the evening.",
"ctaPrimary": { "text": "Reserve a table", "url": "/contact" },
"ctaSecondary": { "text": "Read the menu", "url": "#tasting" }
}
},
{
"block_key": "art-deco:chef_signature",
"title": "Chef's Signature",
"slot": "main",
"sort_order": 2,
"content": {
"eyebrow": "Chef's signature",
"dish": "Gold-leaf oyster, champagne mignonette",
"note": "The dish that opened the room in 1926. We still serve it the same way, on ice, one at a time.",
"chef": "Elise Marchand, Head Chef"
}
},
{
"block_key": "art-deco:menu",
"title": "Tasting Menu",
"slot": "main",
"sort_order": 3,
"content": {
"title": "Chef's Tasting",
"courses": [
{ "name": "Oysters, champagne mignonette", "description": "Half a dozen, on ice.", "price": "$32" },
{ "name": "Consommé doré", "description": "Clarified beef broth, tarragon, gold leaf.", "price": "$24" },
{ "name": "Turbot, beurre blanc", "description": "Line-caught, roasted on the bone.", "price": "$58" },
{ "name": "Crown of lamb", "description": "Herbs, salt-baked roots, jus.", "price": "$62" },
{ "name": "Grand Marnier soufflé", "description": "To finish. Order at the start.", "price": "$28" }
]
}
},
{
"block_key": "art-deco:private_dining",
"title": "Private Dining",
"slot": "main",
"sort_order": 4,
"content": {
"eyebrow": "Private dining",
"title": "The Gold Room",
"description": "A room of your own behind the main floor. One long table, a dedicated maître d', and the full cellar. We write the menu with you.",
"seats": "Seats 2 to 14",
"minimum": "From $2,400",
"enquireLabel": "Enquire",
"enquireUrl": "/contact",
"mediaPosition": "left"
}
}
]
},
{
"slug": "/about",
"title": "The House",
"template_key": "default",
"blocks": [
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "The House", "level": 1 } },
{ "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "<p>Maison Doré opened on a narrow street in 1926 and has kept its hours ever since. We take reservations for dinner and hold a few tables back for late suppers.</p><p>The kitchen runs a short tasting menu that changes with the season. The cellar runs long. The room stays the same, which is the point.</p>" } },
{ "block_key": "divider", "title": "Divider", "slot": "main", "sort_order": 3, "content": { "variant": "ornament", "icon": "lucide:diamond" } },
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 4, "content": { "style": "block", "align": "center", "quote": "Come for dinner, stay for the last pour.", "attribution": "The House" } }
]
},
{
"slug": "/blog",
"title": "From the Floor",
"template_key": "blog-index",
"blocks": [
{ "block_key": "card", "title": "Post: Gold Standard", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "The gold standard", "text": "<p>Why one oyster, served one at a time, still opens every table.</p>", "link": "/blog/the-gold-standard", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: A Night", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "A night at the club", "text": "<p>From the first cocktail to the last soufflé. How an evening unfolds.</p>", "link": "/blog/a-night-at-the-club", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: Cellar", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Notes from the cellar", "text": "<p>What we pour and why. A short walk through the list.</p>", "link": "/blog/notes-from-the-cellar", "linkLabel": "Read on" } }
]
},
{
"slug": "/blog/the-gold-standard",
"title": "The gold standard",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "The gold standard", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The oyster came first, in 1926, and it has not left the menu since. We serve it on ice, one at a time, with a champagne mignonette and a single leaf of gold.</p><p>The point is restraint. One oyster, done right, sets the tone for the meal that follows.</p>" } }
]
},
{
"slug": "/blog/a-night-at-the-club",
"title": "A night at the club",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "A night at the club", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Doors open at six. The first cocktail arrives before the menu. Dinner runs five courses, unhurried, and the soufflé is ordered at the start.</p><p>The band plays after nine. Late suppers run until the last table is ready to leave.</p>" } }
]
},
{
"slug": "/blog/notes-from-the-cellar",
"title": "Notes from the cellar",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Notes from the cellar", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The list is long and the choices are few, by design. We pour champagne by the glass, keep a deep bench of burgundy, and hold a small shelf of older bottles for the ones who ask.</p><p>Tell the sommelier what you are eating. The rest takes care of itself.</p>" } }
]
},
{
"slug": "/contact",
"title": "Reservations",
"template_key": "contact",
"blocks": [
{
"block_key": "contact-form",
"title": "Reservation Enquiry",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "Request a table",
"description": "Tell us the date, the hour and the party. We will confirm by email.",
"submitLabel": "Request a table",
"successMessage": "Thank you. Your table request has been received. We will confirm 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": "party", "label": "Party size", "type": "text", "required": false, "width": "half" },
{ "name": "message", "label": "Date, time and any notes", "type": "textarea", "required": true, "width": "full" }
],
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
}
},
{
"block_key": "hours-location",
"title": "Hours & Location",
"slot": "aside",
"sort_order": 1,
"content": {
"heading": "Hours",
"hours": [
{ "day": "Tuesday to Thursday", "hours": "6pm to late" },
{ "day": "Friday to Saturday", "hours": "5pm to late" },
{ "day": "Sunday", "hours": "5pm to 10pm" },
{ "day": "Monday", "closed": true }
],
"specialHoursNote": "Late suppers from 10pm, Friday and Saturday.",
"address": "12 Gilt Lane\nThe Old Quarter",
"phone": "+1 555 0126"
}
}
]
},
{
"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 book priority tables and private-room dates.</p>" } }
]
}
],
"menu_items": [
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
{ "menu": "main", "label": "The House", "page_slug": "/about", "sort_order": 2 },
{ "menu": "main", "label": "From the Floor", "page_slug": "/blog", "sort_order": 3 },
{ "menu": "main", "label": "Reservations", "page_slug": "/contact", "sort_order": 4 }
]
}

19
seed/workflows.json Normal file
View File

@ -0,0 +1,19 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Reservation enquiry to admins",
"description": "On a reservation-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

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col deco-grain" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, 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="{{ site_settings.Title }} home">
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt }}" class="mx-auto h-12 w-auto">{% else %}<span class="deco-caps text-2xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ site_settings.Title|default:"BlockNinja" }}</span>{% endif %}
</a>
<div class="mx-auto mt-4 mb-6 h-6 w-32 deco-sunburst" aria-hidden="true"></div>
<div>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" 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="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, 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">
<h1 class="deco-caps deco-underline mx-auto w-fit text-4xl md:text-5xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);">{{ title }}</h1>
{% if page_meta.MetaDescription %}<p class="mt-4 text-lg italic" style="color:hsl(var(--muted-foreground));">{{ page_meta.MetaDescription }}</p>{% endif %}
<div class="mx-auto mt-6 h-6 w-40 deco-sunburst" aria-hidden="true"></div>
</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 class="italic" style="color:hsl(var(--muted-foreground));">No posts published yet.</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="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, 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-[hsl(var(--primary)/0.3)] 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 class="italic" 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

@ -0,0 +1,21 @@
{# announcement-bar override — gilded banner. Dismiss <script>, data-bn-annkey and localStorage keying preserved verbatim. Fields mirror builtin: variant, message, linkText, linkUrl, dismissable. #}
{% set anncls = "bg-primary/15 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="art-deco:announcement-bar" data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full border-b border-[hsl(var(--primary)/0.4)] {{ 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="deco-caps text-xs">{{ 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 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 — gold-framed player card. Fields + facade JS mirror builtin verbatim: url, media, artwork, title, artist, download. #}
<figure data-block-override="art-deco:audio-embed" class="bn-audio deco-frame my-8 overflow-hidden bg-card text-card-foreground{% 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="deco-step 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="deco-caps truncate text-sm" style="font-family:var(--font-heading, 'Cinzel', 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="deco-pill inline-flex items-center gap-2 text-sm" 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="deco-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 — gilded login/register. 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). #}
{% if context.isPublicLoggedIn %}
<div data-block-override="art-deco:auth-form" class="auth-form-logged-in mx-auto max-w-md text-center py-8">
<p class="italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">You are signed in as <strong style="color:hsl(var(--foreground));">{{ context.publicUsername }}</strong></p>
<a href="/account" class="deco-link mt-3 inline-block">My account</a>
</div>
{% else %}
<div data-block-override="art-deco:auth-form" class="auth-form deco-frame mx-auto max-w-md bg-card p-8">
<div id="auth-message"></div>
<div class="mb-6 flex border-b border-[hsl(var(--primary)/0.3)]">
<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="deco-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary">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="deco-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--primary))] data-[active]:text-primary">Register</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="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-none 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="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required class="w-full rounded-none 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="deco-pill w-full" hx-disabled-elt="this">Sign in</button>
<p class="text-center mt-3 text-sm"><a href="/forgot-password" class="deco-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="deco-caps block text-xs 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-none 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="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-none 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="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Password</label><input type="password" name="password" required minlength="8" class="w-full rounded-none 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="deco-pill 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 / Reserve links. Logged-in: gold monogram + dropdown (logout posts /api/auth/logout). Driven by context.isPublicLoggedIn / context.publicDisplayName / context.publicUsername. #}
{% if context.isPublicLoggedIn %}
<div data-block-override="art-deco:auth-status" class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="deco-caps flex items-center gap-2 text-xs 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 deco-frame bg-card py-1 z-50">
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My profile</a>
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My reviews</a>
<hr class="my-1 deco-rule"/>
<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-accent">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="art-deco:auth-status" class="auth-status flex items-center gap-2">
<a href="/login" class="deco-caps text-xs px-3 py-1.5 opacity-80 hover:opacity-100 hover:text-primary transition-colors">Sign in</a>
<a href="/login?tab=register" class="deco-pill text-xs">Reserve</a>
</div>
{% endif %}

View File

@ -0,0 +1,22 @@
{# author-bio-hero (art-deco) — author-page header from the `authors` provider. #}
{% if author %}
<section data-block-override="art-deco:author-bio-hero" class="deco-rule-thick bg-background 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="mb-6 h-24 w-24 overflow-hidden rounded-full bg-muted ring-2 ring-primary">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="mb-4 flex items-center justify-center gap-4"><span class="deco-rule w-12"></span><span class="deco-rule w-12"></span></div>
<h1 class="deco-caps text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="deco-caps mt-2 text-sm text-primary">{{ 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, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, 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,14 @@
{# author-bio (art-deco) — byline card sourced from the `post` provider's author_* fields. #}
{% if post.author_name %}
<aside data-block-override="art-deco:author-bio" class="deco-frame bg-card p-6 text-card-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="deco-caps mb-4 text-xs text-primary">{{ heading }}</p>{% endif %}
<div class="flex items-start gap-4">
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full bg-muted ring-1 ring-[hsl(var(--primary)/0.5)]">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="deco-caps text-base text-card-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{% 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>
<span class="deco-rule mt-2 block w-10"></span>
{% if post.author_bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif)">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -0,0 +1,24 @@
{# blog-hero (art-deco) — blog-index header + featured posts from the `posts` provider. #}
<section data-block-override="art-deco:blog-hero" class="bg-background 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">
{% if title %}<h1 class="deco-caps deco-underline text-4xl md:text-5xl text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="mt-4 text-lg text-muted-foreground" style="font-family:var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, 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="deco-step 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 %}">
<span class="deco-rule mb-4 block w-12"></span>
<h2 class="deco-caps {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} leading-snug text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)"><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, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, 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="deco-caps text-primary">{{ 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,15 @@
{# breadcrumbs (art-deco) — trail from the `breadcrumbs` provider (page ancestry). #}
{# showHome prepends a Home link; separator is text ("/") for default or a chevron #}
{# for the arrows template. The trailing is_current item renders as plain text. #}
{% set arrows = template == "arrows" %}
<nav aria-label="Breadcrumb" data-block-override="art-deco: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="deco-caps 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-primary" 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-primary">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
{% if item.is_current %}{% if showCurrent %}<span class="deco-caps text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="deco-caps text-muted-foreground transition-colors hover:text-primary">{{ item.label }}</a>{% endif %}
</li>
{% endfor %}
</ol>
</nav>

View File

@ -1 +1,10 @@
{% if label %}{% if url %}<a data-block-override="art-deco:button" href="{{ url }}" class="deco-pill inline-block" style="{% if variant == "secondary" %}background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border-color: hsl(var(--primary));{% elif variant == "ghost" %}background-color: transparent; color: hsl(var(--foreground)); border-color: hsl(var(--primary));{% else %}background-color: hsl(var(--foreground)); color: hsl(var(--background)); border-color: hsl(var(--primary));{% endif %}">{{ label }}</a>{% else %}<button type="button" data-block-override="art-deco:button" class="deco-pill" style="{% if variant == "secondary" %}background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); border-color: hsl(var(--primary));{% elif variant == "ghost" %}background-color: transparent; color: hsl(var(--foreground)); border-color: hsl(var(--primary));{% else %}background-color: hsl(var(--foreground)); color: hsl(var(--background)); border-color: hsl(var(--primary));{% endif %}">{{ label }}</button>{% endif %}{% endif %}
{# button override — gold-ringed lacquer pill. 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="art-deco: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="deco-link inline-flex items-center gap-2 text-sm">{% 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="deco-pill inline-flex items-center gap-2">{% 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

@ -0,0 +1,4 @@
{# card override — angular gold Deco panel. Fields mirror builtin: layout, link, media, title, text, linkLabel. Media via {% img %}. #}
{% with layout=layout|default:"vertical" %}
<article data-block-override="art-deco:card" class="bn-card deco-frame overflow-hidden bg-card text-card-foreground transition-shadow hover:deco-foil {% 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="deco-caps text-lg" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="mt-2 inline-flex items-center gap-1 text-sm deco-link">{{ linkLabel }}::lucide:arrow-right:sm::</span>{% endif %}</div>{% if link %}</a>{% endif %}</article>
{% endwith %}

View File

@ -0,0 +1,18 @@
{# category-list (art-deco) — from the `categories` provider. list or pills. #}
{% set pills = style == "pills" %}
<section data-block-override="art-deco:category-list" class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="deco-caps mb-5 text-xs text-primary">{{ heading }}</h2>{% endif %}
{% if pills %}
<div class="flex flex-wrap gap-2">
{% for cat in categories %}
<a href="{{ cat.url }}" class="deco-caps inline-flex items-center gap-1.5 rounded-none border border-[hsl(var(--primary)/0.35)] bg-card px-4 py-1.5 text-xs text-card-foreground transition-colors hover:border-primary hover:text-primary">{{ cat.name }}{% if showCount %}<span class="text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a>
{% endfor %}
</div>
{% else %}
<ul class="space-y-1">
{% for cat in categories %}
<li><a href="{{ cat.url }}" class="deco-rule flex items-center justify-between px-3 py-2.5 text-sm text-muted-foreground transition-colors hover:text-primary"><span class="deco-caps">{{ cat.name }}</span>{% if showCount %}<span class="rounded-none border border-[hsl(var(--primary)/0.35)] px-2 py-0.5 text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a></li>
{% endfor %}
</ul>
{% endif %}
</section>

View File

@ -0,0 +1,17 @@
<section data-block-override="art-deco: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="deco-caps deco-underline text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ heading }}</h2>{% endif %}
{% if description %}<p class="mt-4 mx-auto max-w-2xl text-lg text-muted-foreground" style="font-family:var(--font-body, 'Cormorant Garamond', 'Cormorant', Georgia, 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="deco-frame deco-foil flex items-start gap-4 rounded-none bg-card p-6 text-card-foreground">
{% if channel.icon %}<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-none border border-[hsl(var(--primary)/0.45)] bg-primary/10 text-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="deco-caps text-xs tracking-[0.16em] text-muted-foreground">{{ channel.label }}</div>{% endif %}
{% if channel.value %}<div class="mt-1 break-words text-lg text-foreground" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{% if channel.link %}<a href="{{ channel.link }}" class="deco-link transition-colors hover:text-primary">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,43 @@
{# contact-form override — reservations enquiry. ALL form wiring preserved verbatim from builtin: bid, {% form %} action/hx attrs, honeypot, field loop, form.submitted/errors, submit target. Only inputs restyled to Art Deco. #}
{% with bid=_block_id|default:context.blockId %}
<section data-block-override="art-deco: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="deco-frame bg-card p-10 text-center">
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full text-primary" style="box-shadow:inset 0 0 0 1px hsl(var(--primary));">::lucide:check:lg::</div>
<p class="text-lg italic" style="font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ form.message|default:successMessage|default:"Thank you. Your table request has been received." }}</p>
</div>
{% else %}
<div class="text-center">
{% if heading %}<h2 class="deco-caps text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ heading }}</h2>{% endif %}
{% if description %}<p class="mt-3 text-lg italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ description }}</p>{% endif %}
<div class="mx-auto mt-5 mb-2 h-6 w-40 deco-sunburst" aria-hidden="true"></div>
</div>
{% if form.errors._form %}<div class="mt-6 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="deco-caps mb-1.5 block text-xs" 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-none 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-none 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-none 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="deco-pill inline-flex items-center justify-center">{{ submitLabel|default:"Request a table" }}</button>
</div>
{% endform %}
{% endwith %}
{% endif %}
</div>
</section>
{% endwith %}

View File

@ -0,0 +1,48 @@
<section data-block-override="art-deco:countdown" 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="deco-caps text-3xl tracking-tight md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ 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, 'Cormorant Garamond', 'Cormorant', Georgia, 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" %}deco-frame min-w-[4.5rem] bg-card px-3 py-4 text-card-foreground{% endif %}"><span class="text-4xl font-bold tabular-nums text-primary sm:text-5xl" style="font-family:var(--font-mono)" data-unit="days">--</span><span class="deco-caps mt-1 text-xs text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}deco-frame min-w-[4.5rem] bg-card px-3 py-4 text-card-foreground{% endif %}"><span class="text-4xl font-bold tabular-nums text-primary sm:text-5xl" style="font-family:var(--font-mono)" data-unit="hours">--</span><span class="deco-caps mt-1 text-xs text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}deco-frame min-w-[4.5rem] bg-card px-3 py-4 text-card-foreground{% endif %}"><span class="text-4xl font-bold tabular-nums text-primary sm:text-5xl" style="font-family:var(--font-mono)" data-unit="minutes">--</span><span class="deco-caps mt-1 text-xs text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}deco-frame min-w-[4.5rem] bg-card px-3 py-4 text-card-foreground{% endif %}"><span class="text-4xl font-bold tabular-nums text-primary sm:text-5xl" style="font-family:var(--font-mono)" data-unit="seconds">--</span><span class="deco-caps mt-1 text-xs text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
</div>
<div class="mt-8 hidden" data-countdown-expired>
<p class="deco-caps text-xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ expiredMessage|default:"This has started." }}</p>
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="deco-pill mt-4 inline-flex items-center gap-2">{{ 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,14 @@
<section data-block-override="art-deco: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" %}deco-frame deco-grain bg-primary px-6 py-14 text-center text-primary-foreground md:px-12{% elif background == "card" %}deco-frame bg-card px-6 py-14 text-center text-card-foreground md:px-12{% else %}deco-frame flex flex-col items-center gap-8 bg-muted px-6 py-12 text-foreground md:flex-row md:justify-between md:px-12 md:text-left{% endif %}">
<div class="{% if background != "split" %}mx-auto max-w-2xl{% endif %}">
{% if heading %}<h2 class="deco-caps text-3xl tracking-tight md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', 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, 'Cormorant Garamond', 'Cormorant', Georgia, 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="deco-pill inline-flex items-center gap-2">{{ 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="deco-link inline-flex items-center">{{ secondaryLabel }}</a>{% endif %}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,13 @@
{# divider override — folds in the old divider_fan: gold geometric rules. Fields mirror builtin: variant (line|ornament|spacer-rule), weight, width, icon. ornament renders a sunburst fan crest; spacer-rule a short centred gold rule; line a hairline gold rule. #}
{% with variant=variant|default:"line" weight=weight|default:"thin" width=width|default:"full" %}
{% if variant == "ornament" %}
<div data-block-override="art-deco: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">
<div class="h-8 w-40 deco-sunburst" aria-hidden="true"></div>
<div class="flex w-full items-center gap-4"><span class="deco-rule flex-1"></span><span style="color:hsl(var(--primary));">::{{ icon|default:"lucide:diamond" }}:sm::</span><span class="deco-rule flex-1"></span></div>
</div>
{% elif variant == "spacer-rule" %}
<div data-block-override="art-deco: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="{% if weight == "thick" %}deco-rule-thick{% else %}deco-rule{% endif %} w-16"></span></div>
{% else %}
<div data-block-override="art-deco: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="{% if weight == "thick" %}deco-rule-thick{% else %}deco-rule{% endif %} w-full"></span></div>
{% endif %}
{% endwith %}

View File

@ -0,0 +1,40 @@
<section data-block-override="art-deco: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="deco-caps deco-underline text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-4 mx-auto max-w-2xl text-lg text-muted-foreground" style="font-family:var(--font-body, 'Cormorant Garamond', 'Cormorant', Georgia, 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="deco-frame deco-foil flex flex-col overflow-hidden rounded-none bg-card text-card-foreground{% if event.past %} opacity-60{% endif %}">
{% if event.media %}<div class="deco-step 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="deco-caps text-xs tracking-[0.18em] text-primary"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} &ndash; {{ event.end }}{% endif %}</time>{% if event.timezone %} <span class="text-muted-foreground">{{ event.timezone }}</span>{% endif %}</div>
<h3 class="text-xl text-foreground" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ event.title }}</h3>
{% if event.venue %}<p class="deco-caps text-xs tracking-[0.14em] text-muted-foreground">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="text-sm text-muted-foreground" style="font-family:var(--font-body, 'Cormorant Garamond', 'Cormorant', Georgia, serif)">{{ event.description }}</p>{% endif %}
{% if event.link %}<a href="{{ event.link }}" class="deco-link mt-auto inline-flex items-center gap-2 pt-3 text-sm text-primary">{{ 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 divide-[hsl(var(--primary)/0.35)] border-y-2 border-[hsl(var(--primary)/0.35)] text-left">
{% 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="deco-caps text-sm tracking-[0.16em] text-primary"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
{% if event.end %}<div class="text-xs text-muted-foreground">to {{ event.end }}</div>{% endif %}
{% if event.timezone %}<div class="text-xs text-muted-foreground">{{ event.timezone }}</div>{% endif %}
</div>
<div class="min-w-0 flex-1">
<h3 class="text-lg text-foreground" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ event.title }}</h3>
{% if event.venue %}<p class="deco-caps text-xs tracking-[0.14em] text-muted-foreground">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="mt-1 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Cormorant Garamond', 'Cormorant', Georgia, serif)">{{ event.description }}</p>{% endif %}
</div>
{% if event.link %}<a href="{{ event.link }}" class="deco-pill inline-flex shrink-0 items-center gap-2">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
</li>
{% endif %}{% endfor %}
</ul>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,17 @@
<section data-block-override="art-deco:faq" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4">
{% if heading %}<h2 class="deco-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ 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, 'Cormorant Garamond', 'Cormorant', Georgia, serif)">{{ intro }}</p>{% endif %}
<div class="mt-10 divide-y divide-[hsl(var(--primary)/0.25)] border-y border-[hsl(var(--primary)/0.4)]">
{% 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-medium marker:content-none [&::-webkit-details-marker]:hidden" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">
<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, 'Cormorant Garamond', 'Cormorant', Georgia, serif)">{{ item.answer|safe }}</div>
</details>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,17 @@
<section data-block-override="art-deco:feature-grid" class="py-16 md:py-24 bg-background 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-4"><span class="deco-rule w-12"></span><p class="deco-caps text-xs text-primary">{{ eyebrow }}</p><span class="deco-rule w-12"></span></div>{% endif %}
{% if heading %}<h2 class="deco-caps mt-2 text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ 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, 'Cormorant Garamond', 'Cormorant', Georgia, 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" %}deco-frame bg-card p-6 text-card-foreground deco-foil 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 border border-[hsl(var(--primary)/0.35)] 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="deco-caps text-lg text-foreground" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{% 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, 'Cormorant Garamond', 'Cormorant', Georgia, serif)">{{ item.text }}</p>{% endif %}
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="deco-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,24 @@
{# featured-posts (art-deco) — grid of cards from the `posts` provider #}
{# (params: limit, featured=true). Card: image, title, excerpt, meta. #}
<section data-block-override="art-deco:featured-posts" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="deco-caps deco-underline mb-10 text-center text-2xl md:text-3xl text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{{ heading }}</h2>{% 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="deco-frame group flex flex-col overflow-hidden bg-card 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 text-center">
<h3 class="deco-caps text-lg leading-snug text-card-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h3>
<span class="deco-rule mx-auto mt-3 block w-10"></span>
{% if showExcerpt and post.excerpt %}<p class="mt-3 flex-1 text-sm text-muted-foreground" style="font-family:var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif)">{{ post.excerpt }}</p>{% endif %}
<div class="mt-4 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
{% if showAuthor and post.author_name %}<span class="deco-caps text-primary">{{ 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 — folds in the old footer block: centred gilded footer with gold rule, address, reservations, social. Structure/fields/signup_form preserved from builtin; reskinned to Art Deco. #}
{% set fl = layout %}
{% if not fl %}{% if template == "centered" %}{% set fl = "centered" %}{% else %}{% set fl = "columns" %}{% endif %}{% endif %}
{% set fbg = backgroundColor|default:"bg-foreground text-background" %}
{% 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-background/70 hover:text-primary 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="deco-caps text-sm text-primary">{{ newsletterTitle|default:"Join the guest list" }}</h3>{% if newsletterDescription %}<p class="mt-1 text-sm italic text-background/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 border-t border-[hsl(var(--primary)/0.4)] flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-background/60"><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-primary transition-colors">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
<footer data-block-override="art-deco:footer" class="w-full border-t-2 border-[hsl(var(--primary))] {{ 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="deco-caps text-lg" style="font-family:var(--font-heading, 'Cinzel', Georgia, serif);">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="text-sm italic text-background/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="deco-caps text-2xl text-primary" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);">{{ companyName }}</span>{% endif %}
<div class="h-6 w-40 deco-sunburst" aria-hidden="true"></div>
{% if tagline %}<p class="max-w-md text-sm italic text-background/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="deco-caps text-xs text-background/80 hover:text-primary 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="deco-caps text-xl text-primary" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);">{{ companyName }}</span>{% endif %}
{% if tagline %}<p class="mt-3 max-w-xs text-sm italic text-background/70">{{ tagline }}</p>{% endif %}
{% if address or email or phone %}<address class="mt-4 not-italic text-sm text-background/70 space-y-1">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="hover:text-primary transition-colors">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="hover:text-primary transition-colors">{{ phone }}</a></div>{% endif %}</address>{% endif %}
{{ sociallinks("mt-5") }}
</div>
{% for col in columns %}<div><h3 class="deco-caps text-xs text-primary">{{ 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="text-sm text-background/70 hover:text-primary transition-colors">{{ 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 — folds in the old gallery_fan: symmetric gold-framed grid. 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="art-deco: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="deco-step 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 data-block-override="art-deco:heading" class="deco-caps deco-underline text-4xl md:text-5xl {{ textClass }}" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));">{{ text }}</h1>{% elif level|integer == 3 %}<h3 data-block-override="art-deco:heading" class="deco-caps deco-underline text-2xl md:text-3xl {{ textClass }}" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));">{{ text }}</h3>{% elif level|integer == 4 %}<h4 data-block-override="art-deco:heading" class="deco-caps deco-underline text-xl md:text-2xl {{ textClass }}" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));">{{ text }}</h4>{% elif level|integer == 5 %}<h5 data-block-override="art-deco:heading" class="deco-caps deco-underline text-lg md:text-xl {{ textClass }}" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));">{{ text }}</h5>{% elif level|integer == 6 %}<h6 data-block-override="art-deco:heading" class="deco-caps deco-underline text-base md:text-lg {{ textClass }}" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));">{{ text }}</h6>{% else %}<h2 data-block-override="art-deco:heading" class="deco-caps deco-underline text-3xl md:text-4xl {{ textClass }}" style="font-family: var(--font-heading, &quot;Italiana&quot;, &quot;Cinzel&quot;, Georgia, serif); color: hsl(var(--foreground));">{{ text }}</h2>{% endif %}
{# heading override — gilded Cinzel display caps with a hairline gold underline. Fields: text, level, textClass (mirrors builtin). #}
{% if level == 1 %}<h1 data-block-override="art-deco:heading" class="deco-caps deco-underline text-4xl md:text-5xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h1>
{% elif level == 3 %}<h3 data-block-override="art-deco:heading" class="deco-caps deco-underline text-2xl md:text-3xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h3>
{% elif level == 4 %}<h4 data-block-override="art-deco:heading" class="deco-caps deco-underline text-xl md:text-2xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h4>
{% elif level == 5 %}<h5 data-block-override="art-deco:heading" class="deco-caps deco-underline text-lg md:text-xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h5>
{% elif level == 6 %}<h6 data-block-override="art-deco:heading" class="deco-caps deco-underline text-base md:text-lg flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h6>
{% else %}<h2 data-block-override="art-deco:heading" class="deco-caps deco-underline text-3xl md:text-4xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h2>{% endif %}

View File

@ -0,0 +1,40 @@
{# hero override — folds in the old marquee_hero: sunburst overlay + stepped ziggurat framing, gold-foil Cinzel headline. Structure/fields/{% img %} calls preserved from builtin. 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-4 {% if align == "center" %}justify-center{% endif %}"><span class="deco-rule w-10"></span><span class="deco-caps text-xs text-primary">{{ eyebrowText }}</span><span class="deco-rule w-10"></span></div>{% endif %}
{% if headlineText %}<h1 class="deco-caps text-4xl sm:text-5xl lg:text-6xl leading-tight" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ headlineText }}</h1>{% endif %}
{% if subText %}<h2 class="mt-5 text-xl sm:text-2xl italic max-w-2xl{% if align == "center" %} mx-auto{% endif %}" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, 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="deco-pill inline-flex items-center justify-center gap-2">{{ pText }}</a>{% endif %}
{% if sText and sUrl %}<a href="{{ sUrl }}" class="deco-link inline-flex items-center justify-center">{{ sText }}</a>{% endif %}
</div>{% endif %}
{% if trustLogos %}<div class="mt-10">
<p class="deco-caps text-xs text-muted-foreground mb-3">Acclaim</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="art-deco: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 deco-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="art-deco:hero" class="hero relative flex flex-col overflow-hidden bg-background text-foreground deco-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="pointer-events-none absolute inset-x-0 top-0 z-[1] h-40 opacity-40 deco-sunburst" aria-hidden="true"></div>
<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 %}"><div class="deco-frame bg-transparent px-6 py-10 sm:px-10 sm:py-14">{{ herocontent(align) }}</div></div>
</section>
{% endif %}

View File

@ -0,0 +1,31 @@
{# hours-location override — gilded hours ledger + framed map. Map facade <button onclick> preserved verbatim. Fields mirror builtin: heading, hours[].day/.hours/.closed, specialHoursNote, address, phone, directionsUrl, mapEmbedUrl, mapButtonLabel. #}
<section data-block-override="art-deco: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="deco-caps text-3xl md:text-4xl text-center" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ heading }}</h2>{% endif %}
<div class="mt-10 grid grid-cols-1 gap-10 md:grid-cols-2">
<div class="deco-frame bg-card p-6 md:p-8">
{% if hours %}
<table class="w-full text-sm">
<tbody class="divide-y divide-[hsl(var(--primary)/0.2)]">
{% for row in hours %}
<tr>
<th scope="row" class="deco-caps py-2 pr-4 text-left text-xs font-medium" 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(--primary));{% endif %}">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if specialHoursNote %}<p class="mt-4 border-t border-[hsl(var(--primary)/0.3)] pt-3 text-sm italic" style="color:hsl(var(--muted-foreground));">{{ specialHoursNote }}</p>{% endif %}
{% if address %}<div class="mt-6"><div class="deco-caps text-xs" style="color:hsl(var(--primary));">Address</div><address class="mt-1 whitespace-pre-line not-italic" style="font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ address }}</address></div>{% endif %}
{% if phone %}<div class="mt-4"><div class="deco-caps text-xs" style="color:hsl(var(--primary));">Reservations</div><a href="tel:{{ phone }}" class="deco-link mt-1 inline-block">{{ phone }}</a></div>{% endif %}
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener noreferrer" class="deco-pill mt-6 inline-flex items-center gap-2">::lucide:navigation:sm:: Get directions</a>{% endif %}
</div>
{% if mapEmbedUrl %}
<div class="deco-step 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="deco-caps flex h-full min-h-[16rem] w-full items-center justify-center gap-2 text-sm 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 data-block-override="art-deco:image" class="my-8"><div class="{% if frame == "scallop" %}deco-scallop{% elif frame == "plain" %}{% else %}deco-step{% endif %}"><img src="{% if src %}{{ src }}{% else %}data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22%23222%22%2F%3E%3C%2Fsvg%3E{% endif %}" alt="{{ alt }}" class="block w-full h-auto"></div>{% if caption %}<figcaption class="mt-2 italic text-center text-sm" style="color: hsl(var(--muted-foreground));">{{ caption }}</figcaption>{% endif %}</figure>
{# image override — stepped gold Deco frame. 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="art-deco: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="deco-step {% 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 %}">
<div class="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, 'Cormorant Garamond', Georgia, serif);">{{ caption }}</figcaption>{% endif %}
</figure>
{% endwith %}

View File

@ -0,0 +1,15 @@
<section data-block-override="art-deco:logo-strip" class="py-12 md:py-16 bg-background 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-4"><span class="deco-rule w-12"></span><p class="deco-caps text-xs text-muted-foreground">{{ heading }}</p><span class="deco-rule w-12"></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 %}
{% if logo.url %}<a href="{{ logo.url }}" class="block shrink-0 opacity-80 transition hover:opacity-100">{% endif %}
<span class="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 %}
{% 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 — the supper-club menu card: centred Cinzel section titles, gold leader rules, symmetric columns. Fields mirror builtin: heading, intro, columns, sections[].title/.note/.items[].name/.badges/.description/.price, currency, footnote. #}
<section data-block-override="art-deco: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="deco-caps text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ 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, 'Cormorant Garamond', Georgia, serif);">{{ intro }}</p>{% endif %}
<div class="mx-auto mt-6 mb-4 h-6 w-40 deco-sunburst" aria-hidden="true"></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="deco-caps deco-underline pb-2 text-center text-xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--primary));">{{ 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 border-[hsl(var(--primary)/0.5)]" aria-hidden="true"></span>
{% if item.price %}<span class="shrink-0 tabular-nums" style="color:hsl(var(--primary));font-family:var(--font-heading, 'Cinzel', Georgia, serif);">{% 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="deco-caps inline-block border border-[hsl(var(--primary)/0.4)] px-2 py-0.5 text-[0.65rem]" style="color:hsl(var(--muted-foreground));">{{ 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, 'Cormorant Garamond', Georgia, 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 — gilded masthead. Structure, fixed drawer ids, focus-trap JS and all data-* preserved verbatim from the builtin; presentation reskinned to Art Deco. 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="deco-caps px-3 py-2 text-xs text-foreground/80 hover:text-primary transition-colors {{ item.css_class }}">{{ item.label }}</a>{% endmacro %}
{% macro dropdown(item) %}<div class="relative group">
<button type="button" class="deco-caps px-3 py-2 text-xs text-foreground/80 hover:text-primary transition-colors inline-flex items-center gap-1">{{ 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-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50"><div class="deco-frame p-6 bg-card 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="deco-caps px-2 py-1 text-[0.65rem] text-primary border-b border-[hsl(var(--primary)/0.3)] 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 hover:bg-accent 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-1 w-56 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50"><div class="deco-frame py-2 bg-card">{% for child in item.children %}{% if child.item_type == "header" %}<div class="deco-caps px-4 py-2 text-[0.65rem] 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-accent 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="deco-caps text-xl {{ logoText.color }}" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ 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="deco-caps text-xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">{{ companyName }}</span>{% endif %}</a>{% endmacro %}
{% macro ctabtn(extra) %}{% if cta_text %}<a href="{{ cta_url|default:'#' }}" class="deco-pill inline-flex items-center {{ extra }}">{{ cta_text }}</a>{% endif %}{% endmacro %}
<nav data-block-override="art-deco: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-[hsl(var(--primary)/0.4)]{% endif %} {{ class }}">
{% if showUtilityBar %}<div class="w-full text-xs {% if not showOnMobile %}hidden sm:block{% endif %} bg-foreground text-background"><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-md cursor-pointer hover:bg-accent/50 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-[hsl(var(--primary)/0.4)]" role="dialog" aria-modal="true" aria-label="Site menu">
<div class="flex items-center justify-between p-4 border-b border-[hsl(var(--primary)/0.3)]">{{ brand("h-8") }}<label for="bn-navbar-toggle" data-bn-nav-close class="p-2 rounded-md cursor-pointer text-muted-foreground hover:text-primary hover:bg-accent"><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="deco-caps flex items-center justify-between px-6 py-3 text-sm cursor-pointer hover:bg-accent 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="deco-caps block px-6 py-3 text-sm hover:bg-accent transition-colors">{{ item.label }}</a>{% endif %}{% endfor %}</nav>
{% if cta_text %}<div class="p-4 border-t border-[hsl(var(--primary)/0.3)]"><a href="{{ cta_url|default:'#' }}" class="deco-pill block w-full text-center">{{ 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(--primary) / 0.4);}
</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,13 @@
{# newsletter-signup (art-deco) — the {% signup_form %} tag carries the #}
{# untouchable subscribe wiring (/api/lists/subscribe HTMX post + honeypot); #}
{# this template supplies only the art-deco chrome around it. #}
<section data-block-override="art-deco:newsletter-signup" class="py-12 bg-background text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4">
<div class="deco-frame {% if layout == "inline" %}deco-grain bg-primary px-6 py-12 text-primary-foreground md:px-12{% else %}bg-card px-6 py-12 text-card-foreground md:px-10{% endif %} text-center">
<div class="mb-4 flex items-center justify-center gap-4"><span class="deco-rule w-12"></span><span class="deco-rule w-12"></span></div>
{% if title %}<h2 class="deco-caps text-2xl md:text-3xl" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, 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, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, 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. Fields: title, showDescription, emptyMessage; suggestions[] from the provider (.slug/.title/.excerpt). #}
{% if suggestions %}
<div data-block-override="art-deco:page-suggestions" class="page-suggestions py-10">
{% if title %}<h2 class="deco-caps deco-underline mx-auto w-fit text-xl text-center" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', 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="deco-link font-medium">{{ suggestion.title }}</a>
{% if showDescription and suggestion.excerpt %}<p class="mt-1 text-sm italic" style="color:hsl(var(--muted-foreground));">{{ suggestion.excerpt }}</p>{% endif %}
</div>
</li>
{% endfor %}
</ul>
</div>
{% elif emptyMessage %}
<div data-block-override="art-deco:page-suggestions" class="page-suggestions py-10 text-center">
<p class="italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, serif);">{{ emptyMessage }}</p>
</div>
{% endif %}

View File

@ -0,0 +1,13 @@
{# password-reset-form override — request stage only, matching builtin. Wiring UNTOUCHABLE: /api/auth/request-password-reset (email), hx-target #reset-message. Reskinned to Art Deco. #}
<div data-block-override="art-deco:password-reset-form" class="password-reset-form deco-frame mx-auto max-w-md bg-card p-8">
<div id="reset-message"></div>
<div id="request-reset-panel" class="text-center">
<h2 class="deco-caps text-2xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif);color:hsl(var(--foreground));">Reset password</h2>
<p class="mt-3 text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, 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="deco-caps block text-xs mb-1" style="color:hsl(var(--primary));">Email</label><input type="email" name="email" required class="w-full rounded-none 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="deco-pill w-full" hx-disabled-elt="this">Send reset link</button>
</form>
<p class="text-center mt-4 text-sm"><a href="/login" class="deco-link">Back to sign in</a></p>
</div>
</div>

View File

@ -0,0 +1,16 @@
{# popular-posts (art-deco) — compact ranked list from the `posts` provider. #}
<section data-block-override="art-deco:popular-posts" class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="deco-caps mb-5 text-xs text-primary">{{ heading }}</h2>{% endif %}
<ol class="space-y-4">
{% for post in posts %}
<li class="deco-rule flex items-start gap-3 pt-4">
{% if showRank %}<span class="mt-0.5 text-lg font-bold tabular-nums text-primary" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{{ forloop.Counter }}</span>{% endif %}
{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="deco-step 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="deco-caps text-sm leading-snug text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)"><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,23 @@
{# post-hero (art-deco) — current post from the `post` provider. When the post #}
{# has a featured image it becomes an overlaid banner; otherwise a clean text hero. #}
{% set center = align != "left" %}
{% if post.featured_image_url %}
<section data-block-override="art-deco: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/70"></div><div class="deco-grain absolute inset-0"></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 %}<div class="mb-4 flex items-center justify-center gap-4"><span class="deco-rule w-12"></span><span class="deco-rule w-12"></span></div>{% endif %}
<h1 class="deco-caps text-3xl leading-tight text-white sm:text-4xl lg:text-5xl" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, 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, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, 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="deco-caps 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="art-deco:post-hero" class="bg-background 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 %}<div class="mb-4 flex items-center justify-center gap-4"><span class="deco-rule w-12"></span><span class="deco-rule w-12"></span></div>{% endif %}
<h1 class="deco-caps deco-underline text-3xl leading-tight sm:text-4xl lg:text-5xl text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{{ 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, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, 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="deco-caps text-primary">{{ 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 (art-deco) — author/date/reading-time row from the `post` provider. #}
{% if post %}
<div data-block-override="art-deco: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="h-7 w-7 rounded-full object-cover ring-1 ring-[hsl(var(--primary)/0.5)]" %}{% endif %}<span class="deco-caps text-primary">{{ 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="deco-caps rounded-none border border-[hsl(var(--primary)/0.35)] 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,25 @@
<section data-block-override="art-deco:pricing" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="deco-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ 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, 'Cormorant Garamond', 'Cormorant', Georgia, 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 p-6 {% if tier.highlighted %}deco-frame bg-card text-card-foreground deco-foil{% else %}rounded-none border border-[hsl(var(--primary)/0.35)] bg-card text-card-foreground{% endif %}">
{% if tier.badge %}<span class="deco-caps absolute -top-3 left-1/2 -translate-x-1/2 border border-primary bg-primary px-3 py-1 text-xs text-primary-foreground">{{ tier.badge }}</span>{% endif %}
<h3 class="deco-caps text-center text-lg text-foreground" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ tier.name }}</h3>
<div class="mx-auto my-3 flex items-center justify-center"><span class="deco-rule w-12"></span></div>
<p class="flex items-baseline justify-center gap-1">
<span class="text-4xl font-bold tracking-tight text-primary" style="font-family:var(--font-heading)">{% 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, 'Cormorant Garamond', 'Cormorant', Georgia, 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="deco-pill mt-8 inline-flex w-full items-center justify-center">{{ tier.ctaLabel }}</a>{% else %}<a href="{{ tier.ctaUrl }}" class="deco-link mt-8 inline-flex w-full items-center justify-center">{{ 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 — supper-club press pull-quote: large italic Cormorant flanked by gold sunburst crests. Fields mirror builtin: quote, style, align, cite, attribution, role. #}
{% with style=style|default:"block" align=align|default:"left" %}
<figure data-block-override="art-deco:quote" class="bn-quote my-10 {% if style == "inline-card" %}deco-frame bg-card p-8 md:p-10{% elif style == "pull" %}border-l-2 border-[hsl(var(--primary))] pl-6 md:pl-8 py-2{% else %}px-2 py-1{% endif %}{% if align == "center" %} text-center{% endif %}{% if class %} {{ class }}{% endif %}">
{% if align == "center" %}<div class="mb-5 h-6 deco-scallop" aria-hidden="true"></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, 'Cormorant Garamond', 'Cormorant', Georgia, 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="deco-caps text-xs" 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="deco-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 (art-deco) — grid from the `posts` provider. #}
{% if posts %}
<section data-block-override="art-deco:related-posts" class="deco-rule-thick py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<h2 class="deco-caps deco-underline mb-10 text-center text-2xl md:text-3xl text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{{ heading|default:"Related posts" }}</h2>
<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="deco-step 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="deco-caps text-base leading-snug text-foreground" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)"><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,13 @@
<section data-block-override="art-deco: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-4"><span class="deco-rule w-12"></span><p class="deco-caps text-xs text-primary">{{ eyebrow }}</p></div>{% endif %}
{% if heading %}<h2 class="deco-caps deco-underline mt-2 text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ 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, 'Cormorant Garamond', 'Cormorant', Georgia, serif)">{{ body|safe }}</div>{% endif %}
{% if ctaUrl and ctaLabel %}<a href="{{ ctaUrl }}" class="deco-pill mt-6 inline-flex items-center gap-2">{{ 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="deco-step 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 Cormorant editorial column with gilded links. Field: html (mirrors builtin). #}
{% if html %}<article data-block-override="art-deco:rich-text" class="rich-text prose prose-lg max-w-none [&_a]:text-primary [&_a]:no-underline [&_a]:border-b [&_a:hover]:text-accent [&_h1]:deco-caps [&_h2]:deco-caps [&_h3]:deco-caps [&_blockquote]:border-l-2 [&_blockquote]:border-[hsl(var(--primary))] [&_blockquote]:italic" style="font-family:var(--font-body, 'Cormorant Garamond', 'Cormorant', Georgia, serif);color:hsl(var(--foreground));font-size:1.1rem;line-height:1.75;">{{ html|safe }}</article>{% endif %}

View File

@ -0,0 +1,15 @@
{# sidebar-nav (art-deco) — vertical menu from the `menus` provider. Variants #}
{# default | compact. highlight-current carried as a progressive-enhancement #}
{# script keyed on data-bn-sidebar-link (marks the anchor matching location.pathname). #}
{% set compact = template == "compact" %}
<nav data-block-override="art-deco: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-[hsl(var(--primary)/0.35)]{% endif %}{% if class %} {{ class }}{% endif %}">
{% if title %}<h3 class="deco-caps deco-underline {% if compact %}px-2 mb-4 text-xs text-primary{% else %}px-3 mb-5 text-sm text-foreground{% endif %}" style="font-family:var(--font-heading, &quot;Cinzel&quot;, &quot;Italiana&quot;, Georgia, serif)">{{ 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="deco-caps flex items-center rounded-none text-sm text-muted-foreground transition-colors hover:text-primary hover:bg-accent {% 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-primary" 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,13 @@
<div data-block-override="art-deco:social-links" class="social-links{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="deco-caps mb-4 text-xs tracking-[0.18em] text-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="deco-foil inline-flex items-center gap-2 rounded-none border border-[hsl(var(--primary)/0.45)] text-foreground transition-colors hover:bg-primary/10 hover:text-primary {% if style == "stack" %}w-full px-4 py-2.5{% else %}p-2.5{% endif %}">
{% if link.network %}<svg class="h-5 w-5 text-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="deco-caps text-xs tracking-[0.14em]">{{ link.label }}</span>{% endif %}
</a>
</li>
{% endif %}{% endfor %}
</ul>
</div>

View File

@ -0,0 +1,48 @@
<section data-block-override="art-deco:stats" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}"{% if countUp %} data-stats{% endif %}>
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="deco-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ 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, 'Cormorant Garamond', 'Cormorant', Georgia, serif)">{{ intro }}</p>{% endif %}
<dl class="mt-12 grid grid-cols-2 gap-8 divide-x divide-[hsl(var(--primary)/0.25)] {% 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 font-bold tracking-tight text-primary md:text-5xl" style="font-family:var(--font-heading)">{% 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="deco-caps mt-2 text-xs text-muted-foreground">{{ 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,21 @@
<section data-block-override="art-deco:team" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="deco-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ 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, 'Cormorant Garamond', 'Cormorant', Georgia, 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" %}deco-frame flex items-start gap-5 bg-card p-5 text-card-foreground{% else %}text-center{% endif %}">
{% if member.photo %}<span class="{% if layout == "list" %}h-20 w-20{% else %}mx-auto h-28 w-28{% endif %} block shrink-0 overflow-hidden border border-[hsl(var(--primary)/0.4)] 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="deco-caps text-lg text-foreground" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ member.name }}</h3>{% endif %}
{% if member.role %}<p class="deco-caps text-xs text-primary">{{ member.role }}</p>{% endif %}
{% if member.bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Cormorant Garamond', 'Cormorant', Georgia, 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,27 @@
<section data-block-override="art-deco:testimonial" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="deco-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ 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="deco-frame flex flex-col bg-card p-6 text-card-foreground{% 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-primary{% 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-primary{% 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-primary{% 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-primary{% 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-primary{% 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, 'Cormorant Garamond', 'Cormorant', Georgia, serif)">{% if q.quote %}&ldquo;{{ q.quote }}&rdquo;{% endif %}</blockquote>
<div class="mx-auto my-4 flex items-center justify-center"><span class="deco-rule w-16"></span></div>
<figcaption class="flex items-center justify-center gap-3">
{% if q.avatar %}<span class="h-10 w-10 shrink-0 overflow-hidden border border-[hsl(var(--primary)/0.4)] 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="deco-caps block text-sm text-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 @@
<div data-block-override="art-deco:text" data-deco-dropcap="true" class="deco-dropcap prose max-w-none {{ class }}" style="font-family: var(--font-body, &quot;Cormorant Garamond&quot;, &quot;Cormorant&quot;, Georgia, serif); color: hsl(var(--foreground));">{{ text|safe }}</div>
{# text override — Cormorant body prose with gilded links. Fields: text, proseClass (mirrors builtin). Dropcap is reserved for the article template, not forced on every text block. #}
<div data-block-override="art-deco:text" class="prose max-w-none flex-1 [&_a]:text-primary [&_a]:no-underline [&_a]:border-b [&_a]:border-[hsl(var(--primary))] [&_a:hover]:text-accent {{ proseClass }}" style="font-family:var(--font-body, 'Cormorant Garamond', 'Cormorant', Georgia, serif);color:hsl(var(--foreground));font-size:1.075rem;line-height:1.75;">{{ text|safe }}</div>

View File

@ -0,0 +1,21 @@
<section data-block-override="art-deco:timeline" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4">
{% if heading %}<h2 class="deco-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ 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, 'Cormorant Garamond', 'Cormorant', Georgia, 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-[hsl(var(--primary)/0.4)]{% endif %}">
{% for step in steps %}
<li class="{% if orientation == "horizontal" %}text-center{% else %}relative pl-14{% endif %}">
<span class="deco-caps flex h-8 w-8 items-center justify-center border border-primary bg-primary text-sm text-primary-foreground {% if orientation == "horizontal" %}mx-auto{% else %}absolute left-0 top-0 ring-4 ring-background{% endif %}">
{% if marker == "numbered" %}{{ forloop.counter }}{% elif marker == "dot" %}<span class="h-2 w-2 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="deco-caps text-xs text-primary">{{ step.label }}</p>{% endif %}
{% if step.title %}<h3 class="deco-caps mt-1 text-lg text-foreground" style="font-family:var(--font-heading, 'Cinzel', 'Italiana', Georgia, serif)">{{ step.title }}</h3>{% endif %}
{% if step.text %}<p class="mt-1 text-muted-foreground" style="font-family:var(--font-body, 'Cormorant Garamond', 'Cormorant', Georgia, serif)">{{ step.text }}</p>{% endif %}
{% if step.media %}<div class="deco-step mt-3 overflow-hidden">{% 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 — thin lacquer contact strip, gold text. Fields mirror builtin: variant, showOnMobile, leftItems/rightItems {icon,label,link}. #}
{% set ubcls = "bg-foreground text-background" %}
{% 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="deco-caps 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="deco-caps 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="art-deco:utility-bar" class="w-full text-[0.7rem] {% if not showOnMobile %}hidden sm:block{% endif %} {{ ubcls }} {{ class }}">
<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 — gold-framed facade; play button in a gold-ringed medallion. Fields + facade JS mirror builtin verbatim: aspect, url, media, poster, title, caption. #}
{% with aspect=aspect|default:"16:9" %}<figure data-block-override="art-deco:video-embed" class="bn-video my-8{% if class %} {{ class }}{% endif %}"><div class="deco-step"><div class="bn-video-frame 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="deco-foil relative inline-flex h-16 w-16 items-center justify-center rounded-full bg-background/90 text-primary transition-transform group-hover:scale-105 motion-reduce:transform-none">::lucide:play:xl::</span></button></div></div>{% if caption %}<figcaption class="mt-3 text-center text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Cormorant Garamond', Georgia, 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 %}