Compare commits

..

No commits in common. "50f58205f0302a95eaee695a22e4ff0ccb2e33dd" and "979516b5e938a3970b3ee1be5e5003a51ff44be7" have entirely different histories.

96 changed files with 335 additions and 2151 deletions

View File

@ -1,25 +0,0 @@
# Brutalist
A loud, honest theme built on chunky black borders, candy-bright accents, and hard offset shadows that snap on hover. Oversized grotesque headings sit on a strict grid, with structure left exposed on purpose. It has the sticker energy of a creative studio that wants its work to shout. The look is playful, and it stays legible.
## Good for
We built Brutalist for studios and makers who want a confident, unmissable site. Treat these as starting points, not limits. If the mood fits your brand, use it:
- Design and creative studios
- Indie software and product launches
- Architecture and interior firms
- Art galleries and zine shops
- Portfolios that want to stand out
## What you get
- Four color presets, each with a tuned light mode and a tuned dark mode: Concrete & Cadmium, Hazard, Riso, and Acid.
- Space Grotesk and Archivo Black for headings, Inter for body, and IBM Plex Mono for details, all bundled. Fonts stay admin-controlled, so you can swap them from the typography settings.
- Four persona blocks: a project ledger for indexing work, a sticker wall of pinned badges, a changelog for release notes, and a spec sheet for hard facts.
- A demo site that installs when you activate the theme: a home page, an about page, a short blog, and a contact page whose form emails your admins.
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
## Install
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully built starting point.

View File

@ -1,32 +1,33 @@
# Recommended fonts — Brutalist
# Recommended Fonts — Brutalist
Fonts are admin-controlled. Assign them in Admin → Appearance → Typography; the
theme consumes them only via `var(--font-heading|body|mono, <fallback>)`, never a
hardcoded family.
`fonts.json` ships empty (`[]`) per the wave-1 policy in
`/home/alex/src/blockninja/themes/docs/FONTS.md`. No woff2 files are bundled in
this build pass; the theme uses CSS variable fallback stacks until an admin
assigns fonts in the typography panel.
## Bundled (ship with this theme, SIL OFL 1.1)
All three roles below are available in the Google Fonts tab of the typography
picker. Open the typography panel, pick from the **Google Fonts** tab, and
assign:
These latin-subset woff2 files are bundled in `assets/fonts/` and appear in the
picker as `source=template` rows (see `assets/fonts/LICENSES.md`):
| Role | Source | Family | Notes |
|---|---|---|---|
| Heading | `google:Space Grotesk` | Space Grotesk | Display / wordmark face. The whole point of the theme is that it lives at 200pt+. Bold (700) is what the templates default to. |
| Body | `google:Inter` | Inter | 1618px body copy with generous tracking. Weights 400 and 600. (Inter Tight is an acceptable swap if you prefer tighter setting.) |
| Mono | `google:JetBrains Mono` | JetBrains Mono | 11px mono captions, figure numbers, timestamps, metadata. Weights 400 and 700. |
| Role | Family | Weights | Notes |
|------|--------|---------|-------|
| Heading | **Space Grotesk** | 500, 700 | The signature display face — oversized, uppercase. |
| Display (alt) | **Archivo Black** | 400 | Heavier alternative for masthead-scale headlines. |
| Body | **Inter** | 400, 500, 700 | Neutral, high-legibility body copy. |
| Mono | **IBM Plex Mono** | 400, 500 | Meta labels, captions, buttons, tags. |
## CSS fallback chain (already in the theme)
## Suggested assignment
Until those assignments land, every `font-family` declaration in the theme
flows through CSS custom properties with the following fallback stacks:
- **Heading:** Space Grotesk (700). Alternative: Archivo Black for maximum weight.
- **Body:** Inter (400).
- **Mono:** IBM Plex Mono (400). Alternative from the Google tab: JetBrains Mono.
```css
var(--font-heading, "Space Grotesk", "Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif)
var(--font-body, "Inter", "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif)
var(--font-mono, "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)
```
## Fallback stacks (used when no admin font is set)
## Wave-2 follow-up (out of scope)
- Heading: `"Space Grotesk", "Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif`
- Body: `"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif`
- Mono: `"JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`
All four bundled families are SIL Open Font License 1.1. Swap freely from the
Google Fonts tab; the theme only references the CSS variables.
The wider design system roadmap calls for shipping a custom display face for
Brutalist. When that lands, replace this doc with a `LICENSES.md` and add the
woff2 family to `fonts.json` per the schema in `docs/FONTS.md`.

View File

@ -1,21 +0,0 @@
# Recommended icons — Brutalist
This theme renders icons through the sprite pipeline
(`<svg><use href="/icons/<pack>.svg#<name>"></use></svg>`). Two packs are used by
the theme's block overrides and are declared in `plugin.mod`
(`required_icon_packs = ["lucide", "simple-icons"]`).
Until the loader auto-installs declared packs, install them manually before
activation:
1. Open `/admin/icons`.
2. Install **Lucide** (bundled — usually already present). Used for UI chrome:
`menu`, `x`, `chevron-down`, `chevron-left`, `chevron-right`, `arrow-right`,
`play`, `download`, `check`, `star`, `navigation`, `map-pin`, `flag`, `globe`,
`asterisk`.
3. Install **Simple Icons** (bundled). Used by the author-bio-hero social row:
`x`, `linkedin`, `github`.
`lucide` is the safe default for any icon-picker field. Utility-bar and footer
social icons render whatever `pack:name` string the admin assigns, so they work
with any installed pack.

View File

@ -1,16 +0,0 @@
# Bundled font licenses
All bundled woff2 files in this directory are latin-subset builds redistributed
under the SIL Open Font License 1.1 (OFL). No modifications were made beyond
subsetting to the latin range.
| Family | Files | License | Source |
|--------|-------|---------|--------|
| Space Grotesk | space-grotesk-500.woff2, space-grotesk-700.woff2 | SIL OFL 1.1 | Florian Karsten / Google Fonts |
| Archivo Black | archivo-black-400.woff2 | SIL OFL 1.1 | Omnibus-Type / Google Fonts |
| Inter | inter-400.woff2, inter-500.woff2, inter-700.woff2 | SIL OFL 1.1 | Rasmus Andersson / Google Fonts |
| IBM Plex Mono | ibm-plex-mono-400.woff2, ibm-plex-mono-500.woff2 | SIL OFL 1.1 | IBM / Google Fonts |
Full OFL text: https://openfontlicense.org/. Font-family is still admin-controlled;
these bundled families appear as `source=template` rows in the fonts picker and
are consumed only via `var(--font-heading|body|mono, <fallback>)`.

Binary file not shown.

Binary file not shown.

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,31 +1,48 @@
# Brutalist theme persona block definitions (codeless .bnp, WO-WZ-021 / WO-TF-012).
# Keys are fully qualified (brutalist:<key>) because the codeless loader registers
# definitions verbatim. Blocks that duplicated builtins (masthead, concrete_hero,
# meta_strip, caption_image, pull_quote, colophon) were removed — they are now
# template overrides of navbar/hero/utility-bar/image/quote/footer. Only true
# persona furniture remains here.
# Brutalist theme block definitions (codeless .bnp, WO-WZ-021).
# Keys are fully qualified (brutalist:<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.
blocks:
- key: brutalist:masthead
title: Studio Masthead
description: Oversized studio wordmark with mono index counter top-right
category: layout
schema: masthead.schema.json
template: masthead.ninjatpl
- key: brutalist:project_ledger
title: Project Ledger
description: "Ordered project index on a 12-col grid: number / year / client / role / arrow"
description: "Ordered project list on a 12-col grid: number / year / client / role / arrow"
category: content
schema: project_ledger.schema.json
template: project_ledger.ninjatpl
- key: brutalist:sticker_wall
title: Sticker Wall
description: "Wall of hard-bordered candy stickers — labels, disciplines, or tags with offset shadows"
- key: brutalist:concrete_hero
title: Concrete Hero
description: 200pt+ display headline with optional eyebrow and full-bleed image
category: layout
schema: concrete_hero.schema.json
template: concrete_hero.ninjatpl
- key: brutalist:meta_strip
title: Metadata Strip
description: "Mono uppercase strip: CLIENT / YEAR / DISCIPLINE / LOCATION"
category: content
schema: sticker_wall.schema.json
template: sticker_wall.ninjatpl
- key: brutalist:changelog
title: Changelog
description: "Versioned changelog rows — version tag, date, and note per release"
schema: meta_strip.schema.json
template: meta_strip.ninjatpl
- key: brutalist:caption_image
title: Captioned Image
description: Image with 11px mono caption in the left gutter
category: content
schema: changelog.schema.json
template: changelog.ninjatpl
- key: brutalist:spec_sheet
title: Spec Sheet
description: "Mono label/value spec table with hairline rows"
schema: caption_image.schema.json
template: caption_image.ninjatpl
- key: brutalist:pull_quote
title: Pull Quote
description: Massive quote spanning 8 columns with hairline rules above and below
category: content
schema: spec_sheet.schema.json
template: spec_sheet.ninjatpl
schema: pull_quote.schema.json
template: pull_quote.ninjatpl
- key: brutalist:colophon
title: Colophon Footer
description: Three-row mono footer with copyright stamp
category: layout
schema: colophon.schema.json
template: colophon.ninjatpl

View File

@ -0,0 +1 @@
<figure class="brutalist-figure" data-block="brutalist:caption_image" style="margin: 2rem 0;">{% if image %}<img src="{{ image }}" alt="{{ caption }}" style="display: block; width: 100%; height: auto;">{% endif %}{% if caption or figureNumber %}<figcaption>{% if figureNumber %}<span style="margin-right: 0.5rem;">{{ figureNumber }}</span>{% endif %}{% if caption %}<span>{{ caption }}</span>{% endif %}</figcaption>{% endif %}</figure>

View File

@ -0,0 +1,26 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Captioned Image",
"description": "Image with 11px mono caption in the left gutter",
"type": "object",
"properties": {
"image": {
"type": "string",
"title": "Image",
"description": "Figure image",
"x-editor": "media"
},
"caption": {
"type": "string",
"title": "Caption",
"description": "Mono uppercase caption text",
"x-editor": "text"
},
"figureNumber": {
"type": "string",
"title": "Figure Number",
"description": "e.g. Fig. 01",
"x-editor": "text"
}
}
}

View File

@ -1,8 +0,0 @@
<section class="bru-section bg-background text-foreground" data-block="brutalist:changelog">
<div class="bru-wrap-mid">
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
<div>
{% for entry in entries %}<div class="bru-changelog-row"><div><span class="bru-tag">{{ entry.version }}</span>{% if entry.date %}<div class="bru-meta mt-2">{{ entry.date }}</div>{% endif %}</div><div class="text-foreground">{% if entry.title %}<h3 class="bru-kicker" style="font-size:1.15rem;text-transform:none">{{ entry.title }}</h3>{% endif %}{% if entry.note %}<div class="mt-1 text-muted-foreground">{{ entry.note|safe }}</div>{% endif %}</div></div>{% endfor %}
</div>
</div>
</section>

View File

@ -1,51 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Changelog",
"description": "Versioned changelog rows",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"description": "Optional section heading",
"x-editor": "text"
},
"entries": {
"type": "array",
"title": "Entries",
"description": "Changelog entries, newest first",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"version": {
"type": "string",
"title": "Version",
"description": "Version tag e.g. v1.2",
"x-editor": "text"
},
"date": {
"type": "string",
"title": "Date",
"description": "Release date",
"x-editor": "text"
},
"title": {
"type": "string",
"title": "Title",
"description": "Optional short title for the release",
"x-editor": "text"
},
"note": {
"type": "string",
"title": "Note",
"description": "What changed",
"x-editor": "richtext"
}
},
"required": ["version"]
}
}
}
}

1
blocks/colophon.ninjatpl Normal file
View File

@ -0,0 +1 @@
<section class="brutalist-colophon" data-block="brutalist:colophon">{% if address and showAddress != "false" and showAddress != false %}<address style="font-style: normal; white-space: pre-line;">{{ address }}</address>{% endif %}<div style="display: flex; flex-direction: column; gap: 0.25rem;">{% if email %}<a href="mailto:{{ email }}" style="color: inherit; text-decoration: none;">{{ email }}</a>{% endif %}{% for link in social %}<a href="{{ link.url }}" target="_blank" rel="noopener" style="color: inherit; text-decoration: none;">{{ link.label }}</a>{% endfor %}</div><div class="stamp">© {% now "2006" %}</div></section>

View File

@ -0,0 +1,53 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Colophon Footer",
"description": "Three-row mono footer with address, email, and social links",
"type": "object",
"properties": {
"address": {
"type": "string",
"title": "Address",
"description": "Studio postal address",
"x-editor": "textarea"
},
"email": {
"type": "string",
"title": "Contact Email",
"description": "Contact email rendered in mono",
"x-editor": "text"
},
"showAddress": {
"type": "string",
"title": "Show Address",
"description": "Toggle the address row on or off",
"x-editor": "select",
"enum": ["true", "false"],
"default": "true"
},
"social": {
"type": "array",
"title": "Social Links",
"description": "Optional list of social links",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"title": "Label",
"description": "Display label",
"x-editor": "text"
},
"url": {
"type": "string",
"title": "URL",
"description": "Target URL",
"x-editor": "link"
}
},
"required": ["label", "url"]
}
}
}
}

View File

@ -0,0 +1 @@
<section class="brutalist-hero" data-block="brutalist:concrete_hero" data-has-media="{% if media %}true{% else %}false{% endif %}">{% if media %}<div class="media-bg" style="background-image: url(&amp;#39;{{ media }}&amp;#39;);" role="presentation"></div>{% endif %}<div class="inner">{% if eyebrow %}<p class="eyebrow">{{ eyebrow }}</p>{% endif %}<h1 class="headline">{{ headline }}</h1></div></section>

View File

@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Concrete Hero",
"description": "200pt+ display headline with optional eyebrow and full-bleed image",
"type": "object",
"properties": {
"headline": {
"type": "string",
"title": "Headline",
"description": "Oversized display headline",
"x-editor": "text"
},
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Mono eyebrow line shown above the headline",
"x-editor": "text"
},
"media": {
"type": "string",
"title": "Background Media",
"description": "Optional full-bleed background image",
"x-editor": "media"
}
},
"required": ["headline"]
}

5
blocks/masthead.ninjatpl Normal file
View File

@ -0,0 +1,5 @@
<section class="brutalist-masthead" data-block="brutalist:masthead">
<span class="index-counter brutalist-mono">{{ indexNumber|default:"01 / 14" }}</span>
<h1 class="studio-name">{{ studioName|default:"STUDIO" }}</h1>
{% if tagline %}<p class="tagline">{{ tagline }}</p>{% endif %}
</section>

View File

@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Studio Masthead",
"description": "Oversized studio wordmark with mono index counter top-right",
"type": "object",
"properties": {
"studioName": {
"type": "string",
"title": "Studio Name",
"description": "Wordmark text rendered at display size",
"x-editor": "text",
"default": "STUDIO"
},
"tagline": {
"type": "string",
"title": "Tagline",
"description": "Short tagline rendered below the wordmark",
"x-editor": "text"
},
"indexNumber": {
"type": "string",
"title": "Index Counter",
"description": "Mono index counter top-right, e.g. 01 / 14",
"x-editor": "text",
"default": "01 / 14"
}
},
"required": ["studioName"]
}

View File

@ -0,0 +1 @@
<dl class="brutalist-meta-strip" data-block="brutalist:meta_strip">{% for item in items %}<div style="display: flex; flex-direction: column; gap: 0.25rem;"><dt>{{ item.label }}</dt><dd>{{ item.value }}</dd></div>{% endfor %}</dl>

View File

