). 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.byline|safe }}
+ {% if slots.header %}{% endif %}
+ {% if slots.byline %}{{ slots.byline|safe }}
{% endif %}
-
{% if slots.main %}{{ slots.main|safe }}{% else %}No content blocks assigned to this page.
{% endif %}
-
{{ slots.marginalia|safe }}
+
{% if slots.main %}{{ slots.main|safe }}{% else %}This essay has no content yet.
{% endif %}
+
{{ slots.marginalia|safe }}
-
+ {% if slots.footer %}{% 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 }}
+
+
+
+ {{ 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 %}{% endif %}
+
+
+ {% if title %}
+
+
{{ title }}
+ {% if page_meta.MetaDescription %}
{{ page_meta.MetaDescription }}
{% endif %}
+
+
+ {% endif %}
+ {% if slots.featured %}
{{ slots.featured|safe }} {% endif %}
+ {% if slots.main %}
{% else %}
{% endif %}
+
+
+ {% if slots.footer %}{% 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 %}{% 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 %}{% 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 }}
-
- {% if slots.main %}{{ slots.main|safe }}{% else %}No content blocks assigned to this page.
{% endif %}
-
+ {% if slots.header %}{% endif %}
+
+ {% if slots.main %}{{ slots.main|safe }}
{% else %}No content blocks assigned to this page.
{% endif %}
+
+ {% if slots.footer %}{% 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 }}
-
+ {% if slots.header %}{% endif %}
{% if slots.main %}{{ slots.main|safe }}{% else %}No content blocks assigned to this page.
{% endif %}
-
+ {% if slots.footer %}{% 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 }}
-
-
-
+ {% if slots.masthead %}{{ slots.masthead|safe }} {% endif %}
+
+ {% if slots.lead %}{% endif %}
+ {% if slots.river %}{% endif %}
+
+ {% if slots.footer %}{% 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 %}{% if logo_url %} {% elif site_name %}{{ site_name }} {% endif %} {{ body|safe }} {{ site_name }}
{% if unsubscribe_url %}Unsubscribe
{% endif %}
+
+
+
+
+
+
+
+
+ {{ site_name }}
+
+
+
+ {% if preview_text %}{{ preview_text }}
{% endif %}
+
+
+
+
+
+
+
+ {% if logo_url %} {% elif site_name %}{{ site_name }} {% endif %}
+
+
+
+
+
+
+
+
+
+
+ {{ body|safe }}
+
+
+
+
+
+
+
+
+
+
+
+
+
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 %}
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 %}
+
+{% 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 %}
+
+
+ {{ context.publicDisplayName|default:context.publicUsername|first|upper }}
+ {{ context.publicDisplayName|default:context.publicUsername }}
+
+
+
+
+
+{% 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 %}
+
+{% if heading %}{{ heading }}
{% endif %}
+
+{% if showAvatar and post.author_avatar %}
{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}
{% endif %}
+
+
{% if post.author_slug %}{{ post.author_name }} {% else %}{{ post.author_name }}{% endif %}
+
+{% if post.author_bio %}
{{ post.author_bio }}
{% endif %}
+
+
+
+{% 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 %}
+
+{% 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" %}
+
+
+{% if showHome %}{{ homeLabel|default:"Home" }} {% endif %}
+{% for item in breadcrumbs %}
+
+{% if showHome or not forloop.First %}{% if arrows %} {% else %}{{ separator|default:"/" }} {% endif %}{% endif %}
+{% if item.is_current %}{% if showCurrent %}{{ item.label }} {% endif %}{% else %}{{ item.label }} {% endif %}
+
+{% endfor %}
+
+
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 %}{{ text }} {% else %}{{ text }} {% 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" %}
+{% if link %}{% endif %}{% if media %}{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}
{% endif %}{% if title %}
{{ title }} {% endif %}{% if text %}
{{ text|safe }}
{% endif %}{% if linkLabel and link %}
{{ linkLabel }}::lucide:arrow-right:sm:: {% endif %}
{% if link %} {% endif %}
+{% 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 %}
+
+{% 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 %}{{ field.label }}{% if field.required %} * {% endif %} {% endif %}
+ {% if field.type == "textarea" %}
+
+ {% elif field.type == "select" %}
+
+ {% if field.placeholder %}{{ field.placeholder }} {% endif %}
+ {% for opt in field.options %}{{ opt.label|default:opt.value }} {% endfor %}
+
+ {% else %}
+
+ {% endif %}
+
+ {% endfor %}
+
Leave this field empty
+
+ {{ submitLabel|default:"Request a table" }}
+
+ {% 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" }}
+
+
+
{{ expiredMessage|default:"This has started." }}
+ {% if expiredUrl and expiredLabel %}
{{ expiredLabel }} {% endif %}
+
+
+
Counting down to {{ target }} .
+
+
+
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 %}
+
+
+
+
+
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" %}
+
+
::{{ icon|default:"lucide:diamond" }}:sm::
+
+{% 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 %}
{% 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 %}
+
+
+
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 %}
{% for l in legal_items %}{{ l.label }} {% endfor %} {% endif %}
{% endmacro %}
+
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 %}{% endif %}
+{% if trustLogos %}{% endif %}
+{% endmacro %}
+{% if variant == "split" %}
+
+{% if hasImage %}{% img backgroundImage alt=headlineText class="absolute inset-0 h-full w-full object-cover" layout="hero" %}{% endif %}
+
+
+{% 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 %}
+
+
+{% 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 preserved verbatim. Fields mirror builtin: heading, hours[].day/.hours/.closed, specialHoursNote, address, phone, directionsUrl, mapEmbedUrl, mapButtonLabel. #}
+
+
+ {% if heading %}
{{ heading }} {% endif %}
+
+
+ {% if hours %}
+
+
+ {% for row in hours %}
+
+ {{ row.day }}
+ {% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}
+
+ {% endfor %}
+
+
+ {% endif %}
+ {% if specialHoursNote %}
{{ specialHoursNote }}
{% endif %}
+ {% if address %}
{% endif %}
+ {% if phone %}
{% endif %}
+ {% if directionsUrl %}
::lucide:navigation:sm:: Get directions {% endif %}
+
+ {% if mapEmbedUrl %}
+
+ ::lucide:map-pin:md:: {{ mapButtonLabel|default:"Load map" }}
+
+ {% 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 %} {% 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 %}
{% endif %}
+
+
+
+{% 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) %}
+
{{ item.label }}
+{% if use_mega %}
{% for child in item.children %}
{% endfor %}
+{% else %}
{% for child in item.children %}{% if child.item_type == "header" %}
{{ child.label }}
{% else %}
{{ child.label }} {% endif %}{% endfor %}
{% endif %}
+
{% endmacro %}
+{% macro brand(size) %}{% if logoType == "text" and logoText.text %}{{ logoText.text }} {% elif logo %} {% elif companyName %}{{ companyName }} {% endif %} {% endmacro %}
+{% macro ctabtn(extra) %}{% if cta_text %}{% endif %}{% endmacro %}
+
+{% if showUtilityBar %}{{ utilityBarText|safe }}
{% endif %}
+
+
+{% if nv == "centered-logo" %}
+
{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}
+
{{ brand("h-9") }}
+
{{ ctabtn("") }}
+{% elif nv == "split" %}
+
{% for item in menu.items %}{% if forloop.Counter0 * 2 < menu.items|length %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endif %}{% endfor %}
+
{{ brand("h-9") }}
+
{% for item in menu.items %}{% if forloop.Counter0 * 2 >= menu.items|length %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}
+{% else %}
+
{{ brand("h-8") }}
+
{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}
+{% endif %}
+
+
+
+{# --- Mobile off-canvas drawer: pure-CSS checkbox baseline (works with no JS) --- #}
+
+
+
+
{{ brand("h-8") }}
Close menu
+
{% for item in menu.items %}{% if item.children %}{{ item.label }} {% for child in item.children %}{% if child.item_type != "header" %}
{{ child.label }} {% endif %}{% endfor %}
{% else %}{{ item.label }} {% endif %}{% endfor %}
+{% if cta_text %}
{% endif %}
+
+
+
+
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 %}
+
+{% 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. #}
+
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 %}
+
+{% 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 %}
+
+
+{% if post.published_at %}
{{ post.published_at|date:"M j, Y" }}
{% endif %}
+
+
+{% 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 %}
{{ post.published_at|date:"F j, Y" }} {% 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" }}
+
+
+
+{% 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 @@
+
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 title %}{{ title }} {% endif %}
+
+
+{% 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 @@
+
+ {% if heading %}
{{ heading }}
{% endif %}
+
+
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 %}
+
+
+ {% 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 %}
+
+
+ {% 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 poster %}{% img poster alt=title|default:"" class="absolute inset-0 h-full w-full object-cover" %}{% endif %}::lucide:play:xl::
{% if caption %}{{ caption }} {% endif %} {% endwith %}