diff --git a/RECOMMENDED_FONTS.md b/RECOMMENDED_FONTS.md index 7e8a2c3..348ea31 100644 --- a/RECOMMENDED_FONTS.md +++ b/RECOMMENDED_FONTS.md @@ -1,39 +1,44 @@ -# Editorial — recommended Google Fonts +# Editorial — fonts -This theme ships with no bundled fonts in the current build pass -(`fonts.json = []`). Admins assign fonts via the BlockNinja typography panel. -The theme's CSS routes every `font-family` through the host-provided -`--font-heading`, `--font-body`, and `--font-mono` custom properties with -fallback stacks taken from spec §5, so the layout is usable before any font -is picked. +This theme **bundles** a Latin subset of two open-licence (SIL OFL 1.1) +serif families, so it looks correct the moment it is activated — no admin +setup required. The bundled faces are declared in `fonts.json` and land in +the typography picker as `source=template` rows. -For the intended editorial aesthetic, open the typography panel in admin and -add each of the following from the **Google Fonts** tab: +| Slot | Bundled family | Weights / styles | Role | +|---------|-------------------|---------------------------|-------------------------------------------------------------| +| Heading | Playfair Display | 700 normal, 900 italic | Didone display face for the masthead wordmark and headlines | +| Body | Source Serif 4 | 400 normal, 400 italic | Long-read body type; italic for marginalia and captions | -| Slot | Family | Source | Weights / styles | Why | -|-----------|-----------------------|--------------------|------------------------------------------------------------------|----------------------------------------------------------------------| -| Heading | Playfair Display | `google:Playfair Display` | 700 normal, 900 italic | Didone display face for the masthead wordmark and headlines. | -| Body | Source Serif 4 | `google:Source Serif 4` | 400 normal, 400 italic, 600 normal | Long-read body type; italic for marginalia and figcaptions. | -| UI / caption (body slot) | Inter | `google:Inter` | 500 normal, 600 normal | Byline names, timestamps, and small-caps section labels. | -| Mono | JetBrains Mono | `google:JetBrains Mono` | 400 normal | Code, datelines on financial copy, ISSN in the colophon. | +The bundled woff2 files and their licence record live in `assets/fonts/` +(`OFL.txt`). Every `font-family` in the theme is routed through +`var(--font-heading, ...)`, `var(--font-body, ...)` and `var(--font-mono, ...)` +with a fallback stack, so the layout is usable before any font is assigned +and the admin font picker stays in full control. -## How to assign +## Assigning the bundled faces 1. In the BlockNinja admin, open **Settings → Theme → Typography**. -2. Click the **Google Fonts** tab. -3. Search for the family above (e.g. "Playfair Display"), then click **Add**. -4. Once added, the family appears in the picker for that slot. -5. Assign **Playfair Display** to the *Heading* slot. -6. Assign **Source Serif 4** to the *Body* slot. -7. Assign **JetBrains Mono** to the *Mono* slot. -8. (Optional) For UI / caption elements that ride the body slot but want a - sans-serif treatment, override per-block via the editor. +2. In the **Bundled** tab, assign **Playfair Display** to the *Heading* slot. +3. Assign **Source Serif 4** to the *Body* slot. +4. Leave *Mono* as the system default, or assign **JetBrains Mono** from the + Google Fonts tab — it is used for the masthead kicker, datelines and the + colophon ISSN. -## Wave-2 (out of scope here) +## Optional upgrades (Google Fonts tab) -If the editorial brand is going to be locked in to a specific commercial -display face (e.g. *Migra* or *PP Editorial New* instead of *Playfair -Display*), licence and bundle the woff2 set, then update `fonts.json` to -declare the family + variants. The CSS does not change — the same -`var(--font-heading)` consumer pattern picks up the bundled face once an -admin assigns it. +- **Spectral** (`google:Spectral`) — a refined small-caps serif. Assign it to + the body slot if you want the small-caps section labels to carry true + small-caps rather than the synthesized `font-variant: small-caps` fallback. +- **Newsreader** (`google:Newsreader`) — an alternative reading serif for the + body slot; swaps in without any CSS change via `var(--font-body)`. +- **JetBrains Mono** (`google:JetBrains Mono`) — for the *Mono* slot + (kicker / dateline / ISSN). + +## Bundling a commercial display face (out of scope for Wave A) + +If the brand locks in a licensed display face (e.g. *Migra* or *PP Editorial +New*) in place of Playfair Display, licence and subset the woff2 set, drop it +in `assets/fonts/`, and add the family + variants to `fonts.json`. The CSS +does not change — the same `var(--font-heading)` consumer picks up whichever +face an admin assigns. diff --git a/RECOMMENDED_ICONS.md b/RECOMMENDED_ICONS.md new file mode 100644 index 0000000..e3deeca --- /dev/null +++ b/RECOMMENDED_ICONS.md @@ -0,0 +1,25 @@ +# Recommended Icons — Editorial + +Editorial is a typographic theme. It leans on hairline rules and small-caps +labels, not iconography, so it references only a single bundled pack. +`plugin.mod` declares it in `required_icon_packs`; until the loader +auto-installs declared packs, an admin should confirm it is enabled before +activation. + +## Required pack + +- **lucide** — ships bundled with the CMS (v0.460). Enabled by default. No + action needed in most installs; if icons render as empty boxes, open + `/admin/icons` and confirm **Lucide** is installed and enabled. + +## Icons used by the theme chrome and overrides + +All via the sprite pipeline (`` +or the `::lucide:::` shorthand): + +`arrow-right`, `chevron-down`, `chevron-left`, `chevron-right`, `x`, `check`, +`play`, `map-pin`, `diamond`, `minus`. + +Block content that stores an icon (feature-grid items, social links, contact +channels) may reference any installed pack as a `pack:name` string; the +schemas default to `lucide` so the theme renders without extra setup. diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 0000000..6ef2ae9 --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,24 @@ +Bundled fonts — license record (SIL Open Font License, Version 1.1) +==================================================================== + +The woff2 files in this directory are Latin subsets of the following +open-source families, both distributed under the SIL Open Font License 1.1 +(https://openfontlicense.org). Subsetting and format conversion (TTF to +woff2) are expressly permitted by the OFL; the derived subsets remain under +the same license. + +Playfair Display + playfair-display-700.woff2, playfair-display-900-italic.woff2 + Copyright (c) Claus Eggers Sorensen (https://www.forthehearts.net), with + Reserved Font Name "Playfair Display". Display face for masthead wordmarks + and headlines. + +Source Serif 4 + source-serif-4-400.woff2, source-serif-4-400-italic.woff2 + Copyright (c) Adobe (https://github.com/adobe-fonts/source-serif), with + Reserved Font Name "Source Serif". Long-read body face; italic for + marginalia and figure captions. + +The full SIL Open Font License 1.1 text applies to each family and is +available at https://openfontlicense.org. This file records the license and +copyright attribution as required for redistribution under the OFL. diff --git a/assets/fonts/playfair-display-700.woff2 b/assets/fonts/playfair-display-700.woff2 new file mode 100644 index 0000000..39eefe0 Binary files /dev/null and b/assets/fonts/playfair-display-700.woff2 differ diff --git a/assets/fonts/playfair-display-900-italic.woff2 b/assets/fonts/playfair-display-900-italic.woff2 new file mode 100644 index 0000000..61fa874 Binary files /dev/null and b/assets/fonts/playfair-display-900-italic.woff2 differ diff --git a/assets/fonts/source-serif-4-400-italic.woff2 b/assets/fonts/source-serif-4-400-italic.woff2 new file mode 100644 index 0000000..3595a72 Binary files /dev/null and b/assets/fonts/source-serif-4-400-italic.woff2 differ diff --git a/assets/fonts/source-serif-4-400.woff2 b/assets/fonts/source-serif-4-400.woff2 new file mode 100644 index 0000000..30dbe2d Binary files /dev/null and b/assets/fonts/source-serif-4-400.woff2 differ diff --git a/blocks/blocks.yaml b/blocks/blocks.yaml index d84eae3..30516be 100644 --- a/blocks/blocks.yaml +++ b/blocks/blocks.yaml @@ -1,48 +1,33 @@ -# Editorial theme block definitions (codeless .bnp, WO-WZ-021). -# Keys are fully qualified (editorial:) 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. +# Editorial theme block definitions (codeless .bnp, WO-TF-017). +# +# These are the theme's TRUE persona blocks — furniture with no builtin +# equivalent. Everything else the theme used to ship as a block (masthead, +# byline, pull quote, section label, colophon) is now a re-skin OVERRIDE of a +# builtin (navbar, post-metadata, quote, divider, footer) and lives in +# templates/overrides/editorial/. Keys are fully qualified (editorial:) +# because the codeless loader registers definitions verbatim. blocks: - - key: editorial:masthead - title: Masthead - description: Newspaper-style masthead with Playfair wordmark and hairline rule - category: navigation - schema: masthead.schema.json - template: masthead.ninjatpl - - key: editorial:byline - title: Byline - description: Author byline with optional photo, dateline, and read time - category: theme - schema: byline.schema.json - template: byline.ninjatpl - - key: editorial:pullquote - title: Pull Quote - description: Large oxblood pull quote in Playfair italic - category: theme - schema: pullquote.schema.json - template: pullquote.ninjatpl - key: editorial:dropcap_intro title: Drop Cap Intro - description: Opening paragraph with an oxblood Playfair drop cap + description: Opening paragraph whose first letter renders as an oxblood Playfair drop cap category: theme schema: dropcap_intro.schema.json template: dropcap_intro.ninjatpl - key: editorial:marginalia title: Marginalia Rail - description: Right-rail italic notes for longform articles + description: Right-rail italic notes for longform articles; collapses inline on narrow viewports category: theme schema: marginalia.schema.json template: marginalia.ninjatpl - - key: editorial:section_label - title: Section Label - description: Small-caps section label with optional hairline divider + - key: editorial:reading_list + title: Reading List + description: A curated further-reading list — small-caps heading over hairline-ruled links with notes category: theme - schema: section_label.schema.json - template: section_label.ninjatpl - - key: editorial:colophon - title: Colophon - description: Editorial footer with ISSN, subscribe stub, and link list - category: navigation - schema: colophon.schema.json - template: colophon.ninjatpl + schema: reading_list.schema.json + template: reading_list.ninjatpl + - key: editorial:field_notes + title: Field Notes + description: Dated notebook entries in small caps, separated by hairline rules + category: theme + schema: field_notes.schema.json + template: field_notes.ninjatpl diff --git a/blocks/byline.ninjatpl b/blocks/byline.ninjatpl deleted file mode 100644 index d17b1a0..0000000 --- a/blocks/byline.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/blocks/byline.schema.json b/blocks/byline.schema.json deleted file mode 100644 index c23a51e..0000000 --- a/blocks/byline.schema.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Byline", - "description": "Article byline with author name, optional photo, optional read-time, and dateline.", - "type": "object", - "properties": { - "authorSlug": { - "type": "string", - "title": "Author Slug", - "description": "Slug of the author whose name and photo should be rendered.", - "x-editor": "slug" - }, - "showPhoto": { - "type": "string", - "title": "Show Photo", - "description": "Render the author's avatar to the left of the name.", - "x-editor": "select", - "enum": ["false", "true"], - "default": "true" - }, - "showReadTime": { - "type": "string", - "title": "Show Read Time", - "description": "Include the read-time estimate next to the dateline.", - "x-editor": "select", - "enum": ["false", "true"], - "default": "true" - }, - "dateline": { - "type": "string", - "title": "Dateline", - "description": "Optional explicit dateline (e.g. \"London, March 14\"). Rendered in small caps.", - "x-editor": "text" - } - } -} diff --git a/blocks/colophon.ninjatpl b/blocks/colophon.ninjatpl deleted file mode 100644 index cd476e5..0000000 --- a/blocks/colophon.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -
{% if showSignup == true or showSignup == "true" %}
{% endif %}{% if links %}{% endif %}{% if issn %}
ISSN {{ issn }}
{% endif %}
diff --git a/blocks/colophon.schema.json b/blocks/colophon.schema.json deleted file mode 100644 index 733c8d9..0000000 --- a/blocks/colophon.schema.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Colophon", - "description": "Editorial footer with print-convention ISSN field, optional subscribe stub, and a list of footer links.", - "type": "object", - "properties": { - "showSignup": { - "type": "string", - "title": "Show Subscribe Stub", - "description": "Render the newsletter subscribe stub above the link list.", - "x-editor": "select", - "enum": ["false", "true"], - "default": "false" - }, - "issn": { - "type": "string", - "title": "ISSN", - "description": "Print-convention ISSN number. Rendered in JetBrains Mono small caps.", - "x-editor": "text" - }, - "links": { - "type": "array", - "title": "Footer Links", - "description": "Inline footer link list.", - "default": [], - "x-editor": "array", - "items": { - "type": "object", - "properties": { - "text": { - "type": "string", - "title": "Link Text", - "description": "Display text.", - "x-editor": "text" - }, - "url": { - "type": "string", - "title": "URL", - "description": "External URL.", - "x-editor": "link" - } - } - } - } - } -} diff --git a/blocks/field_notes.ninjatpl b/blocks/field_notes.ninjatpl new file mode 100644 index 0000000..1c61e8b --- /dev/null +++ b/blocks/field_notes.ninjatpl @@ -0,0 +1,14 @@ +{# field_notes (editorial) — dated notebook entries: small-caps dateline over a short note, hairline-ruled. Fields: title, notes[].date/note. #} +
+ {% if title %}{% endif %} + {% if notes %} +
+ {% for entry in notes %} +
+ {% if entry.date %}
{{ entry.date }}
{% endif %} + {% if entry.note %}
{{ entry.note|safe }}
{% endif %} +
+ {% endfor %} +
+ {% endif %} +
diff --git a/blocks/field_notes.schema.json b/blocks/field_notes.schema.json new file mode 100644 index 0000000..76c6f45 --- /dev/null +++ b/blocks/field_notes.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Field Notes", + "description": "Dated notebook entries — a small-caps date over a short note, separated by hairline rules. The essayist's running observations.", + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Heading", + "description": "Small-caps section heading (e.g. \"Field notes\").", + "x-editor": "text", + "default": "Field notes" + }, + "notes": { + "type": "array", + "title": "Notes", + "description": "The dated entries, newest first.", + "default": [], + "x-editor": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "title": "Date", + "description": "The dateline, rendered in small caps (e.g. \"March 14\").", + "x-editor": "text" + }, + "note": { + "type": "string", + "title": "Note", + "description": "The entry body. Rich text is permitted.", + "x-editor": "richtext" + } + } + } + } + } +} diff --git a/blocks/masthead.ninjatpl b/blocks/masthead.ninjatpl deleted file mode 100644 index e2f0f96..0000000 --- a/blocks/masthead.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -
{% if compact == true or compact == "true" %}
{{ title|default:"Editorial" }}{% if menuName %}{{ menuName }}{% endif %}