@ -1,19 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Spec Sheet",
"description": "Mono label/value spec table with hairline rows",
"title": "Metadata Strip",
"description": "Mono uppercase strip: CLIENT / YEAR / DISCIPLINE / LOCATION",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"description": "Optional section heading",
"x-editor": "text"
},
"rows": {
"items": {
"type": "array",
"title": "Rows",
"description": "Spec label/value pairs in display order",
"title": "Items",
"description": "Label/value pairs displayed inline",
"default": [],
"x-editor": "collection",
"items": {
@ -22,13 +16,13 @@
"label": {
"type": "string",
"title": "Label",
"description": "Spec label",
"description": "Short uppercase label",
"x-editor": "text"
},
"value": {
"type": "string",
"title": "Value",
"description": "Spec value",
"description": "Value text shown beside the label",
"x-editor": "text"
}
},

View File

@ -1,6 +1,3 @@
<section class="bru-section bg-background text-foreground" data-block="brutalist:project_ledger">
<div class="bru-wrap">
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
<section data-block="brutalist:project_ledger">
{% if not rows %}<p class="brutalist-mono" style="padding: 1rem 0; color: hsl(var(--muted-foreground));">No projects listed.</p>{% else %}<ol style="list-style: none; padding: 0; margin: 0;">{% for row in rows %}<li class="brutalist-ledger-row"><span class="ledger-no">{{ row.no }}</span><span class="ledger-year">{{ row.year }}</span><span class="ledger-client">{% if row.link %}<a href="{{ row.link }}" style="color: inherit; text-decoration: none;">{{ row.client }}</a>{% else %}{{ row.client }}{% endif %}</span><span class="ledger-role">{{ row.role }}</span><span class="ledger-arrow">{% if row.link %}<a href="{{ row.link }}" aria-label="View project" style="color: inherit; text-decoration: none;">→</a>{% endif %}</span></li>{% endfor %}</ol>{% endif %}
</div>
</section>

View File

@ -4,12 +4,6 @@
"description": "Ordered project list on a 12-col grid: number, year, client, role, link",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"description": "Optional section heading shown above the ledger",
"x-editor": "text"
},
"rows": {
"type": "array",
"title": "Rows",

View File

@ -0,0 +1 @@
<section class="brutalist-pullquote brutalist-grid-12 brutalist-col-span-8" data-block="brutalist:pull_quote"><blockquote>{{ quote|safe }}</blockquote>{% if attribution %}<cite>— {{ attribution }}</cite>{% endif %}</section>

View File

@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Pull Quote",
"description": "Massive quote spanning 8 columns with hairline rules above and below",
"type": "object",
"properties": {
"quote": {
"type": "string",
"title": "Quote",
"description": "The quoted text",
"x-editor": "richtext"
},
"attribution": {
"type": "string",
"title": "Attribution",
"description": "Person or source the quote is attributed to",
"x-editor": "text"
}
},
"required": ["quote"]
}

View File

@ -1,6 +0,0 @@
<section class="bru-section bg-background text-foreground" data-block="brutalist:spec_sheet">
<div class="bru-wrap-mid">
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
<dl>{% for row in rows %}<div class="bru-spec-row"><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div>{% endfor %}</dl>
</div>
</section>

View File

@ -1,8 +0,0 @@
<section class="bru-section bg-background text-foreground" data-block="brutalist:sticker_wall">
<div class="bru-wrap">
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.5rem,4vw,2.75rem)">{{ heading }}</h2>{% endif %}
<div class="flex flex-wrap gap-4">
{% for item in items %}<span class="bru-sticker" style="font-size:{{ item.size|default:"1.1rem" }};{% if item.tone == "ink" %}background:hsl(var(--foreground));color:hsl(var(--background)){% elif item.tone == "paper" %}background:hsl(var(--background));color:hsl(var(--foreground)){% else %}background:hsl(var(--accent));color:hsl(var(--accent-foreground)){% endif %}">{{ item.text }}</span>{% endfor %}
</div>
</div>
</section>

View File

@ -1,47 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Sticker Wall",
"description": "Wall of hard-bordered candy stickers",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"description": "Optional section heading",
"x-editor": "text"
},
"items": {
"type": "array",
"title": "Stickers",
"description": "Sticker labels in display order",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "Label",
"description": "Sticker text",
"x-editor": "text"
},
"tone": {
"type": "string",
"title": "Tone",
"description": "Sticker colour",
"default": "accent",
"enum": ["accent", "ink", "paper"],
"x-editor": "select"
},
"size": {
"type": "string",
"title": "Size",
"description": "Optional font-size (e.g. 1.5rem)",
"x-editor": "text"
}
},
"required": ["text"]
}
}
}
}

View File

@ -1,34 +1 @@
[
{
"name": "Space Grotesk",
"family": "Space Grotesk",
"variants": [
{ "weight": "500", "style": "normal", "file": "fonts/space-grotesk-500.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/space-grotesk-700.woff2" }
]
},
{
"name": "Archivo Black",
"family": "Archivo Black",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/archivo-black-400.woff2" }
]
},
{
"name": "Inter",
"family": "Inter",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/inter-400.woff2" },
{ "weight": "500", "style": "normal", "file": "fonts/inter-500.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/inter-700.woff2" }
]
},
{
"name": "IBM Plex Mono",
"family": "IBM Plex Mono",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/ibm-plex-mono-400.woff2" },
{ "weight": "500", "style": "normal", "file": "fonts/ibm-plex-mono-500.woff2" }
]
}
]
[]

View File

@ -13,96 +13,29 @@ page_templates:
- system: brutalist
key: default
title: Default
description: "Standard masthead + hard-ruled body + colophon footer"
description: "Standard masthead + 12-col body + footer"
slots: [header, main, footer]
- system: brutalist
key: landing
title: Index Sheet
description: "Oversized headline + project ledger"
slots: [hero, ledger, footer]
- system: brutalist
key: article
title: Case Study
description: "Long-form project page with metadata strip"
slots: [header, meta, main, footer]
- system: brutalist
key: full-width
title: Full Bleed
description: "Edge-to-edge gallery layout"
slots: [header, main, footer]
- system: brutalist
key: landing
title: Index Sheet
description: "Oversized display headline + stacked sections"
slots: [header, hero, main, cta, footer]
- system: brutalist
key: article
title: Case Study
description: "Long-form project page with post hero and byline"
slots: [header, post_hero, main, author, related, footer]
- system: brutalist
key: blog-index
title: Index Ledger
description: "Blog listing on a hard grid with featured slab"
slots: [header, featured, main, footer]
- system: brutalist
key: contact
title: Contact Sheet
description: "Contact form with optional details aside"
slots: [header, main, aside, footer]
- system: brutalist
key: auth
title: Access
description: "Centered bordered auth card"
slots: [main, footer]
template_overrides:
# Chrome (4)
- { template: brutalist, block: navbar }
- { template: brutalist, block: footer }
- { template: brutalist, block: utility-bar }
- { template: brutalist, block: announcement-bar }
# Marketing (12)
- { template: brutalist, block: hero }
- { template: brutalist, block: feature-grid }
- { template: brutalist, block: rich-section }
- { template: brutalist, block: stats }
- { template: brutalist, block: testimonial }
- { template: brutalist, block: logo-strip }
- { template: brutalist, block: pricing }
- { template: brutalist, block: team }
- { template: brutalist, block: faq }
- { template: brutalist, block: timeline }
- { template: brutalist, block: cta }
- { template: brutalist, block: countdown }
# Primitives (11)
- { template: brutalist, block: heading }
- { template: brutalist, block: text }
- { template: brutalist, block: rich-text }
- { template: brutalist, block: image }
- { template: brutalist, block: quote }
- { template: brutalist, block: divider }
- { template: brutalist, block: button }
- { template: brutalist, block: card }
- { template: brutalist, block: gallery }
- { template: brutalist, block: video-embed }
- { template: brutalist, block: audio-embed }
# Commerce / local (6)
- { template: brutalist, block: menu-list }
- { template: brutalist, block: event-list }
- { template: brutalist, block: contact-form }
- { template: brutalist, block: contact-card }
- { template: brutalist, block: hours-location }
- { template: brutalist, block: social-links }
# Blog family (12)
- { template: brutalist, block: blog-hero }
- { template: brutalist, block: featured-posts }
- { template: brutalist, block: related-posts }
- { template: brutalist, block: popular-posts }
- { template: brutalist, block: category-list }
- { template: brutalist, block: post-hero }
- { template: brutalist, block: post-metadata }
- { template: brutalist, block: author-bio }
- { template: brutalist, block: author-bio-hero }
- { template: brutalist, block: newsletter-signup }
- { template: brutalist, block: breadcrumbs }
- { template: brutalist, block: sidebar-nav }
# Auth surface (3)
- { template: brutalist, block: auth-form }
- { template: brutalist, block: password-reset-form }
- { template: brutalist, block: auth-status }
# System (1)
- { template: brutalist, block: page-suggestions }
- { template: brutalist, block: image }
email_wrappers:
- brutalist
@ -468,299 +401,3 @@ css:
);
}
/* ============================================================
BRUTALIST OVERRIDE KIT (Wave A) — semantic classes the block
overrides apply. Raw CSS (not Tailwind @apply) so it ships in
the codeless manifest regardless of JIT scanning. All colour
via hsl(var(--token)); all type via var(--font-*).
============================================================ */
.bru-section { padding: 2.75rem 0; }
@media (min-width: 768px) { .bru-section { padding: 4rem 0; } }
.bru-wrap { width: 100%; max-width: 80rem; margin: 0 auto; padding: 0 1.5rem; }
.bru-wrap-mid { width: 100%; max-width: 64rem; margin: 0 auto; padding: 0 1.5rem; }
.bru-wrap-narrow { width: 100%; max-width: 44rem; margin: 0 auto; padding: 0 1.5rem; }
/* Slab = the core bordered block. Shadow = the hard offset. */
.bru-slab {
border: 3px solid hsl(var(--border));
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
}
.bru-slab-bg {
border: 3px solid hsl(var(--border));
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
.bru-shadow { box-shadow: 6px 6px 0 0 hsl(var(--border)); }
.bru-shadow-sm { box-shadow: 4px 4px 0 0 hsl(var(--border)); }
.bru-shadow-accent { box-shadow: 6px 6px 0 0 hsl(var(--accent)); }
/* Hover snap: the block nudges into its own shadow. */
.bru-hover { transition: transform 90ms linear, box-shadow 90ms linear; }
@media (prefers-reduced-motion: no-preference) {
.bru-hover:hover { transform: translate(3px, 3px); box-shadow: 3px 3px 0 0 hsl(var(--border)); }
.bru-hover.bru-shadow-sm:hover { transform: translate(2px, 2px); box-shadow: 2px 2px 0 0 hsl(var(--border)); }
}
/* Type utilities */
.bru-eyebrow {
display: inline-block;
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.6875rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
}
.bru-kicker {
font-family: var(--font-heading, "Space Grotesk", "Inter Tight", sans-serif);
font-weight: 700;
text-transform: uppercase;
letter-spacing: -0.015em;
line-height: 1;
color: hsl(var(--foreground));
}
.bru-meta {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.6875rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: hsl(var(--muted-foreground));
}
/* Sticker tag — candy accent chip with a hard border. */
.bru-tag {
display: inline-flex;
align-items: center;
gap: 0.35rem;
border: 2px solid hsl(var(--border));
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
padding: 0.15rem 0.55rem;
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.65rem;
letter-spacing: 0.1em;
text-transform: uppercase;
line-height: 1.4;
box-shadow: 2px 2px 0 0 hsl(var(--border));
}
.bru-tag-ghost {
background-color: transparent;
color: hsl(var(--foreground));
box-shadow: none;
}
/* Buttons */
.bru-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.7rem 1.4rem;
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
border: 3px solid hsl(var(--border));
text-decoration: none;
cursor: pointer;
box-shadow: 4px 4px 0 0 hsl(var(--border));
transition: transform 90ms linear, box-shadow 90ms linear;
}
.bru-btn-solid { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.bru-btn-ghost { background-color: hsl(var(--background)); color: hsl(var(--foreground)); }
@media (prefers-reduced-motion: no-preference) {
.bru-btn:hover { transform: translate(2px, 2px); box-shadow: 2px 2px 0 0 hsl(var(--border)); }
}
.bru-btn:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
/* Hard rules */
.bru-rule { border: 0; border-top: 3px solid hsl(var(--border)); margin: 0; }
.bru-rule-thin { border: 0; border-top: 1px solid hsl(var(--border)); margin: 0; }
/* Inputs (block-level, complements the page-wide input reset above) */
.bru-field {
width: 100%;
border: 3px solid hsl(var(--input));
background-color: hsl(var(--background));
color: hsl(var(--foreground));
padding: 0.65rem 0.85rem;
font-family: var(--font-body, "Inter", sans-serif);
font-size: 0.95rem;
}
.bru-field:focus { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
.bru-label {
display: block;
margin-bottom: 0.4rem;
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: hsl(var(--foreground));
}
/* Numbered index cell (ledgers, ranks, steps) */
.bru-index {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-variant-numeric: tabular-nums;
color: hsl(var(--accent));
font-weight: 700;
}
/* Sticker wall / changelog / spec-sheet persona furniture */
.bru-sticker {
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
border: 3px solid hsl(var(--border));
padding: 1rem 1.25rem;
font-family: var(--font-heading, "Space Grotesk", sans-serif);
font-weight: 700;
text-transform: uppercase;
letter-spacing: -0.01em;
line-height: 1;
box-shadow: 4px 4px 0 0 hsl(var(--border));
}
.bru-changelog-row {
display: grid;
grid-template-columns: 8rem 1fr;
gap: 1.5rem;
padding: 1.5rem 0;
border-top: 1px solid hsl(var(--border));
}
.bru-changelog-row:last-child { border-bottom: 1px solid hsl(var(--border)); }
@media (max-width: 640px) {
.bru-changelog-row { grid-template-columns: 1fr; gap: 0.5rem; }
}
.bru-spec-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
padding: 0.85rem 0;
border-top: 1px solid hsl(var(--border));
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
font-size: 0.8rem;
letter-spacing: 0.04em;
}
.bru-spec-row:last-child { border-bottom: 1px solid hsl(var(--border)); }
.bru-spec-row dt { text-transform: uppercase; color: hsl(var(--muted-foreground)); }
.bru-spec-row dd { margin: 0; text-align: right; color: hsl(var(--foreground)); }
/* ============================================================
LAYOUT GUARANTEE LAYER — the host's public CSS is a render-driven
JIT built with `@import "tailwindcss" source(none)`. A codeless
theme installs to data/plugins, which is NOT in any @source glob,
so its .ninjatpl utility classes only reach the compiled CSS if a
built-in template emits the exact string or the class is in the
host safelist. These families are NOT safelisted and were silently
dropping on a fresh provision (collapsed grids, zero padding,
touching items): grid-cols-{1,2,4,5}, every responsive grid
variant, col-span-*, the p-* shorthand, gap-{3..12}/gap-x/gap-y,
space-y-*, columns-*, aspect-*, object-*, w/h-full. Define them
here (input_css_append is compiled UNCONDITIONALLY) so every block
lays out deterministically on desktop AND collapses on mobile.
Scoped to .brutalist-page; structural only — no colours. Values
mirror Tailwind's defaults so any host-compiled copy is identical.
============================================================ */
.brutalist-page .grid { display: grid; }
.brutalist-page .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.brutalist-page .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.brutalist-page .grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.brutalist-page .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.brutalist-page .grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.brutalist-page .col-span-2 { grid-column: span 2 / span 2; }
.brutalist-page .col-span-3 { grid-column: span 3 / span 3; }
.brutalist-page .columns-2 { columns: 2; }
/* Padding shorthand (px-*/py-* are safelisted; p-* is not). */
.brutalist-page .p-0 { padding: 0; }
.brutalist-page .p-1 { padding: 0.25rem; }
.brutalist-page .p-2 { padding: 0.5rem; }
.brutalist-page .p-3 { padding: 0.75rem; }
.brutalist-page .p-4 { padding: 1rem; }
.brutalist-page .p-5 { padding: 1.25rem; }
.brutalist-page .p-6 { padding: 1.5rem; }
.brutalist-page .p-8 { padding: 2rem; }
.brutalist-page .p-16 { padding: 4rem; }
/* Gaps (only gap-1/1.5/2 are safelisted). */
.brutalist-page .gap-0 { gap: 0; }
.brutalist-page .gap-3 { gap: 0.75rem; }
.brutalist-page .gap-4 { gap: 1rem; }
.brutalist-page .gap-5 { gap: 1.25rem; }
.brutalist-page .gap-6 { gap: 1.5rem; }
.brutalist-page .gap-8 { gap: 2rem; }
.brutalist-page .gap-10 { gap: 2.5rem; }
.brutalist-page .gap-12 { gap: 3rem; }
.brutalist-page .gap-x-2 { column-gap: 0.5rem; }
.brutalist-page .gap-x-3 { column-gap: 0.75rem; }
.brutalist-page .gap-x-4 { column-gap: 1rem; }
.brutalist-page .gap-x-10 { column-gap: 2.5rem; }
.brutalist-page .gap-y-1 { row-gap: 0.25rem; }
.brutalist-page .gap-y-2 { row-gap: 0.5rem; }
.brutalist-page .gap-y-6 { row-gap: 1.5rem; }
/* Vertical rhythm (space-y-*). */
.brutalist-page .space-y-0 > * + * { margin-top: 0; }
.brutalist-page .space-y-1 > * + * { margin-top: 0.25rem; }
.brutalist-page .space-y-2 > * + * { margin-top: 0.5rem; }
.brutalist-page .space-y-3 > * + * { margin-top: 0.75rem; }
.brutalist-page .space-y-4 > * + * { margin-top: 1rem; }
.brutalist-page .space-y-5 > * + * { margin-top: 1.25rem; }
.brutalist-page .space-y-6 > * + * { margin-top: 1.5rem; }
.brutalist-page .space-y-8 > * + * { margin-top: 2rem; }
.brutalist-page .space-y-10 > * + * { margin-top: 2.5rem; }
.brutalist-page .space-y-12 > * + * { margin-top: 3rem; }
/* Sizing used for media fill + logos. */
.brutalist-page .w-full { width: 100%; }
.brutalist-page .h-full { height: 100%; }
.brutalist-page .w-auto { width: auto; }
.brutalist-page .h-auto { height: auto; }
/* Media boxes: a fixed aspect ratio so ABSENT/empty media can never
balloon into a tall empty box — it stays a compact bordered tile. */
.brutalist-page .aspect-video { aspect-ratio: 16 / 9; }
.brutalist-page .aspect-square { aspect-ratio: 1 / 1; }
.brutalist-page .aspect-\[3\/4\] { aspect-ratio: 3 / 4; }
.brutalist-page .aspect-\[4\/3\] { aspect-ratio: 4 / 3; }
.brutalist-page .aspect-\[21\/9\] { aspect-ratio: 21 / 9; }
.brutalist-page .object-cover { object-fit: cover; }
.brutalist-page .object-contain { object-fit: contain; }
.brutalist-page .object-fill { object-fit: fill; }
.brutalist-page img { max-width: 100%; }
@media (min-width: 640px) {
.brutalist-page .sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.brutalist-page .sm\:col-span-1 { grid-column: span 1 / span 1; }
.brutalist-page .sm\:col-span-2 { grid-column: span 2 / span 2; }
.brutalist-page .sm\:gap-6 { gap: 1.5rem; }
}
@media (min-width: 768px) {
.brutalist-page .md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.brutalist-page .md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.brutalist-page .md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.brutalist-page .md\:col-span-3 { grid-column: span 3 / span 3; }
.brutalist-page .md\:columns-2 { columns: 2; }
.brutalist-page .md\:columns-3 { columns: 3; }
.brutalist-page .md\:columns-4 { columns: 4; }
.brutalist-page .md\:gap-16 { gap: 4rem; }
.brutalist-page .md\:p-6 { padding: 1.5rem; }
.brutalist-page .md\:p-8 { padding: 2rem; }
.brutalist-page .md\:p-14 { padding: 3.5rem; }
.brutalist-page .md\:px-10 { padding-left: 2.5rem; padding-right: 2.5rem; }
.brutalist-page .md\:py-14 { padding-top: 3.5rem; padding-bottom: 3.5rem; }
.brutalist-page .md\:py-16 { padding-top: 4rem; padding-bottom: 4rem; }
}
@media (min-width: 1024px) {
.brutalist-page .lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.brutalist-page .lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.brutalist-page .lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.brutalist-page .lg\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.brutalist-page .lg\:col-span-1 { grid-column: span 1 / span 1; }
.brutalist-page .lg\:col-span-2 { grid-column: span 2 / span 2; }
.brutalist-page .lg\:col-span-3 { grid-column: span 3 / span 3; }
}