{% else %}
{% if kicker %}
{{ kicker }}
{% endif %}{{ title|default:"Editorial" }}
{% if menuName %}{% endif %}
{% endif %}
diff --git a/blocks/masthead.schema.json b/blocks/masthead.schema.json deleted file mode 100644 index 0846b96..0000000 --- a/blocks/masthead.schema.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Masthead", - "description": "Newspaper-style masthead with wordmark, optional kicker, hairline rule, and section navigation menu.", - "type": "object", - "properties": { - "title": { - "type": "string", - "title": "Wordmark", - "description": "The publication name. Rendered in Playfair Display 900 italic.", - "x-editor": "text" - }, - "kicker": { - "type": "string", - "title": "Kicker", - "description": "Small mono caption above the wordmark (e.g. issue label or established date).", - "x-editor": "text" - }, - "menuName": { - "type": "string", - "title": "Section Menu", - "description": "Menu providing the section navigation links rendered beneath the masthead.", - "x-editor": "menu-select" - }, - "compact": { - "type": "string", - "title": "Compact Mode", - "description": "Use the compact masthead variant (single line) on article pages.", - "x-editor": "select", - "enum": ["false", "true"], - "default": "false" - } - } -} diff --git a/blocks/pullquote.ninjatpl b/blocks/pullquote.ninjatpl deleted file mode 100644 index e053801..0000000 --- a/blocks/pullquote.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -{% if quote %}{% endif %} diff --git a/blocks/pullquote.schema.json b/blocks/pullquote.schema.json deleted file mode 100644 index cd14338..0000000 --- a/blocks/pullquote.schema.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Pull Quote", - "description": "Large oxblood pull quote rendered in Playfair Display italic with optional attribution.", - "type": "object", - "properties": { - "quote": { - "type": "string", - "title": "Quote", - "description": "The pull quote text. Rich text is permitted so authors can emphasise inline.", - "x-editor": "richtext" - }, - "attribution": { - "type": "string", - "title": "Attribution", - "description": "Optional source attribution rendered in small caps below the quote.", - "x-editor": "text" - } - } -} diff --git a/blocks/reading_list.ninjatpl b/blocks/reading_list.ninjatpl new file mode 100644 index 0000000..f61caaf --- /dev/null +++ b/blocks/reading_list.ninjatpl @@ -0,0 +1,16 @@ +{# reading_list (editorial) — curated further-reading list: small-caps heading over hairline-ruled entries with optional source + note. Fields: title, intro, items[].label/url/source/note. #} +
+ {% if title %}{% endif %} + {% if intro %}

{{ intro }}

{% endif %} + {% if items %} +
    + {% for item in items %} +
  • + {% if item.url %}{{ item.label }}{% else %}{{ item.label }}{% endif %} + {% if item.source %}{{ item.source }}{% endif %} + {% if item.note %}

    {{ item.note }}

    {% endif %} +
  • + {% endfor %} +
+ {% endif %} +
diff --git a/blocks/reading_list.schema.json b/blocks/reading_list.schema.json new file mode 100644 index 0000000..7fa59cc --- /dev/null +++ b/blocks/reading_list.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Reading List", + "description": "A curated further-reading list rendered under a small-caps heading, each entry a hairline-ruled link with an optional note and source.", + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Heading", + "description": "Small-caps section heading (e.g. \"Further reading\").", + "x-editor": "text", + "default": "Further reading" + }, + "intro": { + "type": "string", + "title": "Intro", + "description": "Optional short line of context above the list.", + "x-editor": "textarea" + }, + "items": { + "type": "array", + "title": "Entries", + "description": "The reading-list entries.", + "default": [], + "x-editor": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Title", + "description": "The title of the piece.", + "x-editor": "text" + }, + "url": { + "type": "string", + "title": "URL", + "description": "Where the entry links to.", + "x-editor": "link" + }, + "source": { + "type": "string", + "title": "Source", + "description": "Optional author or publication, rendered in small caps.", + "x-editor": "text" + }, + "note": { + "type": "string", + "title": "Note", + "description": "Optional one-line note on why it is worth reading.", + "x-editor": "text" + } + } + } + } + } +} diff --git a/blocks/section_label.ninjatpl b/blocks/section_label.ninjatpl deleted file mode 100644 index 2bb3c9f..0000000 --- a/blocks/section_label.ninjatpl +++ /dev/null @@ -1 +0,0 @@ -{% if label %}
{% if divider != "false" and divider != false %}{% endif %}
{% endif %} diff --git a/blocks/section_label.schema.json b/blocks/section_label.schema.json deleted file mode 100644 index b3aacba..0000000 --- a/blocks/section_label.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Section Label", - "description": "Small-caps section label followed by an optional hairline divider rule.", - "type": "object", - "properties": { - "label": { - "type": "string", - "title": "Label", - "description": "Section label text. Rendered uppercase with small-caps.", - "x-editor": "text" - }, - "divider": { - "type": "string", - "title": "Divider", - "description": "Whether to render a hairline rule beneath the label.", - "x-editor": "select", - "enum": ["true", "false"], - "default": "true" - } - } -} diff --git a/fonts.json b/fonts.json index fe51488..9130534 100644 --- a/fonts.json +++ b/fonts.json @@ -1 +1,18 @@ -[] +[ + { + "name": "Playfair Display", + "family": "Playfair Display", + "variants": [ + { "weight": "700", "style": "normal", "file": "fonts/playfair-display-700.woff2" }, + { "weight": "900", "style": "italic", "file": "fonts/playfair-display-900-italic.woff2" } + ] + }, + { + "name": "Source Serif 4", + "family": "Source Serif 4", + "variants": [ + { "weight": "400", "style": "normal", "file": "fonts/source-serif-4-400.woff2" }, + { "weight": "400", "style": "italic", "file": "fonts/source-serif-4-400-italic.woff2" } + ] + } +] diff --git a/manifest.yaml b/manifest.yaml index ba51251..1448a11 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -1,5 +1,10 @@ -# Editorial theme — codeless .bnp manifest (WO-WZ-021 Phase B). +# Editorial theme — codeless .bnp manifest (Theme Fleet Wave A, WO-TF-017). # Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm). +# +# Persona: a personal essayist / independent newsletter. A reading experience +# above all — serif baseline grid, drop caps, running marginalia, small-caps +# section labels, author byline foregrounded. Typography does the design work; +# ornament is limited to hairline rules and generous whitespace. theme_presets: presets.json bundled_fonts: fonts.json master_pages: master_pages.json @@ -7,378 +12,485 @@ master_pages: master_pages.json system_templates: - key: editorial title: Editorial - description: "Didone display type, narrow measure, and hairline rules for newsrooms, longform writers, and opinion columnists." + description: "A personal essayist's reading room: serif baseline grid, drop caps, running marginalia and small-caps labels. Built for longform writers and independent newsletters." page_templates: - system: editorial key: default title: Default - description: "Standard page with masthead, narrow main column, footer." + description: "Standard page: masthead, narrow reading column, colophon." + slots: [header, main, footer] + - system: editorial + key: full-width + title: Full Width + description: "Edge-to-edge for photo essays and cover images." slots: [header, main, footer] - system: editorial key: landing - title: Section Front - description: "Front-page-style stack: masthead, lead story, river, footer." + title: Front Page + description: "Front-page stack: masthead, lead essay, river of stories, colophon." slots: [masthead, lead, river, footer] - system: editorial key: article title: Article - description: "Single-column long read with byline, drop cap, marginalia rail." + description: "The reading centrepiece: byline, drop-cap opener, single column with a marginalia rail." slots: [header, byline, main, marginalia, footer] - system: editorial - key: full-width - title: Full Width - description: "Edge-to-edge for photo essays and data graphics." - slots: [header, main, footer] + key: blog-index + title: Archive + description: "Essay archive: masthead, featured essay, and a river of entries." + slots: [header, featured, main, footer] + - system: editorial + key: contact + title: Correspondence + description: "Contact layout with an optional aside for hours and location." + slots: [header, main, aside, footer] + - system: editorial + key: auth + title: Members + description: "Centred members shell with wordmark and hairline rule." + slots: [main, footer] template_overrides: + # Chrome + - { template: editorial, block: navbar } + - { template: editorial, block: footer } + - { template: editorial, block: utility-bar } + - { template: editorial, block: announcement-bar } + # Marketing + - { template: editorial, block: hero } + - { template: editorial, block: feature-grid } + - { template: editorial, block: rich-section } + - { template: editorial, block: stats } + - { template: editorial, block: testimonial } + - { template: editorial, block: logo-strip } + - { template: editorial, block: pricing } + - { template: editorial, block: team } + - { template: editorial, block: faq } + - { template: editorial, block: timeline } + - { template: editorial, block: cta } + - { template: editorial, block: countdown } + # Primitives - { template: editorial, block: heading } - { template: editorial, block: text } - - { template: editorial, block: button } + - { template: editorial, block: rich-text } - { template: editorial, block: image } + - { template: editorial, block: quote } + - { template: editorial, block: divider } + - { template: editorial, block: button } + - { template: editorial, block: card } + - { template: editorial, block: gallery } + - { template: editorial, block: video-embed } + - { template: editorial, block: audio-embed } + # Commerce / local + - { template: editorial, block: menu-list } + - { template: editorial, block: event-list } + - { template: editorial, block: contact-form } + - { template: editorial, block: contact-card } + - { template: editorial, block: hours-location } + - { template: editorial, block: social-links } + # Blog family + - { template: editorial, block: blog-hero } + - { template: editorial, block: featured-posts } + - { template: editorial, block: related-posts } + - { template: editorial, block: popular-posts } + - { template: editorial, block: category-list } + - { template: editorial, block: post-hero } + - { template: editorial, block: post-metadata } + - { template: editorial, block: author-bio } + - { template: editorial, block: author-bio-hero } + - { template: editorial, block: newsletter-signup } + - { template: editorial, block: breadcrumbs } + - { template: editorial, block: sidebar-nav } + # Auth surface + - { template: editorial, block: auth-form } + - { template: editorial, block: password-reset-form } + - { template: editorial, block: auth-status } + # System + - { template: editorial, block: page-suggestions } email_wrappers: - editorial css: input_css_append: | - /* === Editorial theme — utility layer === */ + /* === Editorial theme — utility layer === + * + * Personal-essayist identity: hairline rules, small-caps labels, drop caps, + * marginalia, narrow reading measure. Colour is ALWAYS consumed via + * hsl(var(--token)); font families ALWAYS via var(--font-heading|body|mono, + * ). No literal colours, no hardcoded families. + */ - :root { - --editorial-measure: 64ch; - --editorial-rule: 1px solid hsl(var(--border)); - } - - /* --- Narrow editorial column with indented continuation paragraphs --- */ - - .prose-editorial { - max-width: var(--editorial-measure); - margin-left: auto; - margin-right: auto; - font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif); - color: hsl(var(--foreground)); - line-height: 1.65; - } - - .prose-editorial p { - margin: 0 0 1em 0; - text-indent: 0; - } - - .prose-editorial p + p { - text-indent: 1.5em; - } - - .prose-editorial h1, - .prose-editorial h2, - .prose-editorial h3, - .prose-editorial h4 { - font-family: var(--font-heading, "Playfair Display", Georgia, serif); - font-weight: 700; - letter-spacing: -0.01em; - color: hsl(var(--foreground)); - } - - .prose-editorial a { - color: hsl(var(--accent)); - text-decoration: underline; - text-decoration-thickness: 1px; - text-underline-offset: 2px; - } - - /* --- Drop cap — oxblood accent first letter, ≥ 3x body size --- */ - - .editorial-dropcap p:first-of-type::first-letter { - font-family: var(--font-heading, "Playfair Display", Georgia, serif); - font-weight: 900; - font-style: italic; - color: hsl(var(--accent)); - float: left; - font-size: 4.5em; - line-height: 0.85; - padding-right: 0.12em; - padding-top: 0.05em; - margin-bottom: -0.1em; - } - - /* --- Marginalia rail (right-side italic notes) --- */ - - .editorial-marginalia { - font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif); - font-style: italic; - font-size: 12px; - line-height: 1.5; - color: hsl(var(--muted-foreground)); - } - - .editorial-marginalia aside { - margin: 0 0 1.5rem 0; - padding-left: 0.75rem; - border-left: var(--editorial-rule); - } - - @media (max-width: 768px) { - .editorial-marginalia { - margin-top: 1.5rem; - border-top: var(--editorial-rule); - padding-top: 1rem; + @layer utilities { + :root { + --editorial-measure: 64ch; + --editorial-rule: 1px solid hsl(var(--border)); } - } - /* --- Masthead wordmark --- */ - - .editorial-masthead-wordmark { - font-family: var(--font-heading, "Playfair Display", Georgia, serif); - font-weight: 900; - font-style: italic; - letter-spacing: -0.02em; - color: hsl(var(--foreground)); - } - - .editorial-masthead-kicker { - font-family: var(--font-mono, "JetBrains Mono", "SF Mono", ui-monospace, monospace); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.15em; - color: hsl(var(--muted-foreground)); - } - - .editorial-masthead-rule { - border: 0; - border-top: var(--editorial-rule); - margin: 0.5rem 0 0 0; - } - - /* --- Pull quote — Playfair italic in oxblood --- */ - - .editorial-pullquote { - font-family: var(--font-heading, "Playfair Display", Georgia, serif); - font-style: italic; - font-size: 38px; - line-height: 1.2; - color: hsl(var(--accent)); - margin: 2.5rem auto; - max-width: var(--editorial-measure); - border: none; - } - - .editorial-pullquote-attribution { - display: block; - margin-top: 1rem; - font-family: var(--font-body, "Source Serif 4", Georgia, serif); - font-style: normal; - font-size: 14px; - text-transform: uppercase; - letter-spacing: 0.12em; - color: hsl(var(--muted-foreground)); - } - - /* --- Section label — small-caps with hairline rule --- */ - - .editorial-section-label { - font-family: var(--font-body, "Source Serif 4", Georgia, serif); - text-transform: uppercase; - font-variant: small-caps; - font-feature-settings: "smcp"; - letter-spacing: 0.15em; - font-size: 13px; - color: hsl(var(--foreground)); - } - - .editorial-section-label-rule { - border: 0; - border-top: var(--editorial-rule); - margin: 0.5rem 0 1.5rem 0; - } - - /* --- Hairline rules --- */ - - hr.editorial-rule, - .editorial-hairline { - border: 0; - border-top: var(--editorial-rule); - margin: 1.5rem 0; - } - - /* --- Byline --- */ - - .editorial-byline { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 0; - border-top: var(--editorial-rule); - border-bottom: var(--editorial-rule); - } - - .editorial-byline-photo { - width: 36px; - height: 36px; - border-radius: 50%; - background-color: hsl(var(--muted)); - flex-shrink: 0; - object-fit: cover; - } - - .editorial-byline-author { - font-family: var(--font-mono, "JetBrains Mono", monospace), var(--font-body, "Source Serif 4", Georgia, serif); - /* The spec calls for Inter for UI bylines. Fallback to Inter then system. */ - font-family: var(--font-body, "Inter", -apple-system, sans-serif); - font-weight: 500; - font-size: 15px; - color: hsl(var(--foreground)); - } - - .editorial-byline-meta { - font-family: var(--font-body, "Inter", -apple-system, sans-serif); - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.1em; - color: hsl(var(--muted-foreground)); - } - - /* --- Override: heading (Playfair, tight tracking, optional small-caps kicker) --- */ - - .editorial-heading { - font-family: var(--font-heading, "Playfair Display", Georgia, serif); - font-weight: 700; - letter-spacing: -0.015em; - color: hsl(var(--foreground)); - line-height: 1.15; - } - - .editorial-heading-kicker { - display: block; - margin-bottom: 0.5rem; - font-family: var(--font-body, "Inter", -apple-system, sans-serif); - font-size: 12px; - text-transform: uppercase; - font-variant: small-caps; - font-feature-settings: "smcp"; - letter-spacing: 0.18em; - color: hsl(var(--muted-foreground)); - } - - /* --- Override: text body (Source Serif, hanging quotes) --- */ - - .editorial-text { - font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif); - color: hsl(var(--foreground)); - line-height: 1.7; - max-width: var(--editorial-measure); - } - - .editorial-text p + p { - text-indent: 1.5em; - margin-top: 0; - } - - .editorial-text blockquote { - hanging-punctuation: first; - font-style: italic; - border-left: var(--editorial-rule); - padding-left: 1rem; - margin-left: 0; - } - - /* --- Override: button (hairline border, no fill, accent on hover) --- */ - - .editorial-button { - display: inline-block; - padding: 0.5rem 1rem; - font-family: var(--font-body, "Inter", -apple-system, sans-serif); - font-size: 13px; - text-transform: uppercase; - letter-spacing: 0.12em; - background-color: transparent; - color: hsl(var(--foreground)); - border: 1px solid hsl(var(--border)); - text-decoration: none; - cursor: pointer; - transition: color 150ms ease, border-color 150ms ease; - } - - .editorial-button:hover, - .editorial-button:focus { - color: hsl(var(--accent)); - border-color: hsl(var(--accent)); - } - - .editorial-button:focus-visible { - outline: 2px solid hsl(var(--ring)); - outline-offset: 2px; - } - - /* --- Override: image (Source Serif italic caption with hairline above) --- */ - - .editorial-figure { - margin: 1.5rem 0; - } - - .editorial-figure img { - display: block; - width: 100%; - height: auto; - } - - .editorial-figure figcaption { - margin-top: 0.75rem; - padding-top: 0.5rem; - border-top: var(--editorial-rule); - font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif); - font-style: italic; - font-size: 14px; - color: hsl(var(--muted-foreground)); - } - - /* --- Colophon --- */ - - .editorial-colophon { - border-top: var(--editorial-rule); - padding-top: 1.5rem; - font-family: var(--font-body, "Inter", -apple-system, sans-serif); - font-size: 13px; - color: hsl(var(--muted-foreground)); - } - - .editorial-colophon-issn { - font-family: var(--font-mono, "JetBrains Mono", monospace); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.1em; - } - - /* --- Page layout — narrow editorial column --- */ - - .editorial-column { - max-width: var(--editorial-measure); - margin-left: auto; - margin-right: auto; - padding-left: 1rem; - padding-right: 1rem; - } - - .editorial-wide { - max-width: 96rem; - margin-left: auto; - margin-right: auto; - padding-left: 1rem; - padding-right: 1rem; - } - - .editorial-article-grid { - display: grid; - grid-template-columns: 1fr; - gap: 1.5rem; - max-width: 80rem; - margin-left: auto; - margin-right: auto; - padding-left: 1rem; - padding-right: 1rem; - } - - @media (min-width: 1024px) { + /* --- Reading column + page widths --- */ + .editorial-column { + max-width: var(--editorial-measure); + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + } + .editorial-wide { + max-width: 72rem; + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + } .editorial-article-grid { - grid-template-columns: minmax(0, 64ch) 16rem; + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + max-width: 80rem; + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + } + @media (min-width: 1024px) { + .editorial-article-grid { + grid-template-columns: minmax(0, 64ch) 16rem; + } + } + + /* --- Narrow prose column with indented continuation paragraphs --- */ + .prose-editorial { + max-width: var(--editorial-measure); + margin-left: auto; + margin-right: auto; + font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif); + color: hsl(var(--foreground)); + line-height: 1.7; + } + .prose-editorial p { margin: 0 0 1em 0; } + .prose-editorial p + p { text-indent: 1.5em; } + .prose-editorial h1, + .prose-editorial h2, + .prose-editorial h3, + .prose-editorial h4 { + font-family: var(--font-heading, "Playfair Display", Georgia, serif); + font-weight: 700; + letter-spacing: -0.01em; + color: hsl(var(--foreground)); + } + .prose-editorial a { + color: hsl(var(--accent)); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; + } + .prose-editorial blockquote { + hanging-punctuation: first; + font-style: italic; + border-left: var(--editorial-rule); + padding-left: 1rem; + margin-left: 0; + color: hsl(var(--muted-foreground)); + } + + /* --- Small-caps / tracked labels (kickers, eyebrows, meta) --- */ + .editorial-caps { + font-family: var(--font-body, "Source Serif 4", Georgia, serif); + text-transform: uppercase; + letter-spacing: 0.16em; + font-size: 0.72rem; + line-height: 1.4; + } + .editorial-meta { + font-family: var(--font-body, "Inter", -apple-system, sans-serif); + text-transform: uppercase; + letter-spacing: 0.1em; + font-size: 0.72rem; + color: hsl(var(--muted-foreground)); + } + .editorial-section-label { + font-family: var(--font-body, "Source Serif 4", Georgia, serif); + text-transform: uppercase; + font-variant: small-caps; + font-feature-settings: "smcp"; + letter-spacing: 0.15em; + font-size: 0.8rem; + color: hsl(var(--foreground)); + } + .editorial-section-label-rule { + border: 0; + border-top: var(--editorial-rule); + margin: 0.5rem 0 1.5rem 0; + } + + /* --- Hairline rules --- */ + hr.editorial-rule, + .editorial-rule, + .editorial-hairline { + border: 0; + border-top: var(--editorial-rule); + margin: 0; + } + + /* --- Heading (Playfair, tight tracking, optional kicker) --- */ + .editorial-heading { + font-family: var(--font-heading, "Playfair Display", Georgia, serif); + font-weight: 700; + letter-spacing: -0.015em; + color: hsl(var(--foreground)); + line-height: 1.15; + } + .editorial-heading-kicker { + display: block; + margin-bottom: 0.5rem; + font-family: var(--font-body, "Inter", -apple-system, sans-serif); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.18em; + color: hsl(var(--muted-foreground)); + } + .editorial-underline { + position: relative; + padding-bottom: 0.35em; + } + .editorial-underline::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 1px; + background: hsl(var(--border)); + } + + /* --- Body text override --- */ + .editorial-text { + font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif); + color: hsl(var(--foreground)); + line-height: 1.7; + } + .editorial-text p + p { text-indent: 1.5em; margin-top: 0; } + + /* --- Inline accent link --- */ + .editorial-link { + color: hsl(var(--accent)); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + transition: color 150ms ease, text-decoration-color 150ms ease; + } + .editorial-link:hover, + .editorial-link:focus-visible { color: hsl(var(--foreground)); } + + /* --- Button (hairline border, no fill, accent on hover) --- */ + .editorial-button { + display: inline-block; + padding: 0.55rem 1.1rem; + font-family: var(--font-body, "Inter", -apple-system, sans-serif); + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.14em; + background-color: transparent; + color: hsl(var(--foreground)); + border: 1px solid hsl(var(--border)); + text-decoration: none; + cursor: pointer; + transition: color 150ms ease, border-color 150ms ease; + } + .editorial-button:hover, + .editorial-button:focus { + color: hsl(var(--accent)); + border-color: hsl(var(--accent)); + } + .editorial-button:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 2px; + } + + /* --- Panel / card (hairline frame, no shadow) --- */ + .editorial-panel, + .editorial-card { + border: 1px solid hsl(var(--border)); + background-color: hsl(var(--card)); + transition: border-color 150ms ease; + } + .editorial-panel:hover, + .editorial-card:hover { border-color: hsl(var(--accent)); } + + /* --- Form field (hairline border, accent focus) --- */ + .editorial-input { + width: 100%; + border: 1px solid hsl(var(--input)); + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); + padding: 0.55rem 0.75rem; + font-family: var(--font-body, "Source Serif 4", Georgia, serif); + font-size: 0.95rem; + } + .editorial-input::placeholder { color: hsl(var(--muted-foreground)); } + .editorial-input:focus { + outline: none; + border-color: hsl(var(--accent)); + box-shadow: 0 0 0 1px hsl(var(--accent)); + } + + /* --- Figure / image caption --- */ + .editorial-figure { margin: 1.5rem 0; } + .editorial-figure img { display: block; width: 100%; height: auto; } + .editorial-figure figcaption { + margin-top: 0.75rem; + padding-top: 0.5rem; + border-top: var(--editorial-rule); + font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif); + font-style: italic; + font-size: 0.85rem; + color: hsl(var(--muted-foreground)); + } + + /* --- Drop cap — oxblood accent first letter --- */ + .editorial-dropcap p:first-of-type::first-letter { + font-family: var(--font-heading, "Playfair Display", Georgia, serif); + font-weight: 900; + font-style: italic; + color: hsl(var(--accent)); + float: left; + font-size: 4.5em; + line-height: 0.85; + padding-right: 0.12em; + padding-top: 0.05em; + margin-bottom: -0.1em; + } + + /* --- Marginalia rail (right-side italic notes) --- */ + .editorial-marginalia { + font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif); + font-style: italic; + font-size: 0.8rem; + line-height: 1.5; + color: hsl(var(--muted-foreground)); + } + .editorial-marginalia aside { + margin: 0 0 1.5rem 0; + padding-left: 0.75rem; + border-left: var(--editorial-rule); + opacity: 0; + animation: editorial-fade-in 600ms ease forwards; + } + @keyframes editorial-fade-in { to { opacity: 1; } } + @media (prefers-reduced-motion: reduce) { + .editorial-marginalia aside { animation: none; opacity: 1; } + } + @media (max-width: 768px) { + .editorial-marginalia { + margin-top: 1.5rem; + border-top: var(--editorial-rule); + padding-top: 1rem; + } + } + + /* --- Masthead wordmark --- */ + .editorial-masthead-wordmark { + font-family: var(--font-heading, "Playfair Display", Georgia, serif); + font-weight: 900; + font-style: italic; + letter-spacing: -0.02em; + color: hsl(var(--foreground)); + } + .editorial-masthead-kicker { + font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: hsl(var(--muted-foreground)); + } + .editorial-masthead-rule { + border: 0; + border-top: var(--editorial-rule); + margin: 0.5rem 0 0 0; + } + + /* --- Pull quote — Playfair italic in oxblood --- */ + .editorial-pullquote { + font-family: var(--font-heading, "Playfair Display", Georgia, serif); + font-style: italic; + font-size: 2rem; + line-height: 1.2; + color: hsl(var(--accent)); + margin: 2.5rem auto; + max-width: var(--editorial-measure); + border: none; + } + .editorial-pullquote-attribution { + display: block; + margin-top: 1rem; + font-family: var(--font-body, "Source Serif 4", Georgia, serif); + font-style: normal; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: hsl(var(--muted-foreground)); + } + + /* --- Byline --- */ + .editorial-byline { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 0; + border-top: var(--editorial-rule); + border-bottom: var(--editorial-rule); + } + .editorial-byline-photo { + width: 36px; + height: 36px; + border-radius: 50%; + background-color: hsl(var(--muted)); + flex-shrink: 0; + object-fit: cover; + } + .editorial-byline-author { + font-family: var(--font-body, "Inter", -apple-system, sans-serif); + font-weight: 500; + font-size: 0.95rem; + color: hsl(var(--foreground)); + } + .editorial-byline-meta { + font-family: var(--font-body, "Inter", -apple-system, sans-serif); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: hsl(var(--muted-foreground)); + } + + /* --- Colophon --- */ + .editorial-colophon { + border-top: var(--editorial-rule); + padding-top: 1.5rem; + font-family: var(--font-body, "Inter", -apple-system, sans-serif); + font-size: 0.8rem; + color: hsl(var(--muted-foreground)); + } + .editorial-colophon-issn { + font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.1em; + } + + /* --- Reading-progress underline (CSS-only atmosphere) --- */ + .editorial-progress { + position: sticky; + top: 0; + height: 2px; + background: hsl(var(--accent)); + transform-origin: 0 50%; + animation: editorial-progress linear both; + animation-timeline: scroll(root block); + z-index: 50; + } + @keyframes editorial-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } } + @media (prefers-reduced-motion: reduce) { + .editorial-progress { animation: none; transform: scaleX(0); } } } diff --git a/master_pages.json b/master_pages.json index 54fb9e2..8e9761e 100644 --- a/master_pages.json +++ b/master_pages.json @@ -2,12 +2,24 @@ { "key": "editorial:default-master", "title": "Editorial Default Master", - "page_templates": ["default", "landing", "full-width"], + "page_templates": ["default", "full-width", "blog-index", "contact"], "blocks": [ - { "block_key": "editorial:masthead", "title": "Masthead", "content": { "menuName": "main", "kicker": "EST. 2026" }, "slot": "header", "sort_order": 0 }, - { "block_key": "navbar", "title": "Section Nav", "content": { "menuName": "sections" }, "slot": "header", "sort_order": 1 }, + { "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "The Margin" } }, "slot": "header", "sort_order": 0 }, + { "block_key": "slot", "title": "Featured", "content": { "slotName": "featured" }, "slot": "featured", "sort_order": 0 }, { "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 }, - { "block_key": "editorial:colophon", "title": "Colophon", "content": { "showSignup": true, "issn": "0000-0000" }, "slot": "footer", "sort_order": 0 } + { "block_key": "slot", "title": "Aside", "content": { "slotName": "aside" }, "slot": "aside", "sort_order": 0 }, + { "block_key": "footer", "title": "Colophon", "content": { "layout": "centered", "companyName": "The Margin", "tagline": "A newsletter about paying attention.", "showNewsletter": true, "newsletterTitle": "Subscribe", "newsletterDescription": "New essays, every other Sunday.", "copyright": "The Margin" }, "slot": "footer", "sort_order": 0 } + ] + }, + { + "key": "editorial:landing-master", + "title": "Editorial Front Page Master", + "page_templates": ["landing"], + "blocks": [ + { "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "The Margin" } }, "slot": "masthead", "sort_order": 0 }, + { "block_key": "slot", "title": "Lead", "content": { "slotName": "lead", "placeholder": "Lead essay" }, "slot": "lead", "sort_order": 0 }, + { "block_key": "slot", "title": "River", "content": { "slotName": "river", "placeholder": "Recent essays" }, "slot": "river", "sort_order": 0 }, + { "block_key": "footer", "title": "Colophon", "content": { "layout": "centered", "companyName": "The Margin", "tagline": "A newsletter about paying attention.", "showNewsletter": true, "newsletterTitle": "Subscribe", "newsletterDescription": "New essays, every other Sunday.", "copyright": "The Margin" }, "slot": "footer", "sort_order": 0 } ] }, { @@ -15,11 +27,11 @@ "title": "Editorial Article Master", "page_templates": ["article"], "blocks": [ - { "block_key": "editorial:masthead", "title": "Masthead", "content": { "menuName": "main", "compact": true }, "slot": "header", "sort_order": 0 }, - { "block_key": "editorial:byline", "title": "Byline", "content": { "showPhoto": true, "showReadTime": true }, "slot": "byline", "sort_order": 0 }, - { "block_key": "slot", "title": "Article Body", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }, - { "block_key": "editorial:marginalia", "title": "Marginalia Rail", "content": { "items": [] }, "slot": "marginalia", "sort_order": 0 }, - { "block_key": "editorial:colophon", "title": "Colophon", "content": { "showSignup": true }, "slot": "footer", "sort_order": 0 } + { "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "The Margin" } }, "slot": "header", "sort_order": 0 }, + { "block_key": "slot", "title": "Byline", "content": { "slotName": "byline" }, "slot": "byline", "sort_order": 0 }, + { "block_key": "slot", "title": "Article Body", "content": { "slotName": "main", "placeholder": "Essay body" }, "slot": "main", "sort_order": 0 }, + { "block_key": "slot", "title": "Marginalia Rail", "content": { "slotName": "marginalia" }, "slot": "marginalia", "sort_order": 0 }, + { "block_key": "footer", "title": "Colophon", "content": { "layout": "centered", "companyName": "The Margin", "tagline": "A newsletter about paying attention.", "copyright": "The Margin" }, "slot": "footer", "sort_order": 0 } ] } ] diff --git a/plugin.mod b/plugin.mod index 6f38ff4..6d16cce 100644 --- a/plugin.mod +++ b/plugin.mod @@ -3,7 +3,8 @@ name = "editorial" display_name = "Editorial" scope = "@themes" version = "0.2.0" -description = "Didone display type, narrow measure, and hairline rules for newsrooms, longform writers, and opinion columnists." +description = "A personal essayist's reading room: warm book-paper light, sepia-on-charcoal dark, serif baseline grid, drop caps, running marginalia and small-caps labels for longform writers and independent newsletters." kind = "theme" categories = ["templates"] -tags = ["editorial", "serif", "news", "longform", "magazine", "law", "finance", "opinion"] +tags = ["editorial", "serif", "essay", "newsletter", "longform", "reading", "typographic", "blog", "writer"] +required_icon_packs = ["lucide"] diff --git a/presets.json b/presets.json index 69151d9..c544a57 100644 --- a/presets.json +++ b/presets.json @@ -1,88 +1,201 @@ [ { - "id": "broadsheet", - "name": "Broadsheet", - "description": "Default light: cream paper, ink black, oxblood accent. Newsroom front page.", + "id": "book-paper", + "name": "Book Paper", + "description": "The default. Warm off-white paper and dark serif ink by day, low-glare charcoal with sepia type by night. Oxblood accent throughout.", "theme": { - "mode": "light", + "mode": "both", "lightColors": { - "background": "36 30% 96%", - "foreground": "20 14% 10%", - "card": "36 30% 98%", - "cardForeground": "20 14% 10%", - "popover": "36 30% 98%", - "popoverForeground": "20 14% 10%", - "primary": "20 14% 10%", - "primaryForeground": "36 30% 96%", - "secondary": "36 20% 90%", - "secondaryForeground": "20 14% 10%", - "muted": "36 20% 92%", - "mutedForeground": "20 10% 38%", - "accent": "0 55% 28%", - "accentForeground": "36 30% 96%", - "destructive": "0 72% 42%", - "destructiveForeground": "36 30% 96%", - "border": "30 12% 82%", - "input": "30 12% 82%", - "ring": "0 55% 28%" + "background": "38 32% 95%", + "foreground": "24 16% 12%", + "card": "38 34% 97%", + "cardForeground": "24 16% 12%", + "popover": "38 34% 97%", + "popoverForeground": "24 16% 12%", + "primary": "24 16% 12%", + "primaryForeground": "38 32% 95%", + "secondary": "36 22% 89%", + "secondaryForeground": "24 16% 14%", + "muted": "36 22% 91%", + "mutedForeground": "26 10% 38%", + "accent": "3 58% 34%", + "accentForeground": "38 32% 95%", + "destructive": "0 70% 42%", + "destructiveForeground": "38 32% 95%", + "border": "32 16% 82%", + "input": "32 16% 80%", + "ring": "3 58% 34%" + }, + "darkColors": { + "background": "26 10% 10%", + "foreground": "36 22% 82%", + "card": "26 10% 13%", + "cardForeground": "36 22% 82%", + "popover": "26 10% 13%", + "popoverForeground": "36 22% 82%", + "primary": "36 22% 82%", + "primaryForeground": "26 10% 10%", + "secondary": "26 8% 18%", + "secondaryForeground": "36 20% 80%", + "muted": "26 8% 16%", + "mutedForeground": "34 12% 58%", + "accent": "6 62% 56%", + "accentForeground": "26 10% 10%", + "destructive": "0 68% 52%", + "destructiveForeground": "36 22% 88%", + "border": "28 8% 22%", + "input": "28 8% 20%", + "ring": "6 62% 56%" } } }, { - "id": "nightdesk", - "name": "Nightdesk", - "description": "Dark mode for after-hours reading. Inverted broadsheet with deeper accent.", + "id": "sepia-review", + "name": "Sepia Review", + "description": "Warm sepia in both modes for a soft, magazine-like read. Amber paper and umber ink by day, deep tobacco ground by night.", "theme": { - "mode": "dark", + "mode": "both", + "lightColors": { + "background": "34 44% 93%", + "foreground": "22 30% 16%", + "card": "34 46% 96%", + "cardForeground": "22 30% 16%", + "popover": "34 46% 96%", + "popoverForeground": "22 30% 16%", + "primary": "22 30% 16%", + "primaryForeground": "34 44% 93%", + "secondary": "32 30% 87%", + "secondaryForeground": "22 30% 18%", + "muted": "32 28% 89%", + "mutedForeground": "24 18% 40%", + "accent": "14 62% 38%", + "accentForeground": "34 44% 94%", + "destructive": "0 70% 42%", + "destructiveForeground": "34 44% 94%", + "border": "30 22% 78%", + "input": "30 22% 76%", + "ring": "14 62% 38%" + }, "darkColors": { - "background": "24 10% 8%", - "foreground": "36 20% 92%", - "card": "24 10% 11%", - "cardForeground": "36 20% 92%", - "popover": "24 10% 11%", - "popoverForeground": "36 20% 92%", - "primary": "36 20% 92%", - "primaryForeground": "24 10% 8%", - "secondary": "24 8% 16%", - "secondaryForeground": "36 20% 92%", - "muted": "24 8% 14%", - "mutedForeground": "36 12% 62%", - "accent": "0 60% 52%", - "accentForeground": "24 10% 8%", - "destructive": "0 72% 50%", - "destructiveForeground": "36 20% 92%", - "border": "24 8% 20%", - "input": "24 8% 18%", - "ring": "0 60% 52%" + "background": "24 18% 11%", + "foreground": "34 30% 80%", + "card": "24 18% 14%", + "cardForeground": "34 30% 80%", + "popover": "24 18% 14%", + "popoverForeground": "34 30% 80%", + "primary": "34 30% 80%", + "primaryForeground": "24 18% 11%", + "secondary": "24 14% 19%", + "secondaryForeground": "34 28% 78%", + "muted": "24 14% 17%", + "mutedForeground": "32 16% 60%", + "accent": "18 68% 54%", + "accentForeground": "24 18% 10%", + "destructive": "0 68% 52%", + "destructiveForeground": "34 30% 86%", + "border": "26 14% 23%", + "input": "26 14% 21%", + "ring": "18 68% 54%" } } }, { "id": "legal-pad", "name": "Legal Pad", - "description": "Warm cream for law and finance verticals. Tighter saturation, deeper ink.", + "description": "Deeper cream and near-black ink for law, finance and long essays. Tight saturation by day, cool ink-black night desk.", "theme": { - "mode": "light", + "mode": "both", "lightColors": { - "background": "48 40% 94%", - "foreground": "28 30% 14%", - "card": "48 40% 97%", - "cardForeground": "28 30% 14%", - "popover": "48 40% 97%", - "popoverForeground": "28 30% 14%", - "primary": "28 30% 14%", - "primaryForeground": "48 40% 94%", - "secondary": "44 30% 86%", - "secondaryForeground": "28 30% 14%", - "muted": "44 28% 90%", - "mutedForeground": "28 18% 36%", - "accent": "10 60% 30%", - "accentForeground": "48 40% 94%", - "destructive": "0 72% 42%", - "destructiveForeground": "48 40% 94%", - "border": "40 20% 78%", - "input": "40 20% 80%", - "ring": "10 60% 30%" + "background": "48 42% 93%", + "foreground": "30 28% 13%", + "card": "48 44% 96%", + "cardForeground": "30 28% 13%", + "popover": "48 44% 96%", + "popoverForeground": "30 28% 13%", + "primary": "30 28% 13%", + "primaryForeground": "48 42% 93%", + "secondary": "46 30% 86%", + "secondaryForeground": "30 28% 15%", + "muted": "46 28% 89%", + "mutedForeground": "30 16% 36%", + "accent": "10 58% 32%", + "accentForeground": "48 42% 94%", + "destructive": "0 70% 42%", + "destructiveForeground": "48 42% 94%", + "border": "42 22% 77%", + "input": "42 22% 75%", + "ring": "10 58% 32%" + }, + "darkColors": { + "background": "220 12% 9%", + "foreground": "44 20% 84%", + "card": "220 12% 12%", + "cardForeground": "44 20% 84%", + "popover": "220 12% 12%", + "popoverForeground": "44 20% 84%", + "primary": "44 20% 84%", + "primaryForeground": "220 12% 9%", + "secondary": "220 10% 17%", + "secondaryForeground": "44 18% 82%", + "muted": "220 10% 15%", + "mutedForeground": "40 12% 58%", + "accent": "8 64% 55%", + "accentForeground": "220 12% 9%", + "destructive": "0 68% 52%", + "destructiveForeground": "44 20% 88%", + "border": "220 10% 21%", + "input": "220 10% 19%", + "ring": "8 64% 55%" + } + } + }, + { + "id": "nightdesk", + "name": "Nightdesk", + "description": "Cooler neutral paper and a crisp after-hours reading mode. A brighter red accent for the late edition.", + "theme": { + "mode": "both", + "lightColors": { + "background": "40 20% 96%", + "foreground": "220 14% 14%", + "card": "40 22% 98%", + "cardForeground": "220 14% 14%", + "popover": "40 22% 98%", + "popoverForeground": "220 14% 14%", + "primary": "220 14% 14%", + "primaryForeground": "40 20% 96%", + "secondary": "40 14% 90%", + "secondaryForeground": "220 14% 16%", + "muted": "40 14% 92%", + "mutedForeground": "220 8% 40%", + "accent": "352 60% 42%", + "accentForeground": "40 20% 96%", + "destructive": "0 72% 44%", + "destructiveForeground": "40 20% 96%", + "border": "36 12% 83%", + "input": "36 12% 81%", + "ring": "352 60% 42%" + }, + "darkColors": { + "background": "222 16% 9%", + "foreground": "40 16% 86%", + "card": "222 16% 12%", + "cardForeground": "40 16% 86%", + "popover": "222 16% 12%", + "popoverForeground": "40 16% 86%", + "primary": "40 16% 86%", + "primaryForeground": "222 16% 9%", + "secondary": "222 12% 17%", + "secondaryForeground": "40 14% 84%", + "muted": "222 12% 15%", + "mutedForeground": "40 10% 60%", + "accent": "352 70% 58%", + "accentForeground": "222 16% 9%", + "destructive": "0 70% 54%", + "destructiveForeground": "40 16% 90%", + "border": "222 12% 22%", + "input": "222 12% 20%", + "ring": "352 70% 58%" } } } diff --git a/seed/seed.json b/seed/seed.json new file mode 100644 index 0000000..df336f4 --- /dev/null +++ b/seed/seed.json @@ -0,0 +1,196 @@ +{ + "demo": true, + "settings": { + "merge": { + "companyName": "The Margin", + "tagline": "A newsletter about paying attention." + } + }, + "data_tables": [ + { + "key": "contact_submissions", + "name": "Letters", + "description": "Seeded contact-form submissions for the The Margin 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": "subject", "label": "Subject", "type": "text", "required": false }, + { "key": "message", "label": "Message", "type": "text", "required": true } + ] + } + ], + "pages": [ + { + "slug": "/", + "title": "The Margin", + "template_key": "landing", + "blocks": [ + { + "block_key": "hero", + "title": "Lead", + "slot": "lead", + "sort_order": 1, + "content": { + "template": "centered", + "eyebrow": { "text": "Issue 12" }, + "headline": { "text": "The Margin" }, + "subheadline": { "text": "A newsletter about paying attention." }, + "description": { "text": "Essays on reading, attention, and the slow work of noticing. New writing lands every other Sunday, free to your inbox." }, + "ctaPrimary": { "text": "Read the latest", "url": "/blog", "style": "primary" }, + "ctaSecondary": { "text": "About the writer", "url": "/about", "style": "outline" } + } + }, + { + "block_key": "heading", + "title": "Recent heading", + "slot": "river", + "sort_order": 1, + "content": { "text": "Recent essays", "level": 2 } + }, + { + "block_key": "card", + "title": "Essay: On Attention", + "slot": "river", + "sort_order": 2, + "content": { "layout": "vertical", "title": "On attention", "text": "

What it costs to look at one thing for a long time, and what it gives back.

", "link": "/blog/on-attention", "linkLabel": "Read the essay" } + }, + { + "block_key": "card", + "title": "Essay: The Slow Read", + "slot": "river", + "sort_order": 3, + "content": { "layout": "vertical", "title": "The slow read", "text": "

A case for reading less, and reading it twice.

", "link": "/blog/the-slow-read", "linkLabel": "Read the essay" } + }, + { + "block_key": "card", + "title": "Essay: Notes on Noticing", + "slot": "river", + "sort_order": 4, + "content": { "layout": "vertical", "title": "Notes on noticing", "text": "

Small habits that keep the world from going quiet.

", "link": "/blog/notes-on-noticing", "linkLabel": "Read the essay" } + }, + { + "block_key": "newsletter-signup", + "title": "Subscribe", + "slot": "river", + "sort_order": 5, + "content": { "title": "Subscribe", "description": "New essays every other Sunday. No noise, no schedule creep.", "listSlug": "main", "source": "home", "submitText": "Subscribe", "successMessage": "You are on the list. Look for the next issue on Sunday.", "placeholder": "you@example.com", "layout": "card" } + } + ] + }, + { + "slug": "/about", + "title": "About", + "template_key": "default", + "blocks": [ + { "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "About", "level": 1 } }, + { "block_key": "rich-text", "title": "Bio", "slot": "main", "sort_order": 2, "content": { "html": "

The Margin is a newsletter by Frances Hale. It is about reading, attention, and the slow work of noticing.

I write one essay every other week. Some are short. Some take the long way around. All of them start in the margins of a book.

You can read the archive for free. Members get the full run and the occasional letter.

" } }, + { "block_key": "editorial:reading_list", "title": "Shelf", "slot": "main", "sort_order": 3, "content": { "title": "On the shelf", "intro": "Books I keep going back to.", "items": [ { "label": "The Writing Life", "source": "Annie Dillard", "note": "On the daily cost of the work." }, { "label": "Ways of Seeing", "source": "John Berger", "note": "How looking is never neutral." }, { "label": "The Art of Stillness", "source": "Pico Iyer", "note": "A short book about going nowhere." } ] } }, + { "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 4, "content": { "style": "block", "align": "center", "quote": "Attention is the beginning of devotion.", "attribution": "Mary Oliver" } } + ] + }, + { + "slug": "/blog", + "title": "The archive", + "template_key": "blog-index", + "blocks": [ + { "block_key": "card", "title": "Featured: On Attention", "slot": "featured", "sort_order": 1, "content": { "layout": "horizontal", "title": "On attention", "text": "

What it costs to look at one thing for a long time, and what it gives back. The lead essay this issue.

", "link": "/blog/on-attention", "linkLabel": "Read the essay" } }, + { "block_key": "card", "title": "Post: The Slow Read", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "The slow read", "text": "

A case for reading less, and reading it twice.

", "link": "/blog/the-slow-read", "linkLabel": "Read on" } }, + { "block_key": "card", "title": "Post: Notes on Noticing", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "Notes on noticing", "text": "

Small habits that keep the world from going quiet.

", "link": "/blog/notes-on-noticing", "linkLabel": "Read on" } }, + { "block_key": "card", "title": "Post: The Second Draft", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "The second draft", "text": "

Where the real writing happens, once the first draft is out of the way.

", "link": "/blog/on-attention", "linkLabel": "Read on" } } + ] + }, + { + "slug": "/blog/on-attention", + "title": "On attention", + "template_key": "article", + "parent_slug": "/blog", + "blocks": [ + { "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "

By Frances Hale. Issue 12.

" } }, + { "block_key": "editorial:dropcap_intro", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "lead": "

There is a kind of looking that costs something. You give a thing your whole attention and the rest of the world goes quiet for a while. It is not comfortable. It is not always rewarded. But it is the only way I know to see anything clearly.

" } }, + { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "

I started keeping a notebook of things I had failed to notice. A tree I passed every day for a year. The way a friend says my name. The list grew fast, which told me something about how little I had been paying attention.

Attention is not a switch. It is a muscle, and it gets tired. The trick is not to force it but to make room for it. Read slower. Walk without a phone. Let one thing be the whole thing for an hour.

What you get back is not efficiency. It is the world, a little louder than before.

" } }, + { "block_key": "editorial:marginalia", "title": "Marginalia", "slot": "marginalia", "sort_order": 1, "content": { "items": [ { "anchor": "On notebooks", "note": "I use a cheap one. A nice notebook makes me careful, and careful is the enemy here." }, { "anchor": "On walking", "note": "The phone stays home. That is the whole method." } ] } } + ] + }, + { + "slug": "/blog/the-slow-read", + "title": "The slow read", + "template_key": "article", + "parent_slug": "/blog", + "blocks": [ + { "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "

By Frances Hale. Issue 11.

" } }, + { "block_key": "editorial:dropcap_intro", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "lead": "

Most of what I read once, I forget by the weekend. The books that stay with me are the ones I read twice. So I have started reading less, on purpose, and going back.

" } }, + { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "

The second reading is a different book. You know where it is going, so you stop rushing. You notice the joins. You see what the writer set up on page ten and paid off on page two hundred.

This is slower, and that is the point. A shelf of books read once is a list. A shelf of books read twice is a conversation.

" } }, + { "block_key": "editorial:marginalia", "title": "Marginalia", "slot": "marginalia", "sort_order": 1, "content": { "items": [ { "anchor": "A rule", "note": "One new book for every reread. It keeps the shelf honest." } ] } } + ] + }, + { + "slug": "/blog/notes-on-noticing", + "title": "Notes on noticing", + "template_key": "article", + "parent_slug": "/blog", + "blocks": [ + { "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "

By Frances Hale. Issue 10.

" } }, + { "block_key": "editorial:dropcap_intro", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "lead": "

Noticing is a habit before it is a talent. Here are the small ones that keep my world from going quiet, collected over a slow year of trying.

" } }, + { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "

Name one thing you saw today that you had not seen before. Do it out loud, at dinner, and make someone else do it too.

Carry a small notebook. Write the thing down before you explain it to yourself. The explanation can wait.

None of this is hard. That is what makes it easy to skip. Do it anyway, and the days stop blurring together.

" } }, + { "block_key": "editorial:field_notes", "title": "Field notes", "slot": "main", "sort_order": 3, "content": { "title": "Field notes", "notes": [ { "date": "March 14", "note": "

A heron on the river wall, standing so still I mistook it for a post.

" }, { "date": "March 9", "note": "

The bakery changed its bread and told no one. I noticed on the second bite.

" } ] } }, + { "block_key": "editorial:marginalia", "title": "Marginalia", "slot": "marginalia", "sort_order": 1, "content": { "items": [ { "anchor": "Try it", "note": "The dinner question works on children better than on adults. Adults think it is a test." } ] } } + ] + }, + { + "slug": "/contact", + "title": "Say hello", + "template_key": "contact", + "blocks": [ + { + "block_key": "contact-form", + "title": "Write to me", + "slot": "main", + "sort_order": 1, + "content": { + "heading": "Write to me", + "description": "A note, a question, a book you think I should read. I answer everything, though it can take a week.", + "submitLabel": "Send", + "successMessage": "Thank you. Your note has been received. I will write back soon.", + "fields": [ + { "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" }, + { "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" }, + { "name": "subject", "label": "Subject", "type": "text", "required": false, "width": "full" }, + { "name": "message", "label": "Your note", "type": "textarea", "required": true, "width": "full" } + ], + "formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" } + } + }, + { + "block_key": "contact-card", + "title": "Other ways to reach me", + "slot": "aside", + "sort_order": 1, + "content": { + "heading": "Elsewhere", + "description": "The inbox is best. These work too.", + "columns": 1, + "channels": [ + { "icon": "lucide:mail", "label": "Email", "value": "frances@themargin.example", "link": "mailto:frances@themargin.example" }, + { "icon": "lucide:rss", "label": "Feed", "value": "Subscribe by RSS", "link": "/feed" } + ] + } + } + ] + }, + { + "slug": "/login", + "title": "Members", + "template_key": "auth", + "blocks": [ + { "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } }, + { "block_key": "text", "title": "Foot", "slot": "footer", "sort_order": 1, "content": { "text": "

Members get the full archive and the occasional letter.

" } } + ] + } + ], + "menu_items": [ + { "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 }, + { "menu": "main", "label": "Essays", "page_slug": "/blog", "sort_order": 2 }, + { "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 3 }, + { "menu": "main", "label": "Say hello", "page_slug": "/contact", "sort_order": 4 } + ] +} diff --git a/seed/workflows.json b/seed/workflows.json new file mode 100644 index 0000000..252aafb --- /dev/null +++ b/seed/workflows.json @@ -0,0 +1,19 @@ +{ + "workflows": [ + { + "key": "contact_notify_admins", + "name": "Letter 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" } + } + ] + } + ] +} diff --git a/templates/editorial/article.ninjatpl b/templates/editorial/article.ninjatpl index c578d44..38c5bf4 100644 --- a/templates/editorial/article.ninjatpl +++ b/templates/editorial/article.ninjatpl @@ -1,15 +1,16 @@ - + {{ head_html|safe }} - + + {{ admin_banner_html|safe }} -
{{ slots.header|safe }}
-
{{ slots.byline|safe }}
+ {% if slots.header %}
{{ slots.header|safe }}
{% endif %} + {% if slots.byline %}
{{ slots.byline|safe }}
{% endif %}
-
{% if slots.main %}{{ slots.main|safe }}{% else %}

No content blocks assigned to this page.

{% endif %}
- +
{% if slots.main %}{{ slots.main|safe }}{% else %}

This essay has no content yet.

{% endif %}
+
-
{{ slots.footer|safe }}
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} {{ body_end_html|safe }} diff --git a/templates/editorial/auth.ninjatpl b/templates/editorial/auth.ninjatpl new file mode 100644 index 0000000..b794c65 --- /dev/null +++ b/templates/editorial/auth.ninjatpl @@ -0,0 +1,18 @@ + + +{{ head_html|safe }} + + {{ admin_banner_html|safe }} +
+
+ + {% if site_settings.Logo %}{{ site_settings.LogoAlt }}{% else %}{{ site_settings.Title|default:"Editorial" }}{% endif %} + +
+
{% if slots.main %}{{ slots.main|safe }}{% else %}

No form assigned to this page.

{% endif %}
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} +
+
+ {{ body_end_html|safe }} + + diff --git a/templates/editorial/blog-index.ninjatpl b/templates/editorial/blog-index.ninjatpl new file mode 100644 index 0000000..be365bf --- /dev/null +++ b/templates/editorial/blog-index.ninjatpl @@ -0,0 +1,23 @@ + + +{{ head_html|safe }} + + {{ admin_banner_html|safe }} + {% if slots.header %}
{{ slots.header|safe }}
{% endif %} +
+
+ {% if title %} +
+

{{ title }}

+ {% if page_meta.MetaDescription %}

{{ page_meta.MetaDescription }}

{% endif %} +
+
+ {% endif %} + {% if slots.featured %}
{{ slots.featured|safe }}
{% endif %} + {% if slots.main %}
{{ slots.main|safe }}
{% else %}

No essays published yet.

{% endif %} +
+
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} + {{ body_end_html|safe }} + + diff --git a/templates/editorial/contact.ninjatpl b/templates/editorial/contact.ninjatpl new file mode 100644 index 0000000..64d42fa --- /dev/null +++ b/templates/editorial/contact.ninjatpl @@ -0,0 +1,22 @@ + + +{{ head_html|safe }} + + {{ admin_banner_html|safe }} + {% if slots.header %}
{{ slots.header|safe }}
{% endif %} +
+
+ {% if slots.aside %} +
+
{% if slots.main %}{{ slots.main|safe }}{% endif %}
+ +
+ {% else %} +
{% if slots.main %}{{ slots.main|safe }}{% else %}

No content blocks assigned to this page.

{% endif %}
+ {% endif %} +
+
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} + {{ body_end_html|safe }} + + diff --git a/templates/editorial/default.ninjatpl b/templates/editorial/default.ninjatpl index 4c7e528..434bd80 100644 --- a/templates/editorial/default.ninjatpl +++ b/templates/editorial/default.ninjatpl @@ -1,11 +1,13 @@ - + {{ head_html|safe }} - + {{ admin_banner_html|safe }} -
{{ slots.header|safe }}
-
{% if slots.main %}{{ slots.main|safe }}{% else %}

No content blocks assigned to this page.

{% endif %}
-
{{ slots.footer|safe }}
+ {% if slots.header %}
{{ slots.header|safe }}
{% endif %} +
+ {% if slots.main %}
{{ slots.main|safe }}
{% else %}

No content blocks assigned to this page.

{% endif %} +
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} {{ body_end_html|safe }} diff --git a/templates/editorial/full-width.ninjatpl b/templates/editorial/full-width.ninjatpl index fc86238..b5a34af 100644 --- a/templates/editorial/full-width.ninjatpl +++ b/templates/editorial/full-width.ninjatpl @@ -1,11 +1,11 @@ - + {{ head_html|safe }} - + {{ admin_banner_html|safe }} -
{{ slots.header|safe }}
+ {% if slots.header %}
{{ slots.header|safe }}
{% endif %}
{% if slots.main %}{{ slots.main|safe }}{% else %}

No content blocks assigned to this page.

{% endif %}
-
{{ slots.footer|safe }}
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} {{ body_end_html|safe }} diff --git a/templates/editorial/landing.ninjatpl b/templates/editorial/landing.ninjatpl index 9d036c4..3ae7119 100644 --- a/templates/editorial/landing.ninjatpl +++ b/templates/editorial/landing.ninjatpl @@ -1,12 +1,14 @@ - + {{ head_html|safe }} - + {{ admin_banner_html|safe }} -
{{ slots.masthead|safe }}
-
{{ slots.lead|safe }}
-
{{ slots.river|safe }}
-
{{ slots.footer|safe }}
+ {% if slots.masthead %}
{{ slots.masthead|safe }}
{% endif %} +
+ {% if slots.lead %}
{{ slots.lead|safe }}
{% endif %} + {% if slots.river %}