View File

@ -1,32 +1,33 @@
[
{
"key": "brutalist:default-master",
"title": "Brutalist Default",
"page_templates": ["default", "full-width", "contact", "blog-index", "article"],
"title": "Brutalist Default Master",
"page_templates": ["default", "article"],
"blocks": [
{ "block_key": "navbar", "title": "Studio Nav", "content": { "menuName": "main", "companyName": "HARDSET", "ctaLabel": "Start a project", "ctaUrl": "/contact" }, "slot": "header", "sort_order": 0 },
{ "block_key": "navbar", "title": "Masthead Nav", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 },
{ "block_key": "brutalist:masthead", "title": "Studio Masthead", "content": { "studioName": "STUDIO" }, "slot": "header", "sort_order": 1 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "columns", "companyName": "HARDSET", "tagline": "A creative studio for people who ship." }, "slot": "footer", "sort_order": 0 }
{ "block_key": "brutalist:colophon", "title": "Colophon Footer", "content": { "showAddress": true }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "brutalist:landing-master",
"title": "Brutalist Index Sheet",
"key": "brutalist:index-master",
"title": "Brutalist Index Master",
"page_templates": ["landing"],
"blocks": [
{ "block_key": "navbar", "title": "Studio Nav", "content": { "menuName": "main", "companyName": "HARDSET", "ctaLabel": "Start a project", "ctaUrl": "/contact" }, "slot": "header", "sort_order": 0 },
{ "block_key": "hero", "title": "Display Hero", "content": { "template": "left-aligned", "eyebrow": "Creative studio", "headline": "WE MAKE LOUD THINGS", "subheadline": "Brand, web, and product work with a hard edge." }, "slot": "hero", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "cta", "title": "Closing CTA", "content": { "background": "band", "heading": "Have a project in mind?", "text": "Tell us what you are building.", "primaryUrl": "/contact", "primaryLabel": "Start a project" }, "slot": "cta", "sort_order": 0 },
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "columns", "companyName": "HARDSET", "tagline": "A creative studio for people who ship." }, "slot": "footer", "sort_order": 0 }
{ "block_key": "brutalist:masthead", "title": "Studio Masthead", "content": { "studioName": "STUDIO" }, "slot": "hero", "sort_order": 0 },
{ "block_key": "slot", "title": "Ledger Slot", "content": { "slotName": "ledger" }, "slot": "ledger", "sort_order": 0 },
{ "block_key": "brutalist:colophon", "title": "Colophon Footer", "content": { "showAddress": true }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "brutalist:auth-master",
"title": "Brutalist Access",
"page_templates": ["auth"],
"key": "brutalist:fullbleed-master",
"title": "Brutalist Full Bleed Master",
"page_templates": ["full-width"],
"blocks": [
{ "block_key": "slot", "title": "Form", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }
{ "block_key": "navbar", "title": "Masthead Nav", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Full Bleed Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "brutalist:colophon", "title": "Colophon Footer", "content": { "showAddress": false }, "slot": "footer", "sort_order": 0 }
]
}
]

View File

@ -2,8 +2,8 @@
name = "brutalist"
display_name = "Brutalist"
scope = "@themes"
version = "0.2.1"
version = "0.2.0"
description = "Concrete, oversized type, hard 12-col grid theme for design studios, architecture firms and art galleries."
kind = "theme"
categories = ["templates"]
tags = ["brutalist", "neo-brutalist", "studio", "agency", "bold", "mono", "editorial", "candy"]
tags = ["brutalist", "raw", "mono", "agency", "architecture", "gallery", "editorial", "minimal"]

View File

@ -2,7 +2,7 @@
{
"id": "concrete-red",
"name": "Concrete & Cadmium",
"description": "Paper-white concrete with a cadmium red accent. Black rules in light, paper rules in wet-concrete dark.",
"description": "Off-white concrete with cadmium red accent. Light mode primary; dark mode for case-study deep-dives.",
"theme": {
"mode": "both",
"lightColors": {
@ -33,28 +33,28 @@
"cardForeground": "40 14% 93%",
"popover": "0 0% 9%",
"popoverForeground": "40 14% 93%",
"primary": "4 86% 56%",
"primary": "4 86% 54%",
"primaryForeground": "0 0% 6%",
"secondary": "0 0% 14%",
"secondaryForeground": "40 14% 93%",
"muted": "0 0% 12%",
"mutedForeground": "40 6% 65%",
"accent": "4 86% 56%",
"accent": "4 86% 54%",
"accentForeground": "0 0% 6%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "40 14% 93%",
"input": "40 14% 93%",
"ring": "4 86% 56%"
"ring": "4 86% 54%"
}
}
},
{
"id": "hazard-yellow",
"name": "Hazard",
"description": "Construction-site safety yellow. Ink type sits on yellow; wet-concrete dark glows.",
"description": "Construction-site safety yellow on ink black. Dark mode primary.",
"theme": {
"mode": "both",
"mode": "dark",
"lightColors": {
"background": "40 14% 93%",
"foreground": "0 0% 4%",
@ -83,119 +83,19 @@
"cardForeground": "48 100% 88%",
"popover": "0 0% 9%",
"popoverForeground": "48 100% 88%",
"primary": "48 100% 52%",
"primary": "48 100% 50%",
"primaryForeground": "0 0% 6%",
"secondary": "0 0% 14%",
"secondaryForeground": "48 100% 88%",
"muted": "0 0% 12%",
"mutedForeground": "48 20% 65%",
"accent": "48 100% 52%",
"accent": "48 100% 50%",
"accentForeground": "0 0% 6%",
"destructive": "0 84% 55%",
"destructiveForeground": "0 0% 100%",
"border": "48 100% 88%",
"input": "48 100% 88%",
"ring": "48 100% 52%"
}
}
},
{
"id": "riso-blue",
"name": "Riso",
"description": "Off-white with a riso-print electric blue. Cool concrete dark with a glowing blue accent.",
"theme": {
"mode": "both",
"lightColors": {
"background": "40 14% 93%",
"foreground": "0 0% 4%",
"card": "40 10% 96%",
"cardForeground": "0 0% 4%",
"popover": "40 10% 96%",
"popoverForeground": "0 0% 4%",
"primary": "222 89% 52%",
"primaryForeground": "0 0% 100%",
"secondary": "40 8% 88%",
"secondaryForeground": "0 0% 4%",
"muted": "40 6% 90%",
"mutedForeground": "0 0% 35%",
"accent": "222 89% 52%",
"accentForeground": "0 0% 100%",
"destructive": "0 84% 50%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 4%",
"input": "0 0% 4%",
"ring": "222 89% 52%"
},
"darkColors": {
"background": "222 20% 7%",
"foreground": "210 30% 92%",
"card": "222 18% 10%",
"cardForeground": "210 30% 92%",
"popover": "222 18% 10%",
"popoverForeground": "210 30% 92%",
"primary": "222 92% 62%",
"primaryForeground": "222 20% 7%",
"secondary": "222 14% 15%",
"secondaryForeground": "210 30% 92%",
"muted": "222 14% 13%",
"mutedForeground": "213 18% 66%",
"accent": "222 92% 62%",
"accentForeground": "222 20% 7%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "210 30% 92%",
"input": "210 30% 92%",
"ring": "222 92% 62%"
}
}
},
{
"id": "acid-lime",
"name": "Acid",
"description": "Off-white with an acid lime accent. Ink type on lime; wet-concrete dark with a lime glow.",
"theme": {
"mode": "both",
"lightColors": {
"background": "40 14% 93%",
"foreground": "0 0% 4%",
"card": "40 10% 96%",
"cardForeground": "0 0% 4%",
"popover": "40 10% 96%",
"popoverForeground": "0 0% 4%",
"primary": "84 78% 40%",
"primaryForeground": "0 0% 4%",
"secondary": "40 8% 88%",
"secondaryForeground": "0 0% 4%",
"muted": "40 6% 90%",
"mutedForeground": "0 0% 35%",
"accent": "84 78% 40%",
"accentForeground": "0 0% 4%",
"destructive": "0 84% 50%",
"destructiveForeground": "0 0% 100%",
"border": "0 0% 4%",
"input": "0 0% 4%",
"ring": "84 78% 40%"
},
"darkColors": {
"background": "90 12% 7%",
"foreground": "80 20% 92%",
"card": "90 10% 10%",
"cardForeground": "80 20% 92%",
"popover": "90 10% 10%",
"popoverForeground": "80 20% 92%",
"primary": "84 82% 52%",
"primaryForeground": "90 12% 7%",
"secondary": "90 8% 15%",
"secondaryForeground": "80 20% 92%",
"muted": "90 8% 13%",
"mutedForeground": "82 15% 66%",
"accent": "84 82% 52%",
"accentForeground": "90 12% 7%",
"destructive": "0 84% 60%",
"destructiveForeground": "0 0% 100%",
"border": "80 20% 92%",
"input": "80 20% 92%",
"ring": "84 82% 52%"
"ring": "48 100% 50%"
}
}
}

View File

@ -1,212 +0,0 @@
{
"demo": true,
"settings": {
"merge": {
"companyName": "HARDSET",
"tagline": "A creative studio for people who ship."
}
},
"data_tables": [
{
"key": "contact_submissions",
"name": "Project Enquiries",
"description": "Seeded contact-form submissions for the HARDSET 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": "project", "label": "Project type", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"pages": [
{
"slug": "/",
"title": "HARDSET",
"template_key": "landing",
"blocks": [
{
"block_key": "brutalist:sticker_wall",
"title": "Disciplines",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "What we do",
"items": [
{ "text": "Brand", "tone": "accent", "size": "1.6rem" },
{ "text": "Web", "tone": "ink", "size": "1.6rem" },
{ "text": "Product", "tone": "paper", "size": "1.6rem" },
{ "text": "Motion", "tone": "accent", "size": "1.6rem" },
{ "text": "Type", "tone": "ink", "size": "1.6rem" },
{ "text": "Print", "tone": "paper", "size": "1.6rem" }
]
}
},
{
"block_key": "feature-grid",
"title": "Services",
"slot": "main",
"sort_order": 2,
"content": {
"eyebrow": "Services",
"heading": "How we work",
"layout": "card",
"columns": 3,
"items": [
{ "icon": "lucide:box", "title": "Brand systems", "text": "Names, marks, and the rules that hold them together." },
{ "icon": "lucide:code", "title": "Websites", "text": "Fast, accessible sites built to last past the launch." },
{ "icon": "lucide:zap", "title": "Product design", "text": "Interfaces that read clearly and ship on schedule." }
]
}
},
{
"block_key": "brutalist:project_ledger",
"title": "Selected work",
"slot": "main",
"sort_order": 3,
"content": {
"rows": [
{ "no": "01", "year": "2024", "client": "Independent record label", "role": "Brand system" },
{ "no": "02", "year": "2024", "client": "Climbing gym chain", "role": "Website" },
{ "no": "03", "year": "2023", "client": "Furniture maker", "role": "Product + web" },
{ "no": "04", "year": "2023", "client": "Community radio station", "role": "Identity" }
]
}
}
]
},
{
"slug": "/about",
"title": "The Studio",
"template_key": "default",
"blocks": [
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "The Studio", "level": 1 } },
{ "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "<p>HARDSET is a small studio. We take on a few projects at a time and give each one the whole team. No account layer, no handoff. You talk to the people doing the work.</p><p>We like clear briefs and hard deadlines. We ship. Then we help you run it.</p>" } },
{
"block_key": "brutalist:spec_sheet",
"title": "Studio facts",
"slot": "main",
"sort_order": 3,
"content": {
"heading": "Studio",
"rows": [
{ "label": "Founded", "value": "2019" },
{ "label": "Team", "value": "Six" },
{ "label": "Based", "value": "Melbourne" },
{ "label": "Focus", "value": "Brand, web, product" }
]
}
},
{
"block_key": "brutalist:changelog",
"title": "Recent",
"slot": "main",
"sort_order": 4,
"content": {
"heading": "Lately",
"entries": [
{ "version": "Now", "date": "This quarter", "title": "Booking two new projects", "note": "<p>We have room for two more brand or web projects this quarter.</p>" },
{ "version": "New", "date": "Last month", "title": "Two more designers", "note": "<p>The studio grew to six. Same way of working, more hands.</p>" }
]
}
},
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 5, "content": { "style": "pull", "quote": "Make it obvious, then make it loud.", "attribution": "HARDSET" } }
]
},
{
"slug": "/blog",
"title": "Journal",
"template_key": "blog-index",
"blocks": [
{ "block_key": "card", "title": "Post: Borders", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "In praise of a hard border", "text": "<p>Why a thick black line still does more work than a soft shadow.</p>", "link": "/blog/in-praise-of-a-hard-border", "linkLabel": "Read" } },
{ "block_key": "card", "title": "Post: Ship", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "Ship the ugly first draft", "text": "<p>How shipping early keeps a project honest.</p>", "link": "/blog/ship-the-ugly-first-draft", "linkLabel": "Read" } },
{ "block_key": "card", "title": "Post: Type", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Set it in one typeface", "text": "<p>A short case for doing more with less type.</p>", "link": "/blog/set-it-in-one-typeface", "linkLabel": "Read" } }
]
},
{
"slug": "/blog/in-praise-of-a-hard-border",
"title": "In praise of a hard border",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "In praise of a hard border", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>A soft shadow asks you to squint. A hard border does not. It draws the edge, holds the block, and tells you where one thing ends and the next begins.</p><p>We reach for the line first. It is honest. It prints. It reads on a bad screen in bright sun. When we want energy we add one loud colour and a hard offset. That is the whole trick.</p>" } }
]
},
{
"slug": "/blog/ship-the-ugly-first-draft",
"title": "Ship the ugly first draft",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Ship the ugly first draft", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The first draft is meant to be wrong. Its job is to exist. Once it is on a real screen, the real problems show up and the fake ones fall away.</p><p>We ship early and often. It keeps the brief honest and the feedback specific. Polish comes last, after the shape is right.</p>" } }
]
},
{
"slug": "/blog/set-it-in-one-typeface",
"title": "Set it in one typeface",
"template_key": "article",
"parent_slug": "/blog",
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Set it in one typeface", "level": 1 } },
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>One family, used well, beats three used at random. Pick a face with range, learn its weights, and let scale do the talking.</p><p>We set most of a project in a single grotesque. Big for headlines, tight for meta, plain for body. The result reads as one voice.</p>" } }
]
},
{
"slug": "/contact",
"title": "Start a project",
"template_key": "contact",
"blocks": [
{
"block_key": "contact-form",
"title": "Project enquiry",
"slot": "main",
"sort_order": 1,
"content": {
"heading": "Start a project",
"description": "Tell us what you are building and when you need it. We reply within two working days.",
"submitLabel": "Send it",
"successMessage": "Thanks. Your enquiry is in. We will reply within two working days.",
"fields": [
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
{ "name": "project", "label": "Project type", "type": "text", "required": false, "width": "full" },
{ "name": "message", "label": "What are you building?", "type": "textarea", "required": true, "width": "full" }
],
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
}
},
{
"block_key": "contact-card",
"title": "Direct",
"slot": "aside",
"sort_order": 1,
"content": {
"heading": "Direct",
"columns": 1,
"channels": [
{ "icon": "lucide:mail", "label": "Email", "value": "studio@hardset.example", "link": "mailto:studio@hardset.example" },
{ "icon": "lucide:map-pin", "label": "Studio", "value": "Melbourne, AU" }
]
}
}
]
},
{
"slug": "/login",
"title": "Sign in",
"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>Clients sign in to review work and approve invoices.</p>" } }
]
}
],
"menu_items": [
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
{ "menu": "main", "label": "Studio", "page_slug": "/about", "sort_order": 2 },
{ "menu": "main", "label": "Journal", "page_slug": "/blog", "sort_order": 3 },
{ "menu": "main", "label": "Contact", "page_slug": "/contact", "sort_order": 4 }
]
}

View File

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

View File

@ -1,22 +1,22 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
<html lang="en">
{{ head_html|safe }}
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="article">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.header|safe }}</div>
</header>
<section class="w-full">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 1rem 1.5rem;">{{ slots.meta|safe }}</div>
</section>
<main class="flex-grow w-full">
<article>
{% if slots.post_hero %}{{ slots.post_hero|safe }}{% else %}<div class="bru-wrap-mid pt-12"><h1 class="bru-kicker" style="font-size:clamp(2rem,5vw,3.5rem)">{{ title }}</h1></div>{% endif %}
<div class="bru-wrap-mid py-10 md:py-14"><div class="prose max-w-none space-y-6">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="bru-meta">This post has no content yet.</p>{% endif %}</div></div>
{% if slots.author or slots.related %}
<div class="bru-wrap-mid pb-14 space-y-10">
{% if slots.author %}<div style="border-top:3px solid hsl(var(--border));padding-top:2.5rem">{{ slots.author|safe }}</div>{% endif %}
{% if slots.related %}<div>{{ slots.related|safe }}</div>{% endif %}
</div>
{% endif %}
<article class="brutalist-shell" style="max-width: 60rem; margin: 0 auto; padding: 2rem 1.5rem 4rem;">
{% if slots.main %}{{ slots.main|safe }}{% endif %}
</article>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
<footer class="w-full mt-auto">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.footer|safe }}</div>
</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,19 +0,0 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="auth">
{{ 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" style="max-width:30rem">
<a href="/" class="block text-center mb-8" aria-label="{{ site_settings.Title }} home">
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt }}" class="h-10 w-auto mx-auto">{% else %}<span class="bru-kicker" style="font-size:2rem">{{ site_settings.Title|default:"STUDIO" }}</span>{% endif %}
</a>
<div class="bru-slab bru-shadow p-6 md:p-8">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="bru-meta">No form assigned to this page.</p>{% endif %}
</div>
{% if slots.footer %}<div class="mt-6 text-center bru-meta">{{ slots.footer|safe }}</div>{% endif %}
</div>
</main>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,17 +0,0 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="blog-index">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="bru-wrap py-12 md:py-16 space-y-12">
{% if title %}<div style="border-bottom:4px solid hsl(var(--border));padding-bottom:1.5rem"><h1 class="bru-kicker" style="font-size:clamp(2.5rem,7vw,5rem)">{{ title }}</h1>{% if page_meta.MetaDescription %}<p class="mt-3 text-lg text-muted-foreground">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
{% if slots.featured %}<section>{{ slots.featured|safe }}</section>{% endif %}
{% if slots.main %}<section>{{ slots.main|safe }}</section>{% else %}<p class="bru-meta py-8">No posts published yet.</p>{% endif %}
</div>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,22 +0,0 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="contact">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<main class="flex-grow w-full">
<div class="bru-wrap py-12 md:py-16">
{% if slots.aside %}
<div class="grid grid-cols-1 gap-12 lg:grid-cols-5">
<div class="lg:col-span-3 space-y-8">{% if slots.main %}{{ slots.main|safe }}{% endif %}</div>
<aside class="lg:col-span-2 space-y-8">{{ slots.aside|safe }}</aside>
</div>
{% else %}
<div class="space-y-8">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="bru-meta py-8">No content blocks assigned to this page.</p>{% endif %}</div>
{% endif %}
</div>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,15 +1,19 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
<html lang="en">
{{ head_html|safe }}
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="default">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.header|safe }}</div>
</header>
<main class="flex-grow w-full">
<div class="bru-wrap py-12 md:py-16">
{% if slots.main %}<div class="space-y-10">{{ slots.main|safe }}</div>{% else %}<p class="bru-meta py-16">No content blocks assigned to this page.</p>{% endif %}
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 2rem 1.5rem 4rem;">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="brutalist-mono" style="color: hsl(var(--muted-foreground)); padding: 4rem 0;">NO CONTENT BLOCKS ASSIGNED TO THIS PAGE.</p>{% endif %}
</div>
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
<footer class="w-full mt-auto">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.footer|safe }}</div>
</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,13 +1,17 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
<html lang="en">
{{ head_html|safe }}
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="full-width">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
<header class="w-full" style="border-bottom: 1px solid hsl(var(--border));">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.header|safe }}</div>
</header>
<main class="flex-grow w-full">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="bru-wrap py-16"><p class="bru-meta">No content blocks assigned to this page.</p></div>{% endif %}
{% if slots.main %}{{ slots.main|safe }}{% endif %}
</main>
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
<footer class="w-full mt-auto">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.footer|safe }}</div>
</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,15 +1,19 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
<html lang="en">
{{ head_html|safe }}
<body class="brutalist-page antialiased min-h-screen flex flex-col" data-brutalist-template="landing">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
{% if slots.hero %}<section class="w-full" style="border-bottom:4px solid hsl(var(--border))">{{ slots.hero|safe }}</section>{% endif %}
<section class="w-full">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.hero|safe }}</div>
</section>
<main class="flex-grow w-full">
{% if slots.main %}<div>{{ slots.main|safe }}</div>{% endif %}
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 2rem 1.5rem;">
{% if slots.ledger %}{{ slots.ledger|safe }}{% endif %}
</div>
</main>
{% if slots.cta %}<section class="w-full" style="border-top:4px solid hsl(var(--border))">{{ slots.cta|safe }}</section>{% endif %}
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
<footer class="w-full mt-auto">
<div class="brutalist-shell" style="max-width: 90rem; margin: 0 auto; padding: 0 1.5rem;">{{ slots.footer|safe }}</div>
</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -7,32 +7,32 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ site_name }}</title>
<style type="text/css">
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
@media only screen and (max-width: 620px) {
.brutalist-email-container { width: 100% !important; max-width: 100% !important; }
.brutalist-email-pad { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
@media only screen and (max-width: 620px) {
.brutalist-email-container { width: 100% !important; max-width: 100% !important; }
.brutalist-email-pad { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style="background-color: {{ colors.background|default:"#F2EFE8" }}; margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color: {{ colors.foreground|default:"#0A0A0A" }};">
<body style="background-color: {{ colors.background|default:"#F2EFE8" }}; margin: 0; padding: 0; font-family: &amp;#39;Inter&amp;#39;, -apple-system, BlinkMacSystemFont, &amp;#39;Segoe UI&amp;#39;, Helvetica, Arial, sans-serif; color: {{ colors.foreground|default:"#0A0A0A" }};">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="padding: 48px 16px; background-color: {{ colors.background|default:"#F2EFE8" }};">
<table role="presentation" class="brutalist-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#F7F4ED" }}; border: 3px solid {{ colors.border|default:"#0A0A0A" }}; border-radius: 0;">
<table role="presentation" class="brutalist-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#F7F4ED" }}; border: 1px solid {{ colors.border|default:"#0A0A0A" }}; border-radius: 0;">
<tr>
<td align="left" class="brutalist-email-pad" style="padding: 32px 40px; border-bottom: 3px solid {{ colors.border|default:"#0A0A0A" }};">
{% if site_name %}<h1 style="margin: 0; font-family: 'Space Grotesk', 'Inter Tight', Helvetica, Arial, sans-serif; font-weight: 700; font-size: 26px; letter-spacing: -0.02em; color: {{ colors.foreground|default:"#0A0A0A" }}; text-transform: uppercase;">{{ site_name }}</h1>{% endif %}
<td align="left" class="brutalist-email-pad" style="padding: 32px 40px; border-bottom: 1px solid {{ colors.border|default:"#0A0A0A" }};">
{% if site_name %}<h1 style="margin: 0; font-family: &amp;#39;Space Grotesk&amp;#39;, &amp;#39;Inter Tight&amp;#39;, Helvetica, Arial, sans-serif; font-weight: 700; font-size: 26px; letter-spacing: -0.02em; color: {{ colors.foreground|default:"#0A0A0A" }}; text-transform: uppercase;">{{ site_name }}</h1>{% endif %}
</td>
</tr>
<tr>
<td class="brutalist-email-pad" style="padding: 40px 48px; color: {{ colors.foreground|default:"#0A0A0A" }}; font-size: 16px; line-height: 1.7;">{{ body|safe }}</td>
</tr>
<tr>
<td class="brutalist-email-pad" style="padding: 24px 48px 32px; border-top: 3px solid {{ colors.border|default:"#0A0A0A" }}; font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: {{ colors.mutedForeground|default:"#595959" }};">
<td class="brutalist-email-pad" style="padding: 24px 48px 32px; border-top: 1px solid {{ colors.border|default:"#0A0A0A" }}; font-family: &amp;#39;JetBrains Mono&amp;#39;, ui-monospace, monospace; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: {{ colors.mutedForeground|default:"#595959" }};">
{% if site_url %}<div style="margin-bottom: 8px;"><a href="{{ site_url }}" style="color: {{ colors.foreground|default:"#0A0A0A" }}; text-decoration: none; border-bottom: 1px solid {{ colors.border|default:"#0A0A0A" }};">{{ site_url }}</a></div>{% endif %}
{% if unsubscribe_url %}<div><a href="{{ unsubscribe_url }}" style="color: {{ colors.foreground|default:"#0A0A0A" }}; text-decoration: none; border-bottom: 1px solid {{ colors.border|default:"#0A0A0A" }};">UNSUBSCRIBE</a></div>{% endif %}
</td>

View File