{{ slots.river|safe }}
{% endif %} +
+ {% if slots.footer %}
{{ slots.footer|safe }}
{% endif %} {{ body_end_html|safe }} diff --git a/templates/email/editorial.ninjatpl b/templates/email/editorial.ninjatpl index c5e949a..f2a2d54 100644 --- a/templates/email/editorial.ninjatpl +++ b/templates/email/editorial.ninjatpl @@ -1,14 +1,74 @@ -{{ site_name }}{% if preview_text %}
{{ preview_text }}
{% endif %}
+ + + + + + + + + {{ site_name }} + + + + {% if preview_text %}
{{ preview_text }}
{% endif %} + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/templates/overrides/editorial/announcement-bar.ninjatpl b/templates/overrides/editorial/announcement-bar.ninjatpl new file mode 100644 index 0000000..b386802 --- /dev/null +++ b/templates/overrides/editorial/announcement-bar.ninjatpl @@ -0,0 +1,21 @@ +{# announcement-bar override — quiet notice on a hairline panel, small-caps message. Dismiss {% endif %} diff --git a/templates/overrides/editorial/audio-embed.ninjatpl b/templates/overrides/editorial/audio-embed.ninjatpl new file mode 100644 index 0000000..4ef4a4e --- /dev/null +++ b/templates/overrides/editorial/audio-embed.ninjatpl @@ -0,0 +1,16 @@ +{# audio-embed override — editorial hairline player card. Fields + facade JS mirror builtin verbatim: url, media, artwork, title, artist, download. #} +
{% if artwork %}
{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}
{% endif %}
{% if title %}

{{ title }}

{% endif %}{% if artist %}

{{ artist }}

{% endif %}
{% if download %}::lucide:download:sm:: Download{% endif %}
diff --git a/templates/overrides/editorial/auth-form.ninjatpl b/templates/overrides/editorial/auth-form.ninjatpl new file mode 100644 index 0000000..6159cec --- /dev/null +++ b/templates/overrides/editorial/auth-form.ninjatpl @@ -0,0 +1,33 @@ +{# auth-form (editorial) — typographic login/register in an editorial-panel with small-caps labels, hairline tab rules and oxblood active tab. Wiring UNTOUCHABLE: single shared #auth-message, /api/auth/login (email,password) and /api/auth/register (username,email,password), hx-target #auth-message, captcha injected from _captcha_widget. #} +{% if context.isPublicLoggedIn %} +
+

You are signed in as {{ context.publicUsername }}

+ My account +
+{% else %} +
+
+
+ + +
+
+
+
+
+ {% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %} + +

Forgot password?

+
+
+
+
+
+
+
+ {% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %} + +
+
+
+{% endif %} diff --git a/templates/overrides/editorial/auth-status.ninjatpl b/templates/overrides/editorial/auth-status.ninjatpl new file mode 100644 index 0000000..423218e --- /dev/null +++ b/templates/overrides/editorial/auth-status.ninjatpl @@ -0,0 +1,31 @@ +{# auth-status (editorial) — header login state; small-caps links, hairline monogram and quiet editorial-panel dropdown. Driven by context.isPublicLoggedIn / context.publicDisplayName / context.publicUsername; logout posts /api/auth/logout. #} +{% if context.isPublicLoggedIn %} +
+ + + +
+{% else %} + +{% endif %} diff --git a/templates/overrides/editorial/author-bio-hero.ninjatpl b/templates/overrides/editorial/author-bio-hero.ninjatpl new file mode 100644 index 0000000..c8bb084 --- /dev/null +++ b/templates/overrides/editorial/author-bio-hero.ninjatpl @@ -0,0 +1,22 @@ +{# author-bio-hero (editorial) — author-page header framed by hairline rules over generous whitespace. #} +{% if author %} +
+
+{% if showAvatar and author.avatar_url %}
{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}
{% endif %} +
+

{{ author.display_name }}

+{% if author.job_title or author.company %}

{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}

{% endif %} +{% if author.bio %}

{{ author.bio }}

{% endif %} +
+{% if showPostCount %}{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}{% endif %} +{% if author.location %}{{ author.location }}{% endif %} +
+{% if showSocial %}
+{% if author.website_url %}{% endif %} +{% if author.twitter_handle %}{% endif %} +{% if author.linkedin_url %}{% endif %} +{% if author.github_url %}{% endif %} +
{% endif %} +
+
+{% endif %} diff --git a/templates/overrides/editorial/author-bio.ninjatpl b/templates/overrides/editorial/author-bio.ninjatpl new file mode 100644 index 0000000..2bd8753 --- /dev/null +++ b/templates/overrides/editorial/author-bio.ninjatpl @@ -0,0 +1,14 @@ +{# author-bio (editorial) — byline card in an editorial-panel with hairline rule and small-caps label. #} +{% if post.author_name %} + +{% endif %} diff --git a/templates/overrides/editorial/blog-hero.ninjatpl b/templates/overrides/editorial/blog-hero.ninjatpl new file mode 100644 index 0000000..6bc030e --- /dev/null +++ b/templates/overrides/editorial/blog-hero.ninjatpl @@ -0,0 +1,24 @@ +{# blog-hero (editorial) — blog-index header + featured posts from the `posts` provider; typographic, hairline rules. #} +
+
+
+{% if title %}

{{ title }}

{% endif %} +{% if subtitle %}

{{ subtitle }}

{% endif %} +
+{% if posts %} +
+{% for post in posts %} + +{% endfor %} +
+{% endif %} +
+
diff --git a/templates/overrides/editorial/breadcrumbs.ninjatpl b/templates/overrides/editorial/breadcrumbs.ninjatpl new file mode 100644 index 0000000..539674f --- /dev/null +++ b/templates/overrides/editorial/breadcrumbs.ninjatpl @@ -0,0 +1,13 @@ +{# breadcrumbs (editorial) — small-caps trail with hairline oxblood separators; current item plain. #} +{% set arrows = template == "arrows" %} + diff --git a/templates/overrides/editorial/button.ninjatpl b/templates/overrides/editorial/button.ninjatpl index 3693bb8..47e816b 100644 --- a/templates/overrides/editorial/button.ninjatpl +++ b/templates/overrides/editorial/button.ninjatpl @@ -1 +1,10 @@ -{% if text %}{% if url %}{% else %}{% endif %}{% endif %} +{# button override — hairline uppercase button; ghost/link/secondary styles render as an inline oxblood underline link. Fields: link, style, label, size, icon, iconPosition, newTab, align. #} +{% with align=align|default:"left" %} + +{% endwith %} diff --git a/templates/overrides/editorial/card.ninjatpl b/templates/overrides/editorial/card.ninjatpl new file mode 100644 index 0000000..8ba2fc4 --- /dev/null +++ b/templates/overrides/editorial/card.ninjatpl @@ -0,0 +1,4 @@ +{# card override — editorial hairline panel; accent border on hover, oxblood link. Fields mirror builtin: layout, link, media, title, text, linkLabel. Media via {% img %}. #} +{% with layout=layout|default:"vertical" %} + +{% endwith %} diff --git a/templates/overrides/editorial/category-list.ninjatpl b/templates/overrides/editorial/category-list.ninjatpl new file mode 100644 index 0000000..168f3ba --- /dev/null +++ b/templates/overrides/editorial/category-list.ninjatpl @@ -0,0 +1,18 @@ +{# category-list (editorial) — from the `categories` provider. hairline list or bordered chips. #} +{% set pills = style == "pills" %} +
+{% if heading %}

{{ heading }}

{% endif %} +{% if pills %} +
+{% for cat in categories %} +{{ cat.name }}{% if showCount %}{{ cat.post_count }}{% endif %} +{% endfor %} +
+{% else %} + +{% endif %} +
diff --git a/templates/overrides/editorial/contact-card.ninjatpl b/templates/overrides/editorial/contact-card.ninjatpl new file mode 100644 index 0000000..b70de05 --- /dev/null +++ b/templates/overrides/editorial/contact-card.ninjatpl @@ -0,0 +1,17 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if description %}

{{ description }}

{% endif %} +
+ {% for channel in channels %} +
+ {% if channel.icon %}{% endif %} +
+ {% if channel.label %}
{{ channel.label }}
{% endif %} + {% if channel.value %}
{% if channel.link %}{{ channel.value }}{% else %}{{ channel.value }}{% endif %}
{% endif %} +
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/editorial/contact-form.ninjatpl b/templates/overrides/editorial/contact-form.ninjatpl new file mode 100644 index 0000000..4fdae18 --- /dev/null +++ b/templates/overrides/editorial/contact-form.ninjatpl @@ -0,0 +1,43 @@ +{# contact-form override — editorial enquiry form. ALL form wiring preserved verbatim from builtin: bid, {% form %} action/hx attrs, honeypot, field loop, form.submitted/errors, submit target. Only inputs restyled to hairline editorial fields. #} +{% with bid=_block_id|default:context.blockId %} +
+
+ {% if form.submitted %} +
+
::lucide:check:lg::
+

{{ form.message|default:successMessage|default:"Thank you. Your table request has been received." }}

+
+ {% else %} +
+ {% if heading %}

{{ heading }}

{% endif %} + {% if description %}

{{ description }}

{% endif %} + +
+ {% if form.errors._form %}
{{ form.errors._form }}
{% 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 %} +
+ {% if field.label %}{% endif %} + {% if field.type == "textarea" %} + + {% elif field.type == "select" %} + + {% else %} + + {% endif %} +
+ {% endfor %} + +
+ +
+ {% endform %} + {% endwith %} + {% endif %} +
+
+{% endwith %} diff --git a/templates/overrides/editorial/countdown.ninjatpl b/templates/overrides/editorial/countdown.ninjatpl new file mode 100644 index 0000000..b0fe083 --- /dev/null +++ b/templates/overrides/editorial/countdown.ninjatpl @@ -0,0 +1,48 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+
+
--{{ labelDays|default:"Days" }}
+
--{{ labelHours|default:"Hours" }}
+
--{{ labelMinutes|default:"Minutes" }}
+
--{{ labelSeconds|default:"Seconds" }}
+
+ +
+ +
+
+ diff --git a/templates/overrides/editorial/cta.ninjatpl b/templates/overrides/editorial/cta.ninjatpl new file mode 100644 index 0000000..8fcbd7e --- /dev/null +++ b/templates/overrides/editorial/cta.ninjatpl @@ -0,0 +1,14 @@ +
+
+
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if text %}

{{ text }}

{% endif %} +
+
+ {% if primaryUrl and primaryLabel %}{{ primaryLabel }}{% endif %} + {% if secondaryUrl and secondaryLabel %}{{ secondaryLabel }}{% endif %} +
+
+
+
diff --git a/templates/overrides/editorial/divider.ninjatpl b/templates/overrides/editorial/divider.ninjatpl new file mode 100644 index 0000000..333a1c6 --- /dev/null +++ b/templates/overrides/editorial/divider.ninjatpl @@ -0,0 +1,12 @@ +{# divider override — editorial hairline rules. Fields mirror builtin: variant (line|ornament|spacer-rule), weight, width, icon. ornament renders an icon flanked by hairline rules; spacer-rule a short centred hairline; line a full hairline rule. #} +{% with variant=variant|default:"line" weight=weight|default:"thin" width=width|default:"full" %} +{% if variant == "ornament" %} + +{% elif variant == "spacer-rule" %} + +{% else %} + +{% endif %} +{% endwith %} diff --git a/templates/overrides/editorial/event-list.ninjatpl b/templates/overrides/editorial/event-list.ninjatpl new file mode 100644 index 0000000..e5a6eec --- /dev/null +++ b/templates/overrides/editorial/event-list.ninjatpl @@ -0,0 +1,41 @@ +{# event-list override — editorial listings: hairline panels/rules, oxblood dates, Playfair titles. Fields mirror builtin: heading, intro, layout, hidePast, events[].* . #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} + {% if layout == "cards" %} +
+ {% for event in events %}{% if not (hidePast and event.past) %} +
+ {% if event.media %}
{% img event.media alt=event.title class="h-full w-full object-cover" %}
{% endif %} +
+
{{ event.start }}{% if event.end %} – {{ event.end }}{% endif %}{% if event.timezone %} {{ event.timezone }}{% endif %}
+

{{ event.title }}

+ {% if event.venue %}

{{ event.venue }}

{% endif %} + {% if event.description %}

{{ event.description }}

{% endif %} + {% if event.link %}{{ event.linkLabel|default:"Details" }}{% endif %} +
+
+ {% endif %}{% endfor %} +
+ {% else %} +
    + {% for event in events %}{% if not (hidePast and event.past) %} +
  • +
    +
    {{ event.start }}
    + {% if event.end %}
    to {{ event.end }}
    {% endif %} + {% if event.timezone %}
    {{ event.timezone }}
    {% endif %} +
    +
    +

    {{ event.title }}

    + {% if event.venue %}

    {{ event.venue }}

    {% endif %} + {% if event.description %}

    {{ event.description }}

    {% endif %} +
    + {% if event.link %}{{ event.linkLabel|default:"Details" }}{% endif %} +
  • + {% endif %}{% endfor %} +
+ {% endif %} +
+
diff --git a/templates/overrides/editorial/faq.ninjatpl b/templates/overrides/editorial/faq.ninjatpl new file mode 100644 index 0000000..4bdd166 --- /dev/null +++ b/templates/overrides/editorial/faq.ninjatpl @@ -0,0 +1,17 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for item in items %} +
+ + {{ item.question }} + + +
{{ item.answer|safe }}
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/editorial/feature-grid.ninjatpl b/templates/overrides/editorial/feature-grid.ninjatpl new file mode 100644 index 0000000..40ed9e4 --- /dev/null +++ b/templates/overrides/editorial/feature-grid.ninjatpl @@ -0,0 +1,17 @@ +
+
+ {% if eyebrow %}

{{ eyebrow }}

{% endif %} + {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for item in items %} +
+ {% if item.icon %}
{% endif %} +

{% if item.linkUrl %}{{ item.title }}{% else %}{{ item.title }}{% endif %}

+ {% if item.text %}

{{ item.text }}

{% endif %} + {% if item.linkUrl and item.linkLabel %}{{ item.linkLabel }}{% endif %} +
+ {% endfor %} +
+
+
diff --git a/templates/overrides/editorial/featured-posts.ninjatpl b/templates/overrides/editorial/featured-posts.ninjatpl new file mode 100644 index 0000000..c237a52 --- /dev/null +++ b/templates/overrides/editorial/featured-posts.ninjatpl @@ -0,0 +1,24 @@ +{# featured-posts (editorial) — grid of hairline-framed cards from the `posts` provider #} +{# (params: limit, featured=true). Card: image, title, excerpt, meta. #} +
+
+{% if heading %}

{{ heading }}

{% endif %} +
+{% for post in posts %} +
+{% if showFeaturedImage and post.featured_image_url %}{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" %}{% endif %} +
+

{{ post.title }}

+ +{% if showExcerpt and post.excerpt %}

{{ post.excerpt }}

{% endif %} +
+{% if showAuthor and post.author_name %}{{ post.author_name }}{% endif %} +{% if showDate and post.published_at %}{{ post.published_at|date:"M j, Y" }}{% endif %} +{% if post.reading_time %}{{ post.reading_time }} min read{% endif %} +
+
+
+{% endfor %} +
+
+
diff --git a/templates/overrides/editorial/footer.ninjatpl b/templates/overrides/editorial/footer.ninjatpl new file mode 100644 index 0000000..e345bdd --- /dev/null +++ b/templates/overrides/editorial/footer.ninjatpl @@ -0,0 +1,40 @@ +{# footer override — quiet editorial colophon: hairline rules, small-caps labels, address, reservations, social on a card panel. Structure/fields/signup_form preserved from builtin; reskinned to editorial. #} +{% set fl = layout %} +{% if not fl %}{% if template == "centered" %}{% set fl = "centered" %}{% else %}{% set fl = "columns" %}{% endif %}{% endif %} +{% set fbg = backgroundColor|default:"bg-card text-card-foreground" %} +{% macro sociallinks(cls) %}{% if social_items %}{% endif %}{% endmacro %} +{% macro newsletter() %}{% if showNewsletter %}

{{ newsletterTitle|default:"Join the guest list" }}

{% if newsletterDescription %}

{{ newsletterDescription }}

{% endif %}
{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}
{% endif %}{% endmacro %} +{% macro legalbar() %}

{% if copyright %}© {{ copyright }}{% else %}© {{ companyName }}{% endif %}

{% if legal_items %}{% endif %}
{% endmacro %} +
+
+{% if fl == "minimal" %} +
+
{% if logo %}{{ companyName }}{% elif companyName %}{{ companyName }}{% endif %}{% if tagline %}{% endif %}
+{{ sociallinks("") }} +
+{{ legalbar() }} +{% elif fl == "centered" %} +
+{% if logo %}{{ companyName }}{% elif companyName %}{{ companyName }}{% endif %} + +{% if tagline %}

{{ tagline }}

{% endif %} +{% if columns %}{% endif %} +{{ newsletter() }} +{{ sociallinks("justify-center") }} +
+{{ legalbar() }} +{% else %} +
+
+{% if logo %}{{ companyName }}{% elif companyName %}{{ companyName }}{% endif %} +{% if tagline %}

{{ tagline }}

{% endif %} +{% if address or email or phone %}
{% if address %}
{{ address }}
{% endif %}{% if email %}{% endif %}{% if phone %}{% endif %}
{% endif %} +{{ sociallinks("mt-5") }} +
+{% for col in columns %}

{{ col.header }}

{% endfor %} +{% if showNewsletter %}
{{ newsletter() }}
{% endif %} +
+{{ legalbar() }} +{% endif %} +
+
diff --git a/templates/overrides/editorial/gallery.ninjatpl b/templates/overrides/editorial/gallery.ninjatpl new file mode 100644 index 0000000..8943898 --- /dev/null +++ b/templates/overrides/editorial/gallery.ninjatpl @@ -0,0 +1,52 @@ +{# gallery override — editorial grid: plain hairline framing, generous whitespace, unchanged lightbox. Fields + lightbox JS mirror builtin verbatim: layout, gap, columns, lightbox, images[].src/.alt/.caption. #} +{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}{% if lightbox %}{% endif %}{% endwith %} diff --git a/templates/overrides/editorial/heading.ninjatpl b/templates/overrides/editorial/heading.ninjatpl index 067e99e..620b1a6 100644 --- a/templates/overrides/editorial/heading.ninjatpl +++ b/templates/overrides/editorial/heading.ninjatpl @@ -1 +1,7 @@ -{% if level|integer == 1 %}
{% if kicker %}{{ kicker }}{% endif %}

{{ text }}

{% elif level|integer == 3 %}

{{ text }}

{% elif level|integer == 4 %}

{{ text }}

{% elif level|integer == 5 %}
{{ text }}
{% elif level|integer == 6 %}
{{ text }}
{% else %}

{{ text }}

{% endif %} +{# heading override — editorial display serif with a hairline underline. Fields: text, level, textClass (mirrors builtin). #} +{% if level == 1 %}

{{ text }}

+{% elif level == 3 %}

{{ text }}

+{% elif level == 4 %}

{{ text }}

+{% elif level == 5 %}
{{ text }}
+{% elif level == 6 %}
{{ text }}
+{% else %}

{{ text }}

{% endif %} diff --git a/templates/overrides/editorial/hero.ninjatpl b/templates/overrides/editorial/hero.ninjatpl new file mode 100644 index 0000000..fc6394e --- /dev/null +++ b/templates/overrides/editorial/hero.ninjatpl @@ -0,0 +1,39 @@ +{# hero override — editorial masthead: hairline eyebrow rules, small-caps Playfair headline, roomy reading measure. Structure/fields/{% img %} calls preserved from builtin. Styled-text fields accept {text,color} or a bare string. #} +{% set eyebrowText = eyebrow.text|default:eyebrow %} +{% set headlineText = headline.text|default:headline %} +{% set subText = subheadline.text|default:subheadline %} +{% set descText = description.text|default:description %} +{% set pText = ctaPrimary.text %} +{% set pUrl = ctaPrimary.url %} +{% set sText = ctaSecondary.text %} +{% set sUrl = ctaSecondary.url %} +{% set variant = template|default:"centered" %} +{% set hasImage = backgroundImage %} +{% set showOverlay = hasImage and overlayColor != "none" and overlayColor != "" %} +{% macro herocontent(align) %} +{% if eyebrowText %}
{{ eyebrowText }}
{% endif %} +{% if headlineText %}

{{ headlineText }}

{% endif %} +{% if subText %}

{{ subText }}

{% endif %} +{% if descText %}

{{ descText }}

{% endif %} +{% if pText or sText %}
+{% if pText and pUrl %}{{ pText }}{% endif %} +{% if sText and sUrl %}{{ sText }}{% endif %} +
{% endif %} +{% if trustLogos %}
+

Acclaim

+
{% for logo in trustLogos %}{% if logo.url %}{% img logo.image alt=logo.alt class="h-8 w-auto" %}{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-60" %}{% endif %}{% endfor %}
+
{% endif %} +{% endmacro %} +{% if variant == "split" %} +
+
{% if hasImage %}{% img backgroundImage alt=headlineText class="absolute inset-0 h-full w-full object-cover" layout="hero" %}{% endif %}
+
{{ herocontent("left") }}
+
+{% else %} +{% set align = "center" %} +{% if variant == "left-aligned" %}{% set align = "left" %}{% endif %} +
+{% if hasImage %}
{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}{% if showOverlay %}
{% endif %}
{% endif %} +
{{ herocontent(align) }}
+
+{% endif %} diff --git a/templates/overrides/editorial/hours-location.ninjatpl b/templates/overrides/editorial/hours-location.ninjatpl new file mode 100644 index 0000000..6c125be --- /dev/null +++ b/templates/overrides/editorial/hours-location.ninjatpl @@ -0,0 +1,31 @@ +{# hours-location override (editorial) — typographic hours ledger + hairline-framed map. Map facade + + {% endif %} + + + diff --git a/templates/overrides/editorial/image.ninjatpl b/templates/overrides/editorial/image.ninjatpl index 8769d22..4d4c7fe 100644 --- a/templates/overrides/editorial/image.ninjatpl +++ b/templates/overrides/editorial/image.ninjatpl @@ -1 +1,11 @@ -{% if src %}
{{ alt }}{% if caption %}
{{ caption }}
{% endif %}
{% endif %} +{# image override — plain editorial plate, no ornament frame. Fields mirror builtin: src, alt, caption, size, align, aspectRatio, objectFit. Media resolved via {% img %} (media filter is not registered in the block render path). #} +{% with size=size|default:"full" align=align|default:"center" objectFit=objectFit|default:"cover" %} +
+
+
+ {% if objectFit == "contain" %}{% img src alt=alt class="w-full h-full object-contain" layout=size %}{% elif objectFit == "fill" %}{% img src alt=alt class="w-full h-full object-fill" layout=size %}{% elif objectFit == "none" %}{% img src alt=alt class="w-full h-full object-none" layout=size %}{% else %}{% img src alt=alt class="w-full h-full object-cover" layout=size %}{% endif %} +
+
+ {% if caption %}
{{ caption }}
{% endif %} +
+{% endwith %} diff --git a/templates/overrides/editorial/logo-strip.ninjatpl b/templates/overrides/editorial/logo-strip.ninjatpl new file mode 100644 index 0000000..0c6721b --- /dev/null +++ b/templates/overrides/editorial/logo-strip.ninjatpl @@ -0,0 +1,15 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} +
+
+ {% for logo in logos %} + {% if logo.url %}{% endif %} + {% img logo.image alt=logo.alt class="h-8 w-auto object-contain" %} + {% if logo.url %}{% endif %} + {% endfor %} +
+
+
+
+{% if marquee %}{% endif %} diff --git a/templates/overrides/editorial/menu-list.ninjatpl b/templates/overrides/editorial/menu-list.ninjatpl new file mode 100644 index 0000000..65c78c4 --- /dev/null +++ b/templates/overrides/editorial/menu-list.ninjatpl @@ -0,0 +1,30 @@ +{# menu-list override — editorial menu: Playfair section titles, hairline leader rules, generous columns. Fields mirror builtin: heading, intro, columns, sections[].title/.note/.items[].name/.badges/.description/.price, currency, footnote. #} +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} + +
+ {% for section in sections %} + + {% endfor %} +
+ {% if footnote %}

{{ footnote }}

{% endif %} +
+
diff --git a/templates/overrides/editorial/navbar.ninjatpl b/templates/overrides/editorial/navbar.ninjatpl new file mode 100644 index 0000000..6289f28 --- /dev/null +++ b/templates/overrides/editorial/navbar.ninjatpl @@ -0,0 +1,104 @@ +{# navbar override — editorial masthead. Structure, fixed drawer ids, focus-trap JS and all data-* preserved verbatim from the builtin; presentation reskinned to editorial (hairline rules, small-caps labels). Fields identical: variant/stickyBehavior/megaMenu/logoType/logoText/logo/companyName/cta/menu.items/... #} +{% set nv = variant %} +{% if not nv %}{% if template == "centered" %}{% set nv = "centered-logo" %}{% elif template == "transparent" %}{% set nv = "transparent-over-hero" %}{% else %}{% set nv = "standard" %}{% endif %}{% endif %} +{% set sb = stickyBehavior %} +{% if not sb %}{% if sticky %}{% set sb = "sticky" %}{% else %}{% set sb = "none" %}{% endif %}{% endif %} +{% set use_mega = megaMenu or template == "mega" %} +{% set is_transparent = nv == "transparent-over-hero" %} +{% set cta_text = cta.text|default:ctaLabel %} +{% set cta_url = cta.url|default:ctaUrl %} +{% set bg = backgroundColor|default:"bg-background" %} +{% macro navlink(item) %}{{ item.label }}{% endmacro %} +{% macro dropdown(item) %}
+ +{% if use_mega %} +{% else %}{% endif %} +
{% endmacro %} +{% macro brand(size) %}{% if logoType == "text" and logoText.text %}{{ logoText.text }}{% elif logo %}{{ alt|default:companyName }}{% elif companyName %}{{ companyName }}{% endif %}{% endmacro %} +{% macro ctabtn(extra) %}{% if cta_text %}{{ cta_text }}{% endif %}{% endmacro %} + + + diff --git a/templates/overrides/editorial/newsletter-signup.ninjatpl b/templates/overrides/editorial/newsletter-signup.ninjatpl new file mode 100644 index 0000000..bcc1b23 --- /dev/null +++ b/templates/overrides/editorial/newsletter-signup.ninjatpl @@ -0,0 +1,13 @@ +{# newsletter-signup (editorial) — the {% signup_form %} tag carries the #} +{# untouchable subscribe wiring (/api/lists/subscribe HTMX post + honeypot); #} +{# this template wraps it in a quiet editorial-panel — no loud filled band. #} +
+
+
+
+{% if title %}

{{ title }}

{% endif %} +{% if description %}

{{ description }}

{% endif %} +
{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}
+
+
+
diff --git a/templates/overrides/editorial/page-suggestions.ninjatpl b/templates/overrides/editorial/page-suggestions.ninjatpl new file mode 100644 index 0000000..14c8d33 --- /dev/null +++ b/templates/overrides/editorial/page-suggestions.ninjatpl @@ -0,0 +1,21 @@ +{# page-suggestions (editorial) — 404 suggestions with underlined heading and oxblood markers. Fields: title, showDescription, emptyMessage; suggestions[] (.slug/.title/.excerpt). #} +{% if suggestions %} +
+ {% if title %}

{{ title }}

{% endif %} +
    + {% for suggestion in suggestions %} +
  • + ::lucide:chevron-right:sm:: +
    + {{ suggestion.title }} + {% if showDescription and suggestion.excerpt %}

    {{ suggestion.excerpt }}

    {% endif %} +
    +
  • + {% endfor %} +
+
+{% elif emptyMessage %} +
+

{{ emptyMessage }}

+
+{% endif %} diff --git a/templates/overrides/editorial/password-reset-form.ninjatpl b/templates/overrides/editorial/password-reset-form.ninjatpl new file mode 100644 index 0000000..6d6b6d4 --- /dev/null +++ b/templates/overrides/editorial/password-reset-form.ninjatpl @@ -0,0 +1,13 @@ +{# password-reset-form (editorial) — request stage in an editorial-panel with small-caps label. Wiring UNTOUCHABLE: /api/auth/request-password-reset (email), hx-target #reset-message. #} +
+
+
+

Reset password

+

Enter your email and we will send a link to reset your password.

+
+
+ +
+

Back to sign in

+
+
diff --git a/templates/overrides/editorial/popular-posts.ninjatpl b/templates/overrides/editorial/popular-posts.ninjatpl new file mode 100644 index 0000000..f9d79f2 --- /dev/null +++ b/templates/overrides/editorial/popular-posts.ninjatpl @@ -0,0 +1,16 @@ +{# popular-posts (editorial) — compact ranked list from the `posts` provider, hairline-ruled. #} +
+{% if heading %}

{{ heading }}

{% endif %} +
    +{% for post in posts %} +
  1. +{% if showRank %}{{ forloop.Counter }}{% endif %} +{% if showThumbnail and post.featured_image_url %}{% img post.featured_image_url alt=post.title class="h-full w-full object-cover" %}{% endif %} +
    +

    {{ post.title }}

    +{% if post.published_at %}

    {{ post.published_at|date:"M j, Y" }}

    {% endif %} +
    +
  2. +{% endfor %} +
+
diff --git a/templates/overrides/editorial/post-hero.ninjatpl b/templates/overrides/editorial/post-hero.ninjatpl new file mode 100644 index 0000000..ecd22d3 --- /dev/null +++ b/templates/overrides/editorial/post-hero.ninjatpl @@ -0,0 +1,23 @@ +{# post-hero (editorial) — current post from the `post` provider. When the post #} +{# has a featured image it becomes an overlaid banner; otherwise a clean text hero. #} +{% set center = align != "left" %} +{% if post.featured_image_url %} +
+
{% img post.featured_image_url alt=post.featured_image_alt class="h-full w-full object-cover" layout="hero" %}
+
+{% if center %}
{% endif %} +

{{ post.title }}

+{% if showExcerpt and post.excerpt %}

{{ post.excerpt }}

{% endif %} +{% if showMeta %}
{% if post.author_name %}{{ post.author_name }}{% endif %}{% if post.published_at %}{{ post.published_at|date:"F j, Y" }}{% endif %}{% if post.reading_time %}{{ post.reading_time }} min read{% endif %}
{% endif %} +
+
+{% else %} +
+
+{% if center %}
{% endif %} +

{{ post.title }}

+{% if showExcerpt and post.excerpt %}

{{ post.excerpt }}

{% endif %} +{% if showMeta %}
{% if post.author_name %}{{ post.author_name }}{% endif %}{% if post.published_at %}{{ post.published_at|date:"F j, Y" }}{% endif %}{% if post.reading_time %}{{ post.reading_time }} min read{% endif %}
{% endif %} +
+
+{% endif %} diff --git a/templates/overrides/editorial/post-metadata.ninjatpl b/templates/overrides/editorial/post-metadata.ninjatpl new file mode 100644 index 0000000..085d1ee --- /dev/null +++ b/templates/overrides/editorial/post-metadata.ninjatpl @@ -0,0 +1,9 @@ +{# post-metadata (editorial) — author/date/reading-time row from the `post` provider, hairline chips. #} +{% if post %} +
+{% if showAuthor and post.author_name %}{% if showAvatar and post.author_avatar %}{% img post.author_avatar alt=post.author_name class="h-7 w-7 rounded-full object-cover ring-1 ring-[hsl(var(--border))]" %}{% endif %}{{ post.author_name }}{% endif %} +{% if showDate and post.published_at %}{% endif %} +{% if showReadingTime and post.reading_time %}{{ post.reading_time }} min read{% endif %} +{% if post.categories %}{% for cat in post.categories %}{{ cat.name }}{% endfor %}{% endif %} +
+{% endif %} diff --git a/templates/overrides/editorial/pricing.ninjatpl b/templates/overrides/editorial/pricing.ninjatpl new file mode 100644 index 0000000..d7bc788 --- /dev/null +++ b/templates/overrides/editorial/pricing.ninjatpl @@ -0,0 +1,25 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for tier in tiers %} +
+ {% if tier.badge %}{{ tier.badge }}{% endif %} +

{{ tier.name }}

+
+

+ {% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }} + {% if tier.period %}{{ tier.period }}{% endif %} +

+ {% if tier.description %}

{{ tier.description }}

{% endif %} + {% if tier.features %}
    + {% for feature in tier.features %}
  • {{ feature }}
  • {% endfor %} +
{% endif %} + {% if tier.ctaUrl and tier.ctaLabel %}{% if tier.highlighted %}{{ tier.ctaLabel }}{% else %}{{ tier.ctaLabel }}{% endif %}{% endif %} +
+ {% endfor %} +
+ {% if billingNote %}

{{ billingNote }}

{% endif %} +
+
diff --git a/templates/overrides/editorial/quote.ninjatpl b/templates/overrides/editorial/quote.ninjatpl new file mode 100644 index 0000000..2a16b9d --- /dev/null +++ b/templates/overrides/editorial/quote.ninjatpl @@ -0,0 +1,8 @@ +{# quote override — editorial pull-quote: large italic Source Serif, hairline panel or oxblood accent rule; centred quotes get a short centred hairline. Fields mirror builtin: quote, style, align, cite, attribution, role. #} +{% with style=style|default:"block" align=align|default:"left" %} +
+ {% if align == "center" %}{% endif %} + {{ quote|safe }} + {% if attribution or role or cite %}
{% if attribution %}{{ attribution }}{% endif %}{% if role %}{{ role }}{% endif %}{% if cite %}Source{% endif %}
{% endif %} +
+{% endwith %} diff --git a/templates/overrides/editorial/related-posts.ninjatpl b/templates/overrides/editorial/related-posts.ninjatpl new file mode 100644 index 0000000..2f0fee7 --- /dev/null +++ b/templates/overrides/editorial/related-posts.ninjatpl @@ -0,0 +1,17 @@ +{# related-posts (editorial) — hairline-topped grid from the `posts` provider. #} +{% if posts %} +
+
+

{{ heading|default:"Related posts" }}

+
+{% for post in posts %} + +{% endfor %} +
+
+
+{% endif %} diff --git a/templates/overrides/editorial/rich-section.ninjatpl b/templates/overrides/editorial/rich-section.ninjatpl new file mode 100644 index 0000000..77f56aa --- /dev/null +++ b/templates/overrides/editorial/rich-section.ninjatpl @@ -0,0 +1,13 @@ +
+
+
+ {% if eyebrow %}

{{ eyebrow }}

{% endif %} + {% if heading %}

{{ heading }}

{% endif %} + {% if body %}
{{ body|safe }}
{% endif %} + {% if ctaUrl and ctaLabel %}{{ ctaLabel }}{% endif %} +
+
+ {% if media %}
{% img media alt=mediaAlt class="w-full h-auto object-cover" %}
{% endif %} +
+
+
diff --git a/templates/overrides/editorial/rich-text.ninjatpl b/templates/overrides/editorial/rich-text.ninjatpl new file mode 100644 index 0000000..4ee4eb8 --- /dev/null +++ b/templates/overrides/editorial/rich-text.ninjatpl @@ -0,0 +1,2 @@ +{# rich-text override — long-form Source Serif editorial column with oxblood links and hairline blockquotes. Field: html (mirrors builtin). #} +{% if html %}
{{ html|safe }}
{% endif %} diff --git a/templates/overrides/editorial/sidebar-nav.ninjatpl b/templates/overrides/editorial/sidebar-nav.ninjatpl new file mode 100644 index 0000000..800c4dc --- /dev/null +++ b/templates/overrides/editorial/sidebar-nav.ninjatpl @@ -0,0 +1,13 @@ +{# sidebar-nav (editorial) — small-caps vertical menu with hairline divider rule and oxblood link/icon accents. #} +{% set compact = template == "compact" %} + +{% if highlightCurrent %}{% endif %} diff --git a/templates/overrides/editorial/social-links.ninjatpl b/templates/overrides/editorial/social-links.ninjatpl new file mode 100644 index 0000000..c5998c6 --- /dev/null +++ b/templates/overrides/editorial/social-links.ninjatpl @@ -0,0 +1,13 @@ + diff --git a/templates/overrides/editorial/stats.ninjatpl b/templates/overrides/editorial/stats.ninjatpl new file mode 100644 index 0000000..d5945e2 --- /dev/null +++ b/templates/overrides/editorial/stats.ninjatpl @@ -0,0 +1,48 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for item in items %} +
+
{% if item.prefix %}{{ item.prefix }}{% endif %}{{ item.value }}{% if item.suffix %}{{ item.suffix }}{% endif %}
+ {% if item.label %}
{{ item.label }}
{% endif %} +
+ {% endfor %} +
+
+
+{% if countUp %}{% endif %} diff --git a/templates/overrides/editorial/team.ninjatpl b/templates/overrides/editorial/team.ninjatpl new file mode 100644 index 0000000..aabb213 --- /dev/null +++ b/templates/overrides/editorial/team.ninjatpl @@ -0,0 +1,21 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
+ {% for member in members %} +
+ {% if member.photo %}{% img member.photo alt=member.name class="h-full w-full object-cover" %}{% endif %} +
+ {% if member.name %}

{{ member.name }}

{% endif %} + {% if member.role %}

{{ member.role }}

{% endif %} + {% if member.bio %}

{{ member.bio }}

{% endif %} + {% if member.socials %}
+ {% for social in member.socials %}{% if social.url %}{% endif %}{% endfor %} +
{% endif %} +
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/editorial/testimonial.ninjatpl b/templates/overrides/editorial/testimonial.ninjatpl new file mode 100644 index 0000000..fbc6829 --- /dev/null +++ b/templates/overrides/editorial/testimonial.ninjatpl @@ -0,0 +1,27 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} +
+ {% for q in quotes %} +
+ {% if q.rating %}
+ + + + + +
{% endif %} +
{% if q.quote %}“{{ q.quote }}”{% endif %}
+
+
+ {% if q.avatar %}{% img q.avatar alt=q.name class="h-full w-full object-cover" %}{% endif %} + + {% if q.name %}{{ q.name }}{% endif %} + {% if q.role %}{{ q.role }}{% endif %} + +
+
+ {% endfor %} +
+
+
diff --git a/templates/overrides/editorial/text.ninjatpl b/templates/overrides/editorial/text.ninjatpl index 806161a..44dc43f 100644 --- a/templates/overrides/editorial/text.ninjatpl +++ b/templates/overrides/editorial/text.ninjatpl @@ -1 +1,2 @@ -
{{ text|safe }}
+{# text override — Source Serif reading measure with oxblood underlined links. Fields: text, proseClass (mirrors builtin). Dropcap is reserved for the article template, not forced on every text block. #} +
{{ text|safe }}
diff --git a/templates/overrides/editorial/timeline.ninjatpl b/templates/overrides/editorial/timeline.ninjatpl new file mode 100644 index 0000000..6d05a05 --- /dev/null +++ b/templates/overrides/editorial/timeline.ninjatpl @@ -0,0 +1,21 @@ +
+
+ {% if heading %}

{{ heading }}

{% endif %} + {% if intro %}

{{ intro }}

{% endif %} +
    + {% for step in steps %} +
  1. + + {% if marker == "numbered" %}{{ forloop.counter }}{% elif marker == "dot" %}{% else %}{% endif %} + +
    + {% if step.label %}

    {{ step.label }}

    {% endif %} + {% if step.title %}

    {{ step.title }}

    {% endif %} + {% if step.text %}

    {{ step.text }}

    {% endif %} + {% if step.media %}
    {% img step.media alt=step.title class="w-full h-auto object-cover" %}
    {% endif %} +
    +
  2. + {% endfor %} +
+
+
diff --git a/templates/overrides/editorial/utility-bar.ninjatpl b/templates/overrides/editorial/utility-bar.ninjatpl new file mode 100644 index 0000000..fa18812 --- /dev/null +++ b/templates/overrides/editorial/utility-bar.ninjatpl @@ -0,0 +1,10 @@ +{# utility-bar override — quiet contact strip on a hairline-bordered panel, small-caps labels. Fields mirror builtin: variant, showOnMobile, leftItems/rightItems {icon,label,link}. #} +{% set ubcls = "editorial-panel" %} +{% if variant == "primary" %}{% set ubcls = "editorial-panel" %}{% elif variant == "dark" %}{% set ubcls = "editorial-panel" %}{% endif %} +{% macro ubitem(it) %}{% if it.link %}{% if it.icon %}::{{ it.icon }}::{% endif %}{% if it.label %}{{ it.label }}{% endif %}{% else %}{% if it.icon %}::{{ it.icon }}::{% endif %}{% if it.label %}{{ it.label }}{% endif %}{% endif %}{% endmacro %} +
+
+
{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}
+
{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}
+
+
diff --git a/templates/overrides/editorial/video-embed.ninjatpl b/templates/overrides/editorial/video-embed.ninjatpl new file mode 100644 index 0000000..fc6faae --- /dev/null +++ b/templates/overrides/editorial/video-embed.ninjatpl @@ -0,0 +1,23 @@ +{# video-embed override — editorial hairline frame; oxblood play medallion. Fields + facade JS mirror builtin verbatim: aspect, url, media, poster, title, caption. #} +{% with aspect=aspect|default:"16:9" %}
{% if caption %}
{{ caption }}
{% endif %}
{% endwith %}