@ -1,21 +0,0 @@
{# brutalist announcement-bar override — bold bordered strip, dismissable. Fields: message, variant, linkText, linkUrl, dismissable. #}
{% set annstyle = "background:hsl(var(--accent));color:hsl(var(--accent-foreground))" %}
{% if variant == "alert" %}{% set annstyle = "background:hsl(var(--destructive));color:hsl(var(--destructive-foreground))" %}{% elif variant == "info" %}{% set annstyle = "background:hsl(var(--foreground));color:hsl(var(--background))" %}{% endif %}
<div data-bn-announcement data-bn-annkey="{{ variant|default:"promo" }}:{{ message|slugify }}" class="bn-announcement w-full {{ class }}" style="{{ annstyle }};border-bottom:3px solid hsl(var(--border))">
<div class="bru-wrap py-2 relative flex items-center justify-center gap-2">
<p class="bru-meta" style="color:inherit;letter-spacing:0.08em">{{ 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"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#x"></use></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

@ -1,16 +0,0 @@
{# brutalist audio-embed override — bordered player card. Fields: url, media, artwork, title, artist, download. #}
<figure class="bn-audio my-8 bru-slab bru-shadow overflow-hidden{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden bg-muted" style="border:2px solid hsl(var(--border))">{% 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="bru-kicker truncate" style="font-size:1rem;text-transform:none">{{ title }}</p>{% endif %}{% if artist %}<p class="bru-meta truncate">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="bru-btn bru-btn-solid" style="padding:0.4rem 0.9rem;box-shadow:2px 2px 0 0 hsl(var(--border))" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg>Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="bru-meta inline-flex items-center gap-1 text-foreground">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

@ -1,61 +0,0 @@
{# brutalist auth-form override — bordered login/register card. HTMX endpoints + input names + tab panels are load-bearing (public_auth.go); kept exact. Fields: default_tab, show_social, captchaEnabled; context.isPublicLoggedIn, context.publicUsername, context.site.google_oauth_client_id/facebook_app_id. NOTE: the proof-of-work captcha widget is server-generated and cannot be emitted from a codeless template — do NOT enable captchaEnabled on an override-rendered auth-form (see WO limitation). #}
{% if context.isPublicLoggedIn %}
<div class="bru-slab bru-shadow p-6 text-center" style="max-width:28rem;margin:0 auto">
<p class="bru-meta">Logged in as <strong class="text-foreground">{{ context.publicUsername }}</strong></p>
<a href="/account" class="bru-btn bru-btn-ghost mt-4">My Account</a>
</div>
{% else %}
{% set defaultTab = default_tab|default:"login" %}
<style>
.bru-auth-tab{color:hsl(var(--muted-foreground));border-bottom:3px solid transparent;margin-bottom:-3px;background:transparent}
.bru-auth-tab[data-active="1"]{color:hsl(var(--foreground));border-bottom-color:hsl(var(--accent))}
</style>
<div class="auth-form" style="max-width:28rem;margin:0 auto">
<div id="auth-message"></div>
<div class="flex mb-6" style="border-bottom:3px solid hsl(var(--border))">
<button type="button" data-auth-tab="login" class="bru-auth-tab flex-1 py-2.5 bru-meta" data-active="{% if defaultTab == "register" %}0{% else %}1{% endif %}">Login</button>
<button type="button" data-auth-tab="register" class="bru-auth-tab flex-1 py-2.5 bru-meta" data-active="{% if defaultTab == "register" %}1{% else %}0{% endif %}">Register</button>
</div>
<div id="login-panel"{% if defaultTab == "register" %} class="hidden"{% endif %}>
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML" class="space-y-4">
<div><label class="bru-label">Email</label><input type="email" name="email" required class="bru-field"></div>
<div><label class="bru-label">Password</label><input type="password" name="password" required class="bru-field"></div>
{% if captchaEnabled %}<p class="bru-meta text-destructive">Captcha is enabled but cannot render in this theme. Disable it on the auth-form block.</p>{% endif %}
<button type="submit" class="bru-btn bru-btn-solid w-full" hx-disabled-elt="this">Login</button>
<p class="text-center bru-meta"><a href="/forgot-password" class="text-accent underline underline-offset-4">Forgot password?</a></p>
</form>
{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}
<div class="mt-5"><div class="relative my-4"><hr class="bru-rule-thin"><span class="bru-meta" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:hsl(var(--background));padding:0 .6rem">or</span></div>
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="bru-btn bru-btn-ghost w-full mb-2">Continue with Google</a>{% endif %}
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="bru-btn bru-btn-ghost w-full">Continue with Facebook</a>{% endif %}</div>
{% endif %}
</div>
<div id="register-panel"{% if defaultTab != "register" %} class="hidden"{% endif %}>
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML" class="space-y-4">
<div><label class="bru-label">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="bru-field"></div>
<div><label class="bru-label">Email</label><input type="email" name="email" required class="bru-field"></div>
<div><label class="bru-label">Password</label><input type="password" name="password" required minlength="8" class="bru-field"></div>
{% if captchaEnabled %}<p class="bru-meta text-destructive">Captcha is enabled but cannot render in this theme. Disable it on the auth-form block.</p>{% endif %}
<button type="submit" class="bru-btn bru-btn-solid w-full" hx-disabled-elt="this">Create Account</button>
</form>
{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}
<div class="mt-5"><div class="relative my-4"><hr class="bru-rule-thin"><span class="bru-meta" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:hsl(var(--background));padding:0 .6rem">or</span></div>
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="bru-btn bru-btn-ghost w-full mb-2">Continue with Google</a>{% endif %}
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="bru-btn bru-btn-ghost w-full">Continue with Facebook</a>{% endif %}</div>
{% endif %}
</div>
</div>
<script>
(function(){
var tabs=document.querySelectorAll('[data-auth-tab]');
if(!tabs.length)return;
function activate(name){
var lp=document.getElementById('login-panel'),rp=document.getElementById('register-panel');
if(lp)lp.classList.toggle('hidden',name!=='login');
if(rp)rp.classList.toggle('hidden',name!=='register');
tabs.forEach(function(t){t.setAttribute('data-active',t.getAttribute('data-auth-tab')===name?'1':'0');});
}
tabs.forEach(function(t){t.addEventListener('click',function(){activate(t.getAttribute('data-auth-tab'));});});
})();
</script>
{% endif %}

View File

@ -1,33 +0,0 @@
{# brutalist auth-status override — header login state. Logout/resend HTMX endpoints load-bearing. Context: context.isPublicLoggedIn, context.publicDisplayName, context.publicUsername, context.publicEmailVerified. #}
{% if not context.isPublicLoggedIn %}
<div class="auth-status flex items-center gap-2">
<a href="/login" class="bru-meta px-3 py-1.5 text-foreground hover:text-accent transition-colors">Login</a>
<a href="/login?tab=register" class="bru-btn bru-btn-solid" style="padding:0.4rem 0.9rem">Sign Up</a>
</div>
{% else %}
{% set displayName = context.publicDisplayName|default:context.publicUsername %}
{% if not context.publicEmailVerified %}<div class="fixed top-16 inset-x-0 z-40 text-center bru-meta" style="background:hsl(var(--accent));color:hsl(var(--accent-foreground));border-bottom:3px solid hsl(var(--border));padding:0.5rem 1rem">Please verify your email address. <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="underline">Resend verification email</button></div>{% endif %}
<div class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="flex items-center gap-2 bru-meta px-3 py-1.5 text-foreground hover:text-accent transition-colors">
<span class="inline-flex items-center justify-center w-7 h-7 text-xs" style="background:hsl(var(--accent));color:hsl(var(--accent-foreground));border:2px solid hsl(var(--border));font-family:var(--font-heading,sans-serif);font-weight:700">{{ displayName|first }}</span>
<span>{{ displayName }}</span>
<svg class="w-3 h-3" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
</button>
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-2 w-48 bru-slab bru-shadow py-1 z-50">
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground">My Profile</a>
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground">My Reviews</a>
<hr class="my-1 bru-rule-thin">
<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">Logout</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>
{% endif %}

View File

@ -1,21 +0,0 @@
{# brutalist author-bio-hero override — author-page header from the `authors` provider. Fields: showAvatar, author.*, showPostCount, showSocial. #}
{% if author %}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}" style="border-bottom:4px solid hsl(var(--border))">
<div class="bru-wrap-narrow flex flex-col items-center text-center">
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden bg-muted bru-shadow" style="border:3px solid hsl(var(--border))">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<h1 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,2.5rem)">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="bru-meta mt-2 text-muted-foreground">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
{% if author.bio %}<p class="mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
<div class="bru-meta mt-5 flex flex-wrap items-center justify-center gap-4 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-5 flex items-center justify-center gap-3">
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-accent" 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-accent" 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-accent" 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-accent" 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

@ -1,13 +0,0 @@
{# brutalist author-bio override — byline slab sourced from the `post` provider's author_* fields. Fields: heading, showAvatar, post.author_*. #}
{% if post.author_name %}
<aside class="bru-slab bru-shadow p-6{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="bru-meta mb-4 text-muted-foreground">{{ 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 bg-muted" style="border:3px solid hsl(var(--border))">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="bru-kicker text-base" style="text-transform:none">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-accent">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
{% if post.author_bio %}<p class="mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -1,23 +0,0 @@
{# brutalist blog-hero override — blog-index masthead + featured posts from the `posts` provider. Fields: title, subtitle, posts[], showExcerpt. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap">
<div style="max-width:44rem">
{% if title %}<h1 class="bru-kicker" style="font-size:clamp(2rem,5vw,3.5rem)">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
</div>
{% if posts %}
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
{% for post in posts %}
<article class="group bru-slab bru-shadow bru-hover flex flex-col overflow-hidden{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch{% endif %}">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="block overflow-hidden bg-muted{% if forloop.First %} md:w-1/2{% endif %}" style="border-bottom:3px solid hsl(var(--border))">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-col justify-center gap-2 p-5 md:p-6{% if forloop.First %} md:w-1/2{% endif %}">
<h2 class="bru-kicker {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} leading-snug" style="text-transform:none"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h2>
{% if showExcerpt and post.excerpt %}<p class="text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
<div class="bru-meta mt-2 flex flex-wrap items-center gap-x-3">{% if post.author_name %}<span class="text-foreground">{{ 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

@ -1,13 +0,0 @@
{# brutalist breadcrumbs override — trail from the `breadcrumbs` provider (page ancestry). Mono uppercase trail; keeps the arrows-template chevron verbatim. Fields: template, showHome, homeLabel, breadcrumbs[], separator, showCurrent, item.*. #}
{% set arrows = template == "arrows" %}
<nav aria-label="Breadcrumb" class="py-3{% if class %} {{ class }}{% endif %}">
<ol class="bru-meta flex flex-wrap items-center gap-1">
{% if showHome %}<li class="flex items-center"><a href="/" class="text-muted-foreground transition-colors hover:text-accent">{{ 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-4 w-4 text-muted-foreground" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"></use></svg>{% else %}<span class="mx-2 text-muted-foreground">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
{% if item.is_current %}{% if showCurrent %}<span class="text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-muted-foreground transition-colors hover:text-accent">{{ item.label }}</a>{% endif %}
</li>
{% endfor %}
</ol>
</nav>

View File

@ -1,2 +1 @@
{# brutalist button override — square, hard border, offset shadow, hover snap. Fields: label, link, style, size, icon, iconPosition, align, newTab. #}
{% with align=align|default:"left" %}<div class="flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}"><a href="{{ link|default:"#" }}"{% if newTab %} target="_blank" rel="noopener noreferrer"{% endif %} class="bru-btn {% if style == "secondary" or style == "outline" or style == "ghost" or style == "link" %}bru-btn-ghost{% else %}bru-btn-solid{% endif %}{% if size == "sm" %} text-[0.7rem] px-3 py-2{% elif size == "lg" %} text-sm px-6 py-3.5{% endif %}">{% if icon and iconPosition != "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</a></div>{% endwith %}
<a href="{% if href %}{{ href }}{% elif url %}{{ url }}{% else %}#{% endif %}" class="brutalist-btn" data-brutalist-btn style="display: inline-flex; align-items: center; padding: 0.75rem 1.5rem; font-family: var(--font-mono, 'JetBrains Mono', ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; background-color: transparent; color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); border-radius: 0; text-decoration: none;">{{ text }}</a>

View File

@ -1,2 +0,0 @@
{# brutalist card override — slab with hard offset shadow + hover snap. Fields: title, text, media, link, linkLabel, layout. #}
{% with layout=layout|default:"vertical" %}<article class="bru-slab bru-shadow bru-hover overflow-hidden {% 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 %}" style="border-bottom:3px solid hsl(var(--border))">{% 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="bru-kicker" style="font-size:1.15rem;text-transform:none">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed text-muted-foreground">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="bru-meta mt-2 inline-flex items-center gap-1 text-accent">{{ linkLabel }} →</span>{% endif %}</div>{% if link %}</a>{% endif %}</article>{% endwith %}

View File

@ -1,18 +0,0 @@
{# brutalist category-list override — from the `categories` provider. list or pills. Fields: heading, style, categories[], showCount. #}
{% set pills = style == "pills" %}
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="bru-meta mb-4 text-foreground">{{ heading }}</h2>{% endif %}
{% if pills %}
<div class="flex flex-wrap gap-2">
{% for cat in categories %}
<a href="{{ cat.url }}" class="bru-tag inline-flex items-center gap-1.5">{{ cat.name }}{% if showCount %}<span class="bru-index" style="font-size:0.85rem">{{ cat.post_count }}</span>{% endif %}</a>
{% endfor %}
</div>
{% else %}
<ul class="space-y-2">
{% for cat in categories %}
<li><a href="{{ cat.url }}" class="bru-slab bru-shadow-sm bru-hover flex items-center justify-between px-3 py-2 text-sm text-foreground transition-colors hover:text-accent"><span>{{ cat.name }}</span>{% if showCount %}<span class="bru-index" style="font-size:0.85rem">{{ cat.post_count }}</span>{% endif %}</a></li>
{% endfor %}
</ul>
{% endif %}
</section>

View File

@ -1,18 +0,0 @@
{# brutalist contact-card override — bordered channel slabs, accent icon square. Fields: heading, description, columns, channels[] (icon, label, value, link). Keeps builtin icon split + literal conditional column classes. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-mid">
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if description %}<p class="mt-3 text-lg text-muted-foreground" style="max-width:44rem">{{ description }}</p>{% endif %}
<div class="mt-10 grid grid-cols-1 gap-4 {% 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="bru-slab bru-shadow-sm bru-hover flex items-start gap-4 p-5">
{% if channel.icon %}<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center bg-accent text-accent-foreground" style="border:3px solid hsl(var(--border))"><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="bru-meta text-muted-foreground">{{ channel.label }}</div>{% endif %}
{% if channel.value %}<div class="mt-0.5 break-words bru-kicker" style="font-size:1rem;text-transform:none">{% if channel.link %}<a href="{{ channel.link }}" class="transition-colors hover:text-accent">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -1,40 +0,0 @@
{# brutalist contact-form override — bordered form, mono labels. Submit + honeypot wiring is load-bearing; kept verbatim. Fields: heading, description, successMessage, submitLabel, fields[.name,.label,.type,.required,.placeholder,.width,.options], plus `form` context. #}
{% with bid=_block_id|default:context.blockId %}
<section id="contact-form-{{ bid }}" class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-narrow">
{% if form.submitted %}
<div class="bru-slab bru-shadow p-8 text-center">
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center text-accent-foreground" style="background:hsl(var(--accent));border:3px solid hsl(var(--border))"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg></div>
<p class="bru-kicker" style="font-size:1.2rem;text-transform:none">{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}</p>
</div>
{% else %}
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if description %}<p class="mt-3 text-lg text-muted-foreground">{{ description }}</p>{% endif %}
{% if form.errors._form %}<div class="mt-6 px-4 py-3 text-sm" style="border:3px solid hsl(var(--destructive));background:hsl(var(--destructive)/0.1);color:hsl(var(--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="bru-label">{{ field.label }}{% if field.required %} <span class="text-accent">*</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="bru-field"></textarea>
{% elif field.type == "select" %}
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="bru-field">
{% 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="bru-field">
{% 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">
<button type="submit" class="bru-btn bru-btn-solid">{{ submitLabel|default:"Send message" }}</button>
</div>
{% endform %}
{% endwith %}
{% endif %}
</div>
</section>
{% endwith %}

View File

@ -1,49 +0,0 @@
{# brutalist countdown override — mono digit slabs, timer script + data hooks kept verbatim. Fields: heading, intro, target, variant, labelDays/Hours/Minutes/Seconds, expiredMessage, expiredUrl, expiredLabel. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-mid text-center">
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 text-lg text-muted-foreground" style="max-width:44rem">{{ 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" %}min-w-[4.5rem] bru-slab bru-shadow-sm px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono)" data-unit="days">--</span><span class="bru-meta mt-2 text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] bru-slab bru-shadow-sm px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono)" data-unit="hours">--</span><span class="bru-meta mt-2 text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] bru-slab bru-shadow-sm px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono)" data-unit="minutes">--</span><span class="bru-meta mt-2 text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] bru-slab bru-shadow-sm px-3 py-4{% endif %}"><span class="text-4xl font-bold tabular-nums sm:text-5xl" style="font-family:var(--font-mono)" data-unit="seconds">--</span><span class="bru-meta mt-2 text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
</div>
<div class="mt-8 hidden" data-countdown-expired>
<p class="bru-kicker" style="font-size:1.35rem">{{ expiredMessage|default:"This has started." }}</p>
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="bru-btn bru-btn-solid mt-4">{{ expiredLabel }} →</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

@ -1,13 +0,0 @@
{# brutalist cta override — bordered slab with hard offset. Fields: heading, text, primaryUrl, primaryLabel, secondaryUrl, secondaryLabel, background. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap">
<div class="bru-slab bru-shadow p-8 md:p-14 text-center"{% if background == "band" %} style="background:hsl(var(--accent));color:hsl(var(--accent-foreground))"{% endif %}>
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem);{% if background == "band" %}color:hsl(var(--accent-foreground)){% endif %}">{{ heading }}</h2>{% endif %}
{% if text %}<p class="mt-4 text-lg mx-auto" style="max-width:44rem;{% if background == "band" %}color:hsl(var(--accent-foreground));opacity:0.85{% else %}color:hsl(var(--muted-foreground)){% endif %}">{{ text }}</p>{% endif %}
{% if (primaryUrl and primaryLabel) or (secondaryUrl and secondaryLabel) %}<div class="mt-8 flex flex-wrap items-center justify-center gap-4">
{% if primaryUrl and primaryLabel %}<a href="{{ primaryUrl }}" class="bru-btn {% if background == "band" %}bru-btn-ghost{% else %}bru-btn-solid{% endif %}">{{ primaryLabel }}</a>{% endif %}
{% if secondaryUrl and secondaryLabel %}<a href="{{ secondaryUrl }}" class="bru-btn bru-btn-ghost">{{ secondaryLabel }}</a>{% endif %}
</div>{% endif %}
</div>
</div>
</section>

View File

@ -1,2 +0,0 @@
{# brutalist divider override — hard rule or icon ornament. Fields: variant, weight, width, icon. #}
{% with variant=variant|default:"line" weight=weight|default:"thick" width=width|default:"full" %}{% if variant == "ornament" %}<div class="mx-auto my-10 flex items-center gap-4 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><span class="flex-1" style="border-top:3px solid hsl(var(--border))"></span><svg class="h-5 w-5 text-accent" aria-hidden="true"><use href="/icons/{{ icon|default:"lucide:asterisk"|split:":"|first }}.svg#{{ icon|default:"lucide:asterisk"|split:":"|last }}"></use></svg><span class="flex-1" style="border-top:3px solid hsl(var(--border))"></span></div>{% else %}<hr class="mx-auto my-10 {% if weight == "thin" %}bru-rule-thin{% else %}bru-rule{% endif %} {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator">{% endif %}{% endwith %}

View File

@ -1,41 +0,0 @@
{# brutalist event-list override — bordered event slabs (cards) or hard-ruled rows (list). Fields: heading, intro, layout, hidePast, events[] (past, media, datetime, start, end, timezone, title, venue, description, link, linkLabel). Keeps builtin hidePast guards. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap">
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-3 text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
{% if layout == "cards" %}
<div class="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{% for event in events %}{% if not (hidePast and event.past) %}
<article class="bru-slab bru-shadow bru-hover flex flex-col overflow-hidden{% if event.past %} opacity-60{% endif %}">
{% if event.media %}<div class="aspect-video overflow-hidden bg-muted" style="border-bottom:3px solid hsl(var(--border))">{% 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-5">
<div class="bru-meta text-accent"><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="bru-kicker" style="font-size:1.15rem;text-transform:none">{{ event.title }}</h3>
{% if event.venue %}<p class="text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
{% if event.link %}<a href="{{ event.link }}" class="bru-meta mt-auto inline-flex items-center gap-1 pt-2 text-accent">{{ event.linkLabel|default:"Details" }} →</a>{% endif %}
</div>
</article>
{% endif %}{% endfor %}
</div>
{% else %}
<ul class="mt-10" style="border-top:3px solid hsl(var(--border));border-bottom:3px solid hsl(var(--border))">
{% for event in events %}{% if not (hidePast and event.past) %}
<li class="flex flex-col gap-3 py-5 sm:flex-row sm:items-center sm:gap-6{% if event.past %} opacity-60{% endif %}" style="border-bottom:1px solid hsl(var(--border))">
<div class="shrink-0 sm:w-52">
<div class="bru-meta text-foreground"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
{% if event.end %}<div class="bru-meta text-muted-foreground">to {{ event.end }}</div>{% endif %}
{% if event.timezone %}<div class="bru-meta text-muted-foreground">{{ event.timezone }}</div>{% endif %}
</div>
<div class="min-w-0 flex-1">
<h3 class="bru-kicker" style="font-size:1.05rem;text-transform:none">{{ event.title }}</h3>
{% if event.venue %}<p class="text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="mt-1 text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
</div>
{% if event.link %}<a href="{{ event.link }}" class="bru-btn bru-btn-ghost shrink-0" style="padding:0.4rem 0.9rem">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
</li>
{% endif %}{% endfor %}
</ul>
{% endif %}
</div>
</section>

View File

@ -1,18 +0,0 @@
{# brutalist faq override — bordered accordion, native <details> mechanism kept. Fields: heading, intro, variant, items[] (question, answer). #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-narrow">
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
<div class="mt-10 space-y-4">
{% for item in items %}
<details class="group bru-slab bru-shadow-sm px-5"{% if variant == "open-list" %} open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between gap-4 py-4 text-lg font-medium marker:content-none [&::-webkit-details-marker]:hidden">
<span class="bru-kicker" style="font-size:1.05rem;text-transform:none">{{ item.question }}</span>
<svg class="h-5 w-5 shrink-0 text-accent 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-accent [&_a]:underline">{{ item.answer|safe }}</div>
</details>
{% endfor %}
</div>
</div>
</section>

View File

@ -1,18 +0,0 @@
{# brutalist feature-grid override — bordered slabs with hard offset shadow. Fields: eyebrow, heading, intro, columns, layout, items[] (icon, title, text, linkUrl, linkLabel). Keeps builtin icon split + literal conditional column classes. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap">
{% if eyebrow %}<span class="bru-tag mb-4">{{ eyebrow }}</span>{% endif %}
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-3 text-lg text-muted-foreground" style="max-width:44rem">{{ 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="bru-slab bru-shadow bru-hover flex flex-col gap-2 p-6{% if layout == "centered" %} text-center items-center{% endif %}">
{% if item.icon %}<div class="mb-2 inline-flex h-12 w-12 items-center justify-center bg-accent text-accent-foreground" style="border:3px solid hsl(var(--border))"><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="bru-kicker" style="font-size:1.15rem;text-transform:none">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" class="transition-colors hover:text-accent">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>
{% if item.text %}<p class="text-muted-foreground">{{ item.text }}</p>{% endif %}
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="bru-meta mt-2 inline-flex items-center gap-1 text-accent">{{ item.linkLabel }} →</a>{% endif %}
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -1,22 +0,0 @@
{# brutalist featured-posts override — grid of bordered slab cards from the `posts` provider. Fields: heading, columns, posts[], showFeaturedImage, showExcerpt, showAuthor, showDate. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap">
{% if heading %}<h2 class="bru-kicker mb-8" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
<div class="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 post in posts %}
<article class="group bru-slab bru-shadow bru-hover flex flex-col overflow-hidden">
{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-muted" style="border-bottom:3px solid hsl(var(--border))">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-1 flex-col p-5">
<h3 class="bru-kicker text-lg leading-snug" style="text-transform:none"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
{% if showExcerpt and post.excerpt %}<p class="mt-2 flex-1 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
<div class="bru-meta mt-4 flex flex-wrap items-center gap-x-3 gap-y-1">
{% if showAuthor and post.author_name %}<span class="text-foreground">{{ 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

@ -1,28 +0,0 @@
{# brutalist footer override — colophon. Thick top rule, mono uppercase columns, copyright stamp. Fields: layout, social_items[], showNewsletter, newsletter*, columns[], legal_items[], copyright, companyName, logo, tagline, address, email, phone, fullWidth, backgroundColor. #}
{% set fl = layout|default:"columns" %}
{% macro sociallinks(cls) %}{% if social_items %}<div class="flex items-center gap-3 {{ cls }}">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="text-muted-foreground hover:text-accent transition-colors"><span class="sr-only">{{ s.label }}</span>{% if s.icon %}::{{ s.icon }}::{% else %}{{ s.label }}{% endif %}</a>{% endfor %}</div>{% endif %}{% endmacro %}
{% macro newsletter() %}{% if showNewsletter %}<div class="max-w-sm"><h3 class="bru-meta text-foreground">{{ newsletterTitle|default:"Join the list" }}</h3>{% if newsletterDescription %}<p class="mt-1 text-sm text-muted-foreground">{{ newsletterDescription }}</p>{% endif %}<div class="mt-3">{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div></div>{% endif %}{% endmacro %}
{% macro legalbar() %}<div class="mt-10 pt-6 flex flex-col sm:flex-row items-center justify-between gap-3 bru-meta" style="border-top:1px solid hsl(var(--border))"><p>&copy; {% now "2006" %} {% if copyright %}{{ copyright }}{% else %}{{ companyName }}{% endif %}</p>{% if legal_items %}<nav class="flex flex-wrap items-center gap-x-4 gap-y-1">{% for l in legal_items %}<a href="{{ l.url|default:"#" }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="hover:text-accent transition-colors">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
<footer class="w-full bg-background text-foreground {{ class }}" style="border-top:4px solid hsl(var(--border))">
<div class="{% if fullWidth %}w-full px-6{% else %}bru-wrap{% 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="bru-kicker" style="font-size:1.3rem">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="bru-meta hidden sm:inline">{{ tagline }}</span>{% endif %}</div>
{{ sociallinks("") }}
</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="bru-kicker" style="font-size:1.6rem">{{ companyName }}</span>{% endif %}
{% if tagline %}<p class="mt-3 max-w-xs text-sm text-muted-foreground">{{ tagline }}</p>{% endif %}
{% if address or email or phone %}<address class="mt-4 not-italic bru-meta space-y-1">{% if address %}<div style="white-space:pre-line">{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="hover:text-accent">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="hover:text-accent">{{ phone }}</a></div>{% endif %}</address>{% endif %}
{{ sociallinks("mt-5") }}
</div>
{% for col in columns %}<div><h3 class="bru-meta text-foreground">{{ 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-muted-foreground hover:text-accent 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

@ -1,52 +0,0 @@
{# brutalist gallery override — hard-bordered tiles + dependency-free lightbox. Fields: images[.src,.alt,.caption], layout, columns, gap, lightbox. #}
{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}<div 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="group relative block overflow-hidden bru-slab bru-hover bru-shadow-sm focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring {% 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:2px solid hsl(var(--foreground));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:3px solid hsl(var(--background));'+(reduce?'':'transition:opacity .2s ease;')+'"><figcaption data-lb-cap style="color:hsl(var(--background));opacity:.85;font-family:var(--font-mono,ui-monospace,monospace);font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;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,7 +1 @@
{# brutalist heading override — oversized uppercase display type. Fields: level, text, textClass. #}
{% if level == 1 %}<h1 class="bru-kicker {{ textClass }} flex-1" style="font-size:clamp(2.5rem,7vw,5rem)">{{ text }}</h1>
{% elif level == 3 %}<h3 class="bru-kicker {{ textClass }} flex-1" style="font-size:1.5rem;letter-spacing:-0.01em">{{ text }}</h3>
{% elif level == 4 %}<h4 class="bru-kicker {{ textClass }} flex-1" style="font-size:1.25rem;letter-spacing:-0.005em">{{ text }}</h4>
{% elif level == 5 %}<h5 class="bru-meta {{ textClass }} flex-1" style="font-size:0.95rem">{{ text }}</h5>
{% elif level == 6 %}<h6 class="bru-meta {{ textClass }} flex-1" style="font-size:0.8rem">{{ text }}</h6>
{% else %}<h2 class="bru-kicker {{ textClass }} flex-1" style="font-size:clamp(1.875rem,4.5vw,3.25rem)">{{ text }}</h2>{% endif %}
{% if level|integer == 1 %}<h1 class="{{ textClass }}" style="font-family: var(--font-heading, &amp;#39;Space Grotesk&amp;#39;, &amp;#39;Inter Tight&amp;#39;, sans-serif); font-weight: 700; font-size: clamp(2.5rem, 6vw, 5rem); line-height: 0.9; letter-spacing: -0.03em; text-transform: uppercase; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="{{ textClass }}" style="font-family: var(--font-heading, &amp;#39;Space Grotesk&amp;#39;, sans-serif); font-weight: 700; font-size: 1.5rem; line-height: 1; letter-spacing: -0.015em; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="{{ textClass }}" style="font-family: var(--font-heading, &amp;#39;Space Grotesk&amp;#39;, sans-serif); font-weight: 600; font-size: 1.25rem; line-height: 1.1; letter-spacing: -0.01em; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="{{ textClass }}" style="font-family: var(--font-heading, &amp;#39;Space Grotesk&amp;#39;, sans-serif); font-weight: 600; font-size: 1.125rem; line-height: 1.15; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="{{ textClass }}" style="font-family: var(--font-heading, &amp;#39;Space Grotesk&amp;#39;, sans-serif); font-weight: 600; font-size: 1rem; line-height: 1.2; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h6>{% else %}<h2 class="{{ textClass }}" style="font-family: var(--font-heading, &amp;#39;Space Grotesk&amp;#39;, &amp;#39;Inter Tight&amp;#39;, sans-serif); font-weight: 700; font-size: clamp(1.875rem, 4vw, 3rem); line-height: 0.95; letter-spacing: -0.02em; text-transform: uppercase; color: hsl(var(--foreground)); margin: 0;">{{ text }}</h2>{% endif %}

View File

@ -1,24 +0,0 @@
{# brutalist hero override — oversized display headline (the studio masthead identity). Styled-text fields accept {text} object OR bare string. Fields: eyebrow, headline, subheadline, description, ctaPrimary{.text,.url}, ctaSecondary{.text,.url}, template, backgroundImage. {% img %} takes no nested {% if %} so the bg is one unconditional call. #}
{% 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 %}
<section class="brutalist-hero relative overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}" data-has-media="{% if hasImage %}true{% else %}false{% endif %}">
{% if hasImage %}<div class="absolute inset-0 z-0">{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}</div>{% endif %}
<div class="bru-wrap relative z-10 {% if variant == "centered" %}text-center{% endif %}">
<div style="max-width:70rem;{% if variant == "centered" %}margin:0 auto{% endif %}">
{% if eyebrowText %}<span class="bru-tag mb-6">{{ eyebrowText }}</span>{% endif %}
{% if headlineText %}<h1 class="headline">{{ headlineText }}</h1>{% endif %}
{% if subText %}<p class="mt-6" style="font-family:var(--font-heading,'Space Grotesk',sans-serif);font-weight:400;font-size:clamp(1.1rem,2vw,1.6rem);color:hsl(var(--muted-foreground));max-width:42rem;{% if variant == "centered" %}margin-left:auto;margin-right:auto{% endif %}">{{ subText }}</p>{% endif %}
{% if descText %}<p class="mt-4 text-lg text-muted-foreground" style="max-width:42rem;{% if variant == "centered" %}margin-left:auto;margin-right:auto{% endif %}">{{ descText }}</p>{% endif %}
{% if pText or sText %}<div class="mt-9 flex flex-col sm:flex-row gap-4 {% if variant == "centered" %}justify-center{% endif %}">{% if pText and pUrl %}<a href="{{ pUrl }}" class="bru-btn bru-btn-solid">{{ pText }}</a>{% endif %}{% if sText and sUrl %}<a href="{{ sUrl }}" class="bru-btn bru-btn-ghost">{{ sText }}</a>{% endif %}</div>{% endif %}
{% if trustLogos %}<div class="mt-12"><p class="bru-meta mb-4">Trusted by</p><div class="flex flex-wrap items-center gap-8 {% if variant == "centered" %}justify-center{% endif %}">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}" target="_blank" rel="noopener noreferrer" class="opacity-70 hover:opacity-100 transition-opacity">{% 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-70" %}{% endif %}{% endfor %}</div></div>{% endif %}
</div>
</div>
</section>

View File

@ -1,31 +0,0 @@
{# brutalist hours-location override — bordered hours table + lazy map. Fields: heading, hours[] (day, closed, hours), specialHoursNote, address, phone, directionsUrl, mapEmbedUrl, mapButtonLabel. Keeps map button's inline onclick verbatim. Builtin uses fixed lucide icons (no icon split field). #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-mid">
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
<div class="mt-10 grid grid-cols-1 gap-8 md:grid-cols-2">
<div class="bru-slab bru-shadow-sm p-6">
{% if hours %}
<table class="w-full text-sm">
<tbody>
{% for row in hours %}
<tr style="border-bottom:1px solid hsl(var(--border))">
<th scope="row" class="py-2 pr-4 text-left bru-meta text-foreground">{{ row.day }}</th>
<td class="py-2 text-right {% if row.closed %}text-muted-foreground{% else %}text-foreground{% endif %} tabular-nums">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if specialHoursNote %}<p class="mt-4 bru-slab-bg px-3 py-2 text-sm text-muted-foreground">{{ specialHoursNote }}</p>{% endif %}
{% if address %}<div class="mt-6"><div class="bru-meta text-muted-foreground">Address</div><address class="mt-1 whitespace-pre-line not-italic">{{ address }}</address></div>{% endif %}
{% if phone %}<div class="mt-4"><div class="bru-meta text-muted-foreground">Phone</div><a href="tel:{{ phone }}" class="mt-1 inline-block bru-kicker transition-colors hover:text-accent" style="font-size:1rem;text-transform:none">{{ phone }}</a></div>{% endif %}
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener noreferrer" class="bru-btn bru-btn-solid mt-6"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#navigation"></use></svg>Get directions</a>{% endif %}
</div>
{% if mapEmbedUrl %}
<div class="bru-slab bru-shadow aspect-square w-full overflow-hidden bg-muted md:aspect-auto">
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('class','h-full w-full border-0');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" class="flex h-full min-h-[16rem] w-full items-center justify-center gap-2 bru-meta transition-colors hover:bg-accent hover:text-accent-foreground"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#map-pin"></use></svg>{{ mapButtonLabel|default:"Load map" }}</button>
</div>
{% endif %}
</div>
</div>
</section>

View File

@ -1,2 +1 @@
{# brutalist image override — hard-bordered figure, mono caption. Fields: src, alt, size, align, aspectRatio, objectFit, caption. Rounding is killed page-wide. #}
{% with size=size|default:"full" align=align|default:"center" aspectRatio=aspectRatio|default:"auto" objectFit=objectFit|default:"cover" %}{% if src %}<figure class="flex-1 {% if class %}{{ class }} {% endif %}{% if align == "left" %}flex justify-start{% elif align == "right" %}flex justify-end{% else %}flex justify-center{% endif %}"><div class="bru-slab bru-shadow relative overflow-hidden {% if size == "xs" %}max-w-[200px] w-full{% elif size == "sm" %}max-w-[400px] w-full{% elif size == "md" %}max-w-[600px] w-full{% elif size == "lg" %}max-w-[800px] w-full{% else %}w-full{% endif %}{% 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 %}{% else %}{% img src alt=alt class="w-full h-full object-cover" layout=size %}{% endif %}</div>{% if caption %}<figcaption class="bru-meta mt-3">{{ caption }}</figcaption>{% endif %}</figure>{% endif %}{% endwith %}
<figure class="brutalist-figure" style="margin: 2rem 0;">{% if src %}<img src="{{ src }}" alt="{{ alt }}" style="display: block; width: 100%; height: auto;">{% elif url %}<img src="{{ url }}" alt="{{ alt }}" style="display: block; width: 100%; height: auto;">{% endif %}{% if caption %}<figcaption>{{ caption }}</figcaption>{% endif %}</figure>

View File

@ -1,16 +0,0 @@
{# brutalist logo-strip override — mono heading + optional marquee, keyframe kept verbatim. Fields: heading, marquee, grayscale, logos[] (url, image, alt). #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap">
{% if heading %}<p class="bru-meta text-center text-muted-foreground">{{ heading }}</p>{% 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

@ -1,30 +0,0 @@
{# brutalist menu-list override — bordered menu sections, mono price. Fields: heading, intro, columns, currency, footnote, sections[] (title, note, items[] {name, badges[] {label}, description, price}). Builtin has no icon field so no icon split here. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-mid">
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-3 text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
<div class="mt-10 grid grid-cols-1 gap-6 {% if columns == 2 %}md:grid-cols-2{% endif %}">
{% for section in sections %}
<div class="bru-slab bru-shadow-sm menu-section p-6">
{% if section.title %}<h3 class="bru-kicker" style="font-size:1.35rem;text-transform:none">{{ section.title }}</h3>{% endif %}
{% if section.note %}<p class="mt-2 text-sm text-muted-foreground">{{ section.note }}</p>{% endif %}
<ul class="mt-4 space-y-5" style="border-top:3px solid hsl(var(--border));padding-top:1rem">
{% for item in section.items %}
<li class="flex items-baseline gap-4">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span class="bru-kicker" style="font-size:1rem;text-transform:none">{{ item.name }}</span>
{% for badge in item.badges %}{% if badge.label %}<span class="bru-tag-ghost" style="font-size:0.65rem;padding:0.05rem 0.4rem">{{ badge.label }}</span>{% endif %}{% endfor %}
</div>
{% if item.description %}<p class="mt-0.5 text-sm text-muted-foreground">{{ item.description }}</p>{% endif %}
</div>
{% if item.price %}<span class="shrink-0 bru-index" style="font-size:1.1rem">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% if footnote %}<p class="mt-10 bru-meta text-muted-foreground">{{ footnote }}</p>{% endif %}
</div>
</section>

View File

@ -1,69 +0,0 @@
{# brutalist navbar override — thick-ruled bar, uppercase wordmark, mono links, off-canvas drawer. One navbar per page (fixed drawer ids). Fields: companyName, logo, logoType, logoText.text, alt, menu.items[], cta.text|ctaLabel, cta.url|ctaUrl, stickyBehavior, sticky, showUtilityBar, utilityBar*, class. #}
{% set sb = stickyBehavior %}{% if not sb %}{% if sticky %}{% set sb = "sticky" %}{% else %}{% set sb = "none" %}{% endif %}{% endif %}
{% set cta_text = cta.text|default:ctaLabel %}
{% set cta_url = cta.url|default:ctaUrl %}
{% macro brand() %}<a href="/" class="flex-shrink-0 flex items-center">{% if logoType == "text" and logoText.text %}<span class="bru-kicker" style="font-size:1.4rem">{{ logoText.text }}</span>{% elif logo %}<img src="{{ logo }}" alt="{{ alt|default:companyName }}" class="h-8 w-auto"/>{% else %}<span class="bru-kicker" style="font-size:1.4rem">{{ companyName|default:"STUDIO" }}</span>{% endif %}</a>{% endmacro %}
{% 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="bru-meta px-3 py-2 text-foreground hover:text-accent transition-colors {{ item.css_class }}">{{ item.label }}</a>{% endmacro %}
{% macro dropdown(item) %}<div class="relative group"><button type="button" class="bru-meta px-3 py-2 text-foreground hover:text-accent transition-colors inline-flex items-center gap-1">{{ item.label }}<svg class="h-3 w-3" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg></button><div class="absolute left-0 mt-0 w-56 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-150 z-50"><div class="mt-2 bru-slab bru-shadow py-2">{% for child in item.children %}{% if child.item_type == "header" %}<div class="bru-meta px-4 py-2 text-muted-foreground">{{ 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 hover:text-accent-foreground transition-colors {{ child.css_class }}">{{ child.label }}</a>{% endif %}{% endfor %}</div></div></div>{% endmacro %}
{% macro ctabtn(extra) %}{% if cta_text %}<a href="{{ cta_url|default:"#" }}" class="bru-btn bru-btn-solid {{ extra }}" style="padding:0.5rem 1rem">{{ cta_text }}</a>{% endif %}{% endmacro %}
<nav data-bn-navbar data-bn-sticky="{{ sb }}" class="bn-navbar w-full bg-background text-foreground {% if sb == "sticky" or sb == "shrink-on-scroll" %}sticky top-0 z-40 {% endif %}{{ class }}" style="border-bottom:4px solid hsl(var(--border))">
{% if showUtilityBar %}<div class="w-full bg-foreground text-background {% if not showOnMobile %}hidden sm:block{% endif %}"><div class="bru-wrap flex items-center justify-between h-8 bru-meta" style="color:hsl(var(--background))"><div>{{ utilityBarText|safe }}</div><div class="flex items-center gap-4">{% if utilityBarPhone %}<a href="tel:{{ utilityBarPhone }}" class="hover:text-accent">{{ utilityBarPhone }}</a>{% endif %}{% if utilityBarEmail %}<a href="mailto:{{ utilityBarEmail }}" class="hover:text-accent">{{ utilityBarEmail }}</a>{% endif %}</div></div></div>{% endif %}
<div class="bru-wrap"><div class="bn-navbar-row flex items-center justify-between h-16">
<div class="flex items-center">{{ brand() }}</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>
<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 cursor-pointer" style="border:2px solid hsl(var(--border))"><span class="sr-only">Open menu</span><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#menu"></use></svg></label></div>
</div></div>
<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 md:hidden flex flex-col" style="border-left:4px solid hsl(var(--border))" role="dialog" aria-modal="true" aria-label="Site menu">
<div class="flex items-center justify-between p-4" style="border-bottom:4px solid hsl(var(--border))">{{ brand() }}<label for="bn-navbar-toggle" data-bn-nav-close class="p-2 cursor-pointer text-foreground" style="border:2px solid hsl(var(--border))"><span class="sr-only">Close menu</span><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#x"></use></svg></label></div>
<nav class="flex-1 overflow-y-auto py-4" aria-label="Mobile">{% for item in menu.items %}{% if item.children %}<details class="group/acc"><summary class="flex items-center justify-between px-6 py-3 bru-meta cursor-pointer hover:bg-accent hover:text-accent-foreground list-none">{{ item.label }}<svg class="h-3 w-3 transition-transform group-open/acc:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></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-foreground">{{ child.label }}</a>{% endif %}{% endfor %}</div></details>{% else %}<a href="{{ item.url|default:"#" }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-6 py-3 bru-meta hover:bg-accent hover:text-accent-foreground">{{ item.label }}</a>{% endif %}{% endfor %}</nav>
{% if cta_text %}<div class="p-4" style="border-top:4px solid hsl(var(--border))"><a href="{{ cta_url|default:"#" }}" class="bru-btn bru-btn-solid w-full">{{ cta_text }}</a></div>{% endif %}
</div>
</nav>
<style>
.bn-navbar-toggle{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;}
/* Drawer + overlay geometry lives here, not on Tailwind classes: fixed/top-0/
right-0/h-full/w-80/max-w-[85vw]/inset-0/z-* are not in the host safelist and
a runtime-installed theme's classes aren't JIT-scanned, so relying on them
would leave the mobile drawer unpositioned. display stays on the safelisted
flex/flex-col/md:hidden classes so the drawer is desktop-hidden. */
.bn-navbar-drawer{position:fixed;top:0;right:0;height:100%;width:20rem;max-width:85vw;z-index:50;transform:translateX(100%);transition:transform .3s ease;}
.bn-navbar-overlay{position:fixed;inset:0;z-index:40;background:rgba(0,0,0,0.55);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;}
</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'));});});
}
});
});
})();
</script>

View File

@ -1,10 +0,0 @@
{# brutalist newsletter-signup override — the {% signup_form %} tag carries the untouchable subscribe wiring; this template only supplies brutalist chrome. Fields: layout, title, description, listSlug, source, submitText, successMessage, placeholder. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-narrow">
<div class="bru-slab bru-shadow px-6 py-10 text-center md:px-10"{% if layout == "inline" %} style="background:hsl(var(--accent));color:hsl(var(--accent-foreground))"{% endif %}>
{% if title %}<h2 class="bru-kicker" style="font-size:clamp(1.5rem,3.5vw,2.25rem){% if layout == "inline" %};color:hsl(var(--accent-foreground)){% endif %}">{{ title }}</h2>{% endif %}
{% if description %}<p class="mx-auto mt-3 max-w-xl" style="{% if layout == "inline" %}color:hsl(var(--accent-foreground));opacity:0.85{% else %}color:hsl(var(--muted-foreground)){% endif %}">{{ 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

@ -1,20 +0,0 @@
{# brutalist page-suggestions override — themed 404. Fields: title, emptyMessage, showDescription. NOTE: the builtin computes `suggestions` via a server-side SearchSimilarPages DB query keyed on the requested path; that data is NOT exposed to codeless templates, so the dynamic suggestion list cannot be reproduced here (WO limitation). The `{% for %}` below renders defensively if the host ever supplies `suggestions`; otherwise the themed 404 chrome + a way home is shown. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-mid text-center">
<p class="bru-kicker text-accent" style="font-size:clamp(4rem,18vw,11rem);line-height:0.85">404</p>
<h2 class="bru-kicker mt-4" style="font-size:clamp(1.5rem,3.5vw,2.5rem)">{{ title|default:"Page not found" }}</h2>
{% if suggestions %}
<div class="mt-10 grid grid-cols-1 gap-5 sm:grid-cols-2 text-left">
{% for s in suggestions %}
<a href="/{{ s.Slug }}" class="bru-slab bru-shadow bru-hover block p-5">
<h3 class="bru-kicker" style="font-size:1.1rem;text-transform:none">{{ s.Title }}</h3>
{% if showDescription and s.Excerpt %}<p class="mt-2 text-sm text-muted-foreground">{{ s.Excerpt }}</p>{% endif %}
</a>
{% endfor %}
</div>
{% elif emptyMessage %}
<p class="mt-6 text-lg text-muted-foreground mx-auto" style="max-width:38rem">{{ emptyMessage }}</p>
{% endif %}
<div class="mt-10"><a href="/" class="bru-btn bru-btn-solid">Back to home</a></div>
</div>
</section>

View File

@ -1,13 +0,0 @@
{# brutalist password-reset-form override — request-reset form. HTMX endpoint + input name load-bearing; kept exact. #}
<div class="password-reset-form" style="max-width:28rem;margin:0 auto">
<div id="reset-message"></div>
<div id="request-reset-panel">
<h2 class="bru-kicker" style="font-size:1.75rem">Reset Password</h2>
<p class="mt-3 text-sm text-muted-foreground">Enter your email address and we will send you 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 space-y-4">
<div><label class="bru-label">Email</label><input type="email" name="email" required class="bru-field"></div>
<button type="submit" class="bru-btn bru-btn-solid w-full" hx-disabled-elt="this">Send Reset Link</button>
</form>
<p class="text-center mt-5 bru-meta"><a href="/login" class="text-accent underline underline-offset-4">Back to login</a></p>
</div>
</div>

View File

@ -1,16 +0,0 @@
{# brutalist popular-posts override — compact ranked list from the `posts` provider. Fields: heading, posts[], showRank, showThumbnail. #}
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="bru-meta mb-4 text-foreground">{{ heading }}</h2>{% endif %}
<ol class="space-y-3">
{% for post in posts %}
<li class="bru-slab bru-shadow-sm bru-hover flex items-start gap-3 p-3">
{% if showRank %}<span class="bru-index" style="font-size:1.5rem;line-height:1">{{ forloop.Counter }}</span>{% endif %}
{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="h-12 w-12 flex-shrink-0 overflow-hidden bg-muted" style="border:2px solid hsl(var(--border))">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover" %}</a>{% endif %}
<div class="min-w-0">
<h3 class="bru-kicker text-sm leading-snug" style="text-transform:none"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
{% if post.published_at %}<p class="bru-meta mt-0.5">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
</div>
</li>
{% endfor %}
</ol>
</section>

View File

@ -1,20 +0,0 @@
{# brutalist post-hero override — current post from the `post` provider. Featured image = overlaid banner; otherwise clean text hero. Fields: align, post.*, showExcerpt, showMeta. #}
{% set center = align != "left" %}
{% if post.featured_image_url %}
<section class="hero relative flex min-h-[50vh] flex-col justify-end overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="absolute inset-0 z-0">{% img post.featured_image_url alt=post.featured_image_alt class="h-full w-full object-cover" layout="hero" %}<div class="absolute inset-0 bg-black/60"></div></div>
<div class="bru-wrap-mid relative z-10 w-full py-12 {% if center %}text-center{% endif %}">
<h1 class="bru-kicker text-3xl leading-tight text-white sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
{% if showExcerpt and post.excerpt %}<p class="mt-4 max-w-2xl text-lg text-white/80{% if center %} mx-auto{% endif %}">{{ post.excerpt }}</p>{% endif %}
{% if showMeta %}<div class="bru-meta mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-white/80{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="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 class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-narrow w-full {% if center %}text-center{% endif %}">
<h1 class="bru-kicker text-3xl leading-tight sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
{% if showExcerpt and post.excerpt %}<p class="mt-4 text-lg text-muted-foreground{% if center %} mx-auto max-w-2xl{% endif %}">{{ post.excerpt }}</p>{% endif %}
{% if showMeta %}<div class="bru-meta mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="text-foreground">{{ 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

@ -1,9 +0,0 @@
{# brutalist post-metadata override — author/date/reading-time row from the `post` provider. Fields: showAuthor, showAvatar, showDate, showReadingTime, post.*, post.categories[]. #}
{% if post %}
<div class="bru-meta flex flex-wrap items-center gap-x-4 gap-y-2 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 object-cover" %}{% endif %}<span class="text-foreground">{{ 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="bru-tag">{{ cat.name }}</a>{% endfor %}</span>{% endif %}
</div>
{% endif %}

View File

@ -1,25 +0,0 @@
{# brutalist pricing override — bordered tier slabs, highlighted tier gets accent band. Fields: heading, intro, currency, billingNote, tiers[] (highlighted, badge, name, price, period, description, features[], ctaUrl, ctaLabel). #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap">
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ 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 %}bru-slab bru-shadow{% else %}bru-slab bru-shadow-sm{% endif %}"{% if tier.highlighted %} style="background:hsl(var(--accent));color:hsl(var(--accent-foreground))"{% endif %}>
{% if tier.badge %}<span class="bru-tag absolute -top-4 left-6">{{ tier.badge }}</span>{% endif %}
<h3 class="bru-kicker" style="font-size:1.25rem">{{ tier.name }}</h3>
<p class="mt-4 flex items-baseline gap-1">
<span class="bru-index" style="font-size:2.75rem;line-height:1">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>
{% if tier.period %}<span class="bru-meta {% if tier.highlighted %}opacity-80{% else %}text-muted-foreground{% endif %}">{{ tier.period }}</span>{% endif %}
</p>
{% if tier.description %}<p class="mt-3 text-sm {% if tier.highlighted %}opacity-90{% else %}text-muted-foreground{% endif %}">{{ 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 {% if tier.highlighted %}text-accent-foreground{% else %}text-accent{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg><span>{{ feature }}</span></li>{% endfor %}
</ul>{% endif %}
{% if tier.ctaUrl and tier.ctaLabel %}<a href="{{ tier.ctaUrl }}" class="bru-btn mt-8 w-full justify-center {% if tier.highlighted %}bru-btn-ghost{% else %}bru-btn-solid{% endif %}">{{ tier.ctaLabel }}</a>{% endif %}
</div>
{% endfor %}
</div>
{% if billingNote %}<p class="mt-8 text-center bru-meta text-muted-foreground">{{ billingNote }}</p>{% endif %}
</div>
</section>

View File

@ -1,2 +0,0 @@
{# brutalist quote override — thick accent rule or bordered slab. Fields: quote, cite, attribution, role, style, align. #}
{% with style=style|default:"block" align=align|default:"left" %}<figure class="my-8 {% if style == "inline-card" %}bru-slab bru-shadow p-6 md:p-8{% else %}pl-6 md:pl-8 py-2{% endif %}{% if align == "center" %} text-center{% endif %}{% if class %} {{ class }}{% endif %}"{% if style != "inline-card" %} style="border-left:6px solid hsl(var(--accent))"{% endif %}><blockquote{% if cite %} cite="{{ cite }}"{% endif %} class="bru-kicker text-foreground" style="text-transform:none;line-height:1.15;font-size:{% if style == "pull" %}clamp(1.75rem,4vw,2.75rem){% else %}1.35rem{% endif %}">{{ quote|safe }}</blockquote>{% if attribution or role or cite %}<figcaption class="bru-meta mt-4 {% if align == "center" %}flex flex-col items-center{% endif %}">{% if attribution %}<span class="text-foreground">{{ attribution }}</span>{% endif %}{% if role %}<span>{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" class="text-accent underline underline-offset-4">Source</a>{% endif %}</figcaption>{% endif %}</figure>{% endwith %}

View File

@ -1,19 +0,0 @@
{# brutalist related-posts override — grid from the `posts` provider, thick top rule. Fields: heading, columns, posts[]. #}
{% if posts %}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}" style="border-top:4px solid hsl(var(--border))">
<div class="bru-wrap">
<h2 class="bru-kicker mb-8" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading|default:"Related posts" }}</h2>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% else %}lg:grid-cols-3{% endif %}">
{% for post in posts %}
<article class="group bru-slab bru-shadow bru-hover flex flex-col overflow-hidden">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-muted" style="border-bottom:3px solid hsl(var(--border))">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-col gap-1 p-5">
<h3 class="bru-kicker text-base leading-snug" style="text-transform:none"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
{% if post.published_at %}<p class="bru-meta mt-1">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
</div>
</article>
{% endfor %}
</div>
</div>
</section>
{% endif %}

View File

@ -1,14 +0,0 @@
{# brutalist rich-section override — split text/media with bordered media frame. Fields: background, mediaPosition, eyebrow, heading, body, ctaUrl, ctaLabel, media, mediaAlt. #}
<section class="bru-section {% 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="bru-wrap grid items-center gap-10 md:grid-cols-2 md:gap-16">
<div class="{% if mediaPosition == "left" %}md:order-2{% else %}md:order-1{% endif %}">
{% if eyebrow %}<span class="bru-tag mb-4">{{ eyebrow }}</span>{% endif %}
{% if heading %}<h2 class="bru-kicker" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if body %}<div class="mt-4 space-y-4 text-lg text-muted-foreground [&_a]:text-accent [&_a]:underline [&_strong]:text-foreground">{{ body|safe }}</div>{% endif %}
{% if ctaUrl and ctaLabel %}<a href="{{ ctaUrl }}" class="bru-btn bru-btn-solid mt-6">{{ ctaLabel }} →</a>{% endif %}
</div>
<div class="{% if mediaPosition == "left" %}md:order-1{% else %}md:order-2{% endif %}">
{% if media %}<div class="bru-slab bru-shadow overflow-hidden">{% img media alt=mediaAlt class="w-full h-auto object-cover" %}</div>{% endif %}
</div>
</div>
</section>

View File

@ -1,2 +0,0 @@
{# brutalist rich-text override — imported HTML content. Field: html. #}
{% if html %}<article class="prose max-w-none text-foreground [&_a]:text-accent [&_a]:underline [&_a]:underline-offset-4 [&_h2]:bru-kicker [&_h3]:bru-kicker [&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-5" style="line-height:1.7">{{ html|safe }}</article>{% endif %}

View File

@ -1,13 +0,0 @@
{# brutalist sidebar-nav override — vertical menu from the `menus` provider. Variants default | compact. highlight-current script kept verbatim (toggles text-muted-foreground → text-foreground/bg-accent on data-bn-sidebar-link). Fields: template, showBorder, title, menu.items[], highlightCurrent. #}
{% set compact = template == "compact" %}
<nav class="flex-1 {% if compact %}w-56 py-4 pr-3{% else %}w-64 py-6 pr-4{% endif %}{% if showBorder %} border-r-[3px] border-border{% endif %}{% if class %} {{ class }}{% endif %}">
{% if title %}<h3 class="bru-meta text-foreground {% if compact %}px-2 mb-3{% else %}px-3 mb-4{% endif %}">{{ title }}</h3>{% endif %}
<ul class="{% if compact %}space-y-0.5{% else %}space-y-1{% endif %}">
{% for item in menu.items %}
<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link class="flex items-center text-sm text-muted-foreground transition-colors hover:text-foreground 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-accent" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg>{% endif %}{{ item.label }}</a></li>
{% endfor %}
</ul>
</nav>
{% if highlightCurrent %}<script>
(function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.classList.remove('text-muted-foreground');a.classList.add('text-foreground','bg-accent','font-medium');}});})();
</script>{% endif %}

View File

@ -1,14 +0,0 @@
{# brutalist social-links override — bordered square chips (widget, not a full section). Fields: heading, style, links[] (url, label, network). Keeps builtin icon split on network + literal conditional style classes. #}
<div class="social-links{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="mb-3 bru-meta text-muted-foreground">{{ 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="bru-shadow-sm bru-hover inline-flex items-center gap-2 bg-card text-foreground transition-colors hover:bg-accent hover:text-accent-foreground {% if style == "stack" %}w-full px-4 py-2{% else %}p-2.5{% endif %}" style="border:3px solid hsl(var(--border))">
{% if link.network %}<svg class="h-5 w-5" 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="bru-meta">{{ link.label }}</span>{% endif %}
</a>
</li>
{% endif %}{% endfor %}
</ul>
</div>

View File

@ -1,49 +0,0 @@
{# brutalist stats override — bordered stat tiles with hard offset, count-up script kept verbatim. Fields: heading, intro, columns, countUp, items[] (prefix, value, suffix, label). #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}"{% if countUp %} data-stats{% endif %}>
<div class="bru-wrap">
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ intro }}</p>{% endif %}
<dl class="mt-12 grid grid-cols-2 gap-6 {% 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="bru-slab bru-shadow-sm p-6 text-center">
<dd class="bru-index" style="font-size:clamp(2.25rem,5vw,3.25rem);line-height:1">{% 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="bru-meta mt-3 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

@ -1,22 +0,0 @@
{# brutalist team override — bordered member slabs. Fields: heading, intro, layout, columns, members[] (photo, name, role, bio, socials[] {url, icon}). Keeps builtin icon split + literal conditional column classes. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap">
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ 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-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 %}{% endif %}">
{% for member in members %}
<div class="bru-slab bru-shadow-sm p-5 {% if layout == "list" %}flex items-start gap-5{% 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 bg-muted" style="border:3px solid hsl(var(--border))">{% 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="bru-kicker" style="font-size:1.15rem;text-transform:none">{{ member.name }}</h3>{% endif %}
{% if member.role %}<p class="bru-meta text-accent">{{ member.role }}</p>{% endif %}
{% if member.bio %}<p class="mt-2 text-sm text-muted-foreground">{{ 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-accent" 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

@ -1,27 +0,0 @@
{# brutalist testimonial override — bordered quote slabs with hard offset. Fields: heading, layout, quotes[] (rating, quote, avatar, name, role). Keeps builtin's 5 literal rating-star comparisons. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap">
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ 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="bru-slab bru-shadow flex flex-col p-6{% if layout == "carousel" %} min-w-[18rem] max-w-sm shrink-0 snap-start{% endif %}">
{% if q.rating %}<div class="mb-3 flex gap-0.5" aria-label="Rated {{ q.rating }} out of 5">
<svg class="h-4 w-4 {% if q.rating >= 1 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 2 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 3 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 4 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 5 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
</div>{% endif %}
<blockquote class="flex-1 text-lg leading-relaxed">{% if q.quote %}&ldquo;{{ q.quote }}&rdquo;{% endif %}</blockquote>
<figcaption class="mt-5 flex items-center gap-3">
{% if q.avatar %}<span class="h-11 w-11 shrink-0 overflow-hidden bg-muted" style="border:3px solid hsl(var(--border))">{% img q.avatar alt=q.name class="h-full w-full object-cover" %}</span>{% endif %}
<span>
{% if q.name %}<span class="block bru-kicker" style="font-size:1rem;text-transform:none">{{ q.name }}</span>{% endif %}
{% if q.role %}<span class="block bru-meta text-muted-foreground">{{ q.role }}</span>{% endif %}
</span>
</figcaption>
</figure>
{% endfor %}
</div>
</div>
</section>

View File

@ -1,2 +1 @@
{# brutalist text override — body copy, generous measure. Fields: text, proseClass. #}
<div class="prose max-w-none flex-1 text-foreground [&_a]:text-accent [&_a]:underline [&_a]:underline-offset-4 [&_strong]:text-foreground [&_h2]:bru-kicker [&_h3]:bru-kicker {{ proseClass }}" style="line-height:1.7">{{ text|safe }}</div>
<div class="brutalist-text {{ class }}" style="font-family: var(--font-body, 'Inter', -apple-system, sans-serif); font-size: 1.0625rem; line-height: 1.7; color: hsl(var(--foreground)); max-width: 70ch; letter-spacing: 0.005em;">{{ text|safe }}</div>

View File

@ -1,22 +0,0 @@
{# brutalist timeline override — square step markers + hard rule. Fields: heading, intro, orientation, marker, steps[] (label, title, text, media). Keeps forloop.counter, marker enum branch, and literal conditional orientation classes. #}
<section class="bru-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="bru-wrap-mid">
{% if heading %}<h2 class="bru-kicker text-center" style="font-size:clamp(1.75rem,4vw,3rem)">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 text-center text-lg text-muted-foreground" style="max-width:44rem">{{ 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-[0.6875rem] before:top-2 before:h-[calc(100%-1rem)] before:w-[3px] before:bg-border{% endif %}">
{% for step in steps %}
<li class="{% if orientation == "horizontal" %}text-center{% else %}relative pl-14{% endif %}">
<span class="flex h-9 w-9 items-center justify-center bg-accent text-accent-foreground {% if orientation == "horizontal" %}mx-auto{% else %}absolute left-0 top-0{% endif %}" style="border:3px solid hsl(var(--border))">
{% if marker == "numbered" %}<span class="bru-index" style="font-size:1rem">{{ forloop.counter }}</span>{% elif marker == "dot" %}<span class="h-2 w-2 bg-accent-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="bru-meta text-accent">{{ step.label }}</p>{% endif %}
{% if step.title %}<h3 class="mt-1 bru-kicker" style="font-size:1.15rem;text-transform:none">{{ step.title }}</h3>{% endif %}
{% if step.text %}<p class="mt-1 text-muted-foreground">{{ step.text }}</p>{% endif %}
{% if step.media %}<div class="bru-slab bru-shadow-sm 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

@ -1,10 +0,0 @@
{# brutalist utility-bar override — mono uppercase meta strip. Fields: leftItems[], rightItems[] (.icon,.label,.link), variant, showOnMobile. #}
{% set ubstyle = "background:hsl(var(--background));color:hsl(var(--muted-foreground));border-bottom:1px solid hsl(var(--border))" %}
{% if variant == "primary" %}{% set ubstyle = "background:hsl(var(--accent));color:hsl(var(--accent-foreground))" %}{% elif variant == "dark" %}{% set ubstyle = "background:hsl(var(--foreground));color:hsl(var(--background))" %}{% endif %}
{% macro ubitem(it) %}{% if it.link %}<a href="{{ it.link }}" class="inline-flex items-center gap-1.5 hover:opacity-70 transition-opacity">{% if it.icon %}::{{ it.icon }}:sm::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</a>{% else %}<span class="inline-flex items-center gap-1.5">{% if it.icon %}::{{ it.icon }}:sm::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</span>{% endif %}{% endmacro %}
<div class="w-full bru-meta {% if not showOnMobile %}hidden sm:block{% endif %} {{ class }}" style="{{ ubstyle }}">
<div class="bru-wrap 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

@ -1,23 +0,0 @@
{# brutalist video-embed override — hard-bordered click-to-play facade. Fields: url, media, poster, title, caption, aspect. #}
{% with aspect=aspect|default:"16:9" %}<figure class="bn-video my-8{% if class %} {{ class }}{% endif %}"><div class="bn-video-frame bru-slab bru-shadow relative w-full overflow-hidden {% 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:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-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/25" aria-hidden="true"></span><span class="relative inline-flex h-16 w-16 items-center justify-center text-accent-foreground transition-transform group-hover:scale-105 motion-reduce:transform-none" style="background:hsl(var(--accent));border:3px solid hsl(var(--border));box-shadow:4px 4px 0 0 hsl(var(--border))"><svg class="h-7 w-7" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg></span></button></div>{% if caption %}<figcaption class="bru-meta mt-3 text-center">{{ 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 %}