) because the codeless loader registers definitions verbatim.
blocks:
- - key: terminal:ascii_header
- title: ASCII Header
- description: Figlet-style TTY banner with a shell prompt line
- category: layout
- schema: ascii_header.schema.json
- template: ascii_header.ninjatpl
- key: terminal:manpage_header
title: Man-page Header
description: NAME(section) ... center ... version row, like a man-page top line
category: layout
schema: manpage_header.schema.json
template: manpage_header.ninjatpl
- - key: terminal:toc
- title: Section TOC
- description: Fixed left-rail table of contents for article pages
- category: navigation
- schema: toc.schema.json
- template: toc.ninjatpl
- key: terminal:code_console
title: Console Block
description: Shell prompt + command + output, with language hint
@@ -35,14 +23,8 @@ blocks:
schema: keybind_table.schema.json
template: keybind_table.ninjatpl
- key: terminal:boot_log
- title: Boot Log Hero
+ title: Boot Log
description: Sequential dmesg-style boot lines revealed one after another
category: content
schema: boot_log.schema.json
template: boot_log.ninjatpl
- - key: terminal:footer
- title: TTY Footer
- description: EOF rule, MOTD, optional newsletter signup
- category: layout
- schema: footer.schema.json
- template: footer.ninjatpl
diff --git a/blocks/footer.ninjatpl b/blocks/footer.ninjatpl
deleted file mode 100644
index 6be565b..0000000
--- a/blocks/footer.ninjatpl
+++ /dev/null
@@ -1 +0,0 @@
-{% set m = motd|default:"connection closed." %}
diff --git a/blocks/footer.schema.json b/blocks/footer.schema.json
deleted file mode 100644
index f859b02..0000000
--- a/blocks/footer.schema.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "title": "TTY Footer",
- "description": "EOF rule + motd + optional newsletter signup",
- "type": "object",
- "properties": {
- "motd": {
- "type": "string",
- "title": "MOTD",
- "description": "Message-of-the-day shown in the footer",
- "default": "connection closed.",
- "x-editor": "text"
- },
- "links": {
- "type": "array",
- "title": "Links",
- "description": "Footer links",
- "default": [],
- "x-editor": "collection",
- "items": {
- "type": "object",
- "properties": {
- "label": {
- "type": "string",
- "title": "Label",
- "x-editor": "text"
- },
- "url": {
- "type": "string",
- "title": "URL",
- "x-editor": "link"
- }
- },
- "required": ["label"]
- }
- },
- "showSignup": {
- "type": "string",
- "title": "Show newsletter signup",
- "description": "Render a minimal email signup row",
- "default": "false",
- "x-editor": "select",
- "enum": ["true", "false"]
- }
- }
-}
diff --git a/blocks/toc.ninjatpl b/blocks/toc.ninjatpl
deleted file mode 100644
index 20afef6..0000000
--- a/blocks/toc.ninjatpl
+++ /dev/null
@@ -1 +0,0 @@
-{{ heading|default:"Sections" }} {% if items %}{% else %}// no sections yet
{% endif %}
diff --git a/blocks/toc.schema.json b/blocks/toc.schema.json
deleted file mode 100644
index 5e670c9..0000000
--- a/blocks/toc.schema.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "title": "Section TOC",
- "description": "Fixed left-rail table of contents for article pages",
- "type": "object",
- "properties": {
- "heading": {
- "type": "string",
- "title": "Heading",
- "description": "Heading shown above the list",
- "default": "Sections",
- "x-editor": "text"
- },
- "items": {
- "type": "array",
- "title": "Items",
- "description": "Anchor links into the page body",
- "default": [],
- "x-editor": "collection",
- "items": {
- "type": "object",
- "properties": {
- "label": {
- "type": "string",
- "title": "Label",
- "x-editor": "text"
- },
- "anchor": {
- "type": "string",
- "title": "Anchor",
- "description": "Anchor slug (no leading '#')",
- "x-editor": "slug"
- }
- },
- "required": ["label"]
- }
- }
- }
-}
diff --git a/fonts.json b/fonts.json
index fe51488..69ab1b1 100644
--- a/fonts.json
+++ b/fonts.json
@@ -1 +1,19 @@
-[]
+[
+ {
+ "name": "JetBrains Mono",
+ "family": "JetBrains Mono",
+ "variants": [
+ { "weight": "400", "style": "normal", "file": "fonts/jetbrains-mono-latin.woff2" },
+ { "weight": "500", "style": "normal", "file": "fonts/jetbrains-mono-latin.woff2" },
+ { "weight": "700", "style": "normal", "file": "fonts/jetbrains-mono-latin.woff2" }
+ ]
+ },
+ {
+ "name": "IBM Plex Mono",
+ "family": "IBM Plex Mono",
+ "variants": [
+ { "weight": "400", "style": "normal", "file": "fonts/ibm-plex-mono-400.woff2" },
+ { "weight": "500", "style": "normal", "file": "fonts/ibm-plex-mono-500.woff2" }
+ ]
+ }
+]
diff --git a/manifest.yaml b/manifest.yaml
index decb8b1..b0b7ddb 100644
--- a/manifest.yaml
+++ b/manifest.yaml
@@ -1,41 +1,109 @@
-# Terminal theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
+# Terminal theme — codeless .bnp manifest (WO-TF-025, Wave A).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
+required_icon_packs: ["lucide", "simple-icons"]
system_templates:
- key: terminal
title: Terminal
- description: "Phosphor green on black terminal aesthetic with ASCII art and monospace type."
+ description: "Phosphor-on-black TTY theme for dev tools and OSS docs — window chrome, man-page article layouts, keybind tables, boot logs, monospace type, and a typed-terminal canvas hero. Paper-teletype light, phosphor dark."
page_templates:
- system: terminal
key: default
title: Default
- description: "Standard 80-col centered TTY layout"
+ description: "Centered 80-col TTY page with window chrome and a sticky header"
+ slots: [header, main, footer]
+ - system: terminal
+ key: full-width
+ title: Full Width
+ description: "Edge-to-edge layout for tool dashboards and status pages"
slots: [header, main, footer]
- system: terminal
key: landing
title: Landing
- description: "ASCII hero + feature grid for project README sites"
- slots: [hero, main, cta, footer]
+ description: "Typed-terminal canvas hero, feature grid, boot log, and CTA for README-style project sites"
+ slots: [header, main, footer]
- system: terminal
key: article
title: Article (man-page)
- description: "Man-page two-column with section nav, ideal for writeups"
- slots: [header, toc, main, footer]
+ description: "Man-page two-column layout with a section rail, ideal for docs and writeups"
+ slots: [header, main, aside, footer]
- system: terminal
- key: full-width
- title: Full Width
- description: "Edge-to-edge for tool dashboards & status pages"
+ key: blog-index
+ title: Blog Index
+ description: "Changelog-style post feed with a sidebar"
+ slots: [header, main, footer]
+ - system: terminal
+ key: contact
+ title: Contact
+ description: "Terminal contact console — form panel plus a contact card"
+ slots: [header, main, footer]
+ - system: terminal
+ key: auth
+ title: Auth
+ description: "Centered login/register/reset console in a TTY window"
slots: [header, main, footer]
template_overrides:
+ # Chrome (4)
+ - { template: terminal, block: navbar }
+ - { template: terminal, block: footer }
+ - { template: terminal, block: utility-bar }
+ - { template: terminal, block: announcement-bar }
+ # Marketing (12)
+ - { template: terminal, block: hero }
+ - { template: terminal, block: feature-grid }
+ - { template: terminal, block: rich-section }
+ - { template: terminal, block: stats }
+ - { template: terminal, block: testimonial }
+ - { template: terminal, block: logo-strip }
+ - { template: terminal, block: pricing }
+ - { template: terminal, block: team }
+ - { template: terminal, block: faq }
+ - { template: terminal, block: timeline }
+ - { template: terminal, block: cta }
+ - { template: terminal, block: countdown }
+ # Primitives (11)
- { template: terminal, block: heading }
- { template: terminal, block: text }
- - { template: terminal, block: button }
+ - { template: terminal, block: rich-text }
- { template: terminal, block: image }
+ - { template: terminal, block: quote }
+ - { template: terminal, block: divider }
+ - { template: terminal, block: button }
+ - { template: terminal, block: card }
+ - { template: terminal, block: gallery }
+ - { template: terminal, block: video-embed }
+ - { template: terminal, block: audio-embed }
+ # Commerce / local (6)
+ - { template: terminal, block: menu-list }
+ - { template: terminal, block: event-list }
+ - { template: terminal, block: contact-form }
+ - { template: terminal, block: contact-card }
+ - { template: terminal, block: hours-location }
+ - { template: terminal, block: social-links }
+ # Blog family (12)
+ - { template: terminal, block: blog-hero }
+ - { template: terminal, block: featured-posts }
+ - { template: terminal, block: related-posts }
+ - { template: terminal, block: popular-posts }
+ - { template: terminal, block: category-list }
+ - { template: terminal, block: post-hero }
+ - { template: terminal, block: post-metadata }
+ - { template: terminal, block: author-bio }
+ - { template: terminal, block: author-bio-hero }
+ - { template: terminal, block: newsletter-signup }
+ - { template: terminal, block: breadcrumbs }
+ - { template: terminal, block: sidebar-nav }
+ # Auth surface (3)
+ - { template: terminal, block: auth-form }
+ - { template: terminal, block: password-reset-form }
+ - { template: terminal, block: auth-status }
+ # System (1)
+ - { template: terminal, block: page-suggestions }
email_wrappers:
- terminal
@@ -43,42 +111,48 @@ email_wrappers:
css:
input_css_append: |
/* ============================================================
- Terminal Theme — Phosphor green TTY aesthetic
+ Terminal — Phosphor-on-black TTY theme (WO-TF-025).
+ All colours consume the host shadcn HSL tokens via
+ hsl(var(--token)). No hardcoded hex / rgb / named colours.
+ Dual-mode: light = paper teletype, dark = phosphor CRT.
============================================================ */
- /* --- Globals: square corners, mono stack --- */
+ /* --- Globals: square corners, mono stack ------------------ */
- :root {
- --radius: 0;
- }
+ :root { --radius: 0; }
.terminal-page {
font-family: var(--font-body, "IBM Plex Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
letter-spacing: 0.01em;
}
- .terminal-page * {
- border-radius: 0 !important;
- }
+ .terminal-page * { border-radius: 0 !important; }
- /* --- Typography helpers --- */
+ /* --- Typography helpers ----------------------------------- */
.terminal-heading {
font-family: var(--font-heading, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
text-transform: uppercase;
- letter-spacing: 0.08em;
- color: hsl(var(--foreground));
+ letter-spacing: 0.06em;
}
.terminal-mono {
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
}
- .terminal-prose {
- font-family: var(--font-body, "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
- color: hsl(var(--foreground));
- line-height: 1.6;
- max-width: 80ch;
+ .terminal-eyebrow {
+ font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size: 0.75rem;
+ color: hsl(var(--accent));
+ }
+
+ .terminal-prose,
+ .terminal-prose p,
+ .terminal-prose li {
+ font-family: var(--font-body, "IBM Plex Mono", ui-monospace, monospace);
+ line-height: 1.7;
}
.terminal-prose a {
@@ -87,7 +161,85 @@ css:
text-underline-offset: 0.2em;
}
- /* --- ASCII frame (image override + figure wrappers) --- */
+ .terminal-prose code,
+ code.terminal-mono {
+ color: hsl(var(--accent));
+ background-color: hsl(var(--muted));
+ padding: 0.1rem 0.35rem;
+ border: 1px solid hsl(var(--border));
+ font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
+ font-size: 0.875em;
+ }
+
+ /* --- Caret blink (element opacity; used as _ ) --- */
+
+ @keyframes caret-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
+
+ .caret-blink {
+ display: inline-block;
+ animation: caret-blink 1s step-end infinite;
+ }
+
+ /* --- TTY window chrome (signature) ------------------------ */
+
+ .tty-window {
+ border: 1px solid hsl(var(--border));
+ background-color: hsl(var(--card));
+ color: hsl(var(--card-foreground));
+ }
+
+ .tty-titlebar {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.4rem 0.75rem;
+ border-bottom: 1px solid hsl(var(--border));
+ background-color: hsl(var(--muted));
+ color: hsl(var(--muted-foreground));
+ font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
+ font-size: 0.75rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ }
+
+ .tty-dot {
+ display: inline-block;
+ width: 0.6rem;
+ height: 0.6rem;
+ border: 1px solid hsl(var(--border));
+ background-color: hsl(var(--secondary));
+ }
+
+ /* --- TTY frame: bordered panel with hover glow ------------ */
+
+ .tty-frame {
+ border: 1px solid hsl(var(--border));
+ background-color: hsl(var(--card));
+ transition: box-shadow 180ms ease, border-color 180ms ease;
+ }
+ .tty-frame:hover,
+ .tty-frame:focus-within {
+ border-color: hsl(var(--primary));
+ box-shadow: 0 0 0 1px hsl(var(--primary) / 0.5), 0 0 18px hsl(var(--primary) / 0.18);
+ }
+
+ /* --- ASCII corner brackets (accent + on the corners) ------ */
+
+ .tty-corners { position: relative; }
+ .tty-corners::before,
+ .tty-corners::after {
+ content: "+";
+ position: absolute;
+ color: hsl(var(--primary));
+ font-family: var(--font-mono, ui-monospace, monospace);
+ font-weight: 700;
+ line-height: 1;
+ pointer-events: none;
+ }
+ .tty-corners::before { top: -0.55em; left: -0.35em; }
+ .tty-corners::after { bottom: -0.55em; right: -0.35em; }
+
+ /* --- ASCII figure frame (image override) ------------------ */
.ascii-frame {
display: inline-block;
@@ -96,26 +248,6 @@ css:
background-color: hsl(var(--card));
position: relative;
}
-
- .ascii-frame::before,
- .ascii-frame::after {
- content: "+";
- position: absolute;
- color: hsl(var(--border));
- font-family: var(--font-mono, ui-monospace, monospace);
- font-weight: 700;
- }
-
- .ascii-frame::before {
- top: -0.5em;
- left: -0.5em;
- }
-
- .ascii-frame::after {
- bottom: -0.5em;
- right: -0.5em;
- }
-
.ascii-frame figcaption {
display: block;
margin-top: 0.5rem;
@@ -125,12 +257,25 @@ css:
text-align: center;
}
- /* --- CRT scanlines overlay --- */
+ /* --- TTY scanlines overlay element (hero/landing) --------- */
- .crt-scanlines {
- position: relative;
+ .tty-scanlines {
+ opacity: 0.06;
+ background-image: linear-gradient(
+ to bottom,
+ hsl(var(--foreground)) 0px,
+ hsl(var(--foreground)) 1px,
+ transparent 1px,
+ transparent 3px
+ );
+ background-size: 100% 3px;
+ animation: tty-scan-drift 6s linear infinite;
}
+ @keyframes tty-scan-drift { 0% { background-position: 0 0; } 100% { background-position: 0 3px; } }
+ /* --- Page-level CRT scanline wash (subtle, whole page) ---- */
+
+ .crt-scanlines { position: relative; }
.crt-scanlines::before {
content: "";
position: fixed;
@@ -141,102 +286,47 @@ css:
to bottom,
transparent 0,
transparent 2px,
- hsl(var(--background) / 0.15) 2px,
- hsl(var(--background) / 0.15) 3px
+ hsl(var(--foreground) / 0.04) 2px,
+ hsl(var(--foreground) / 0.04) 3px
);
}
- /* --- Caret blink keyframe + utility --- */
+ /* --- Status dot pulse ------------------------------------- */
- @keyframes caret-blink {
- 0%, 50% { opacity: 1; }
- 50.01%, 100% { opacity: 0; }
- }
+ .terminal-status-dot { animation: terminal-pulse 2.2s ease-in-out infinite; }
+ @keyframes terminal-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
- .caret-blink::after {
- content: "_";
- display: inline-block;
- margin-left: 0.15em;
- animation: caret-blink 1s step-end infinite;
- animation-name: caret-blink;
- color: hsl(var(--foreground));
- }
+ /* --- Typed-terminal canvas showpiece (hero override only) - */
- /* --- Bracketed button (override) --- */
-
- .terminal-button {
- font-family: var(--font-mono, ui-monospace, monospace);
- background: transparent;
- color: hsl(var(--foreground));
- border: 1px solid hsl(var(--border));
- padding: 0.4rem 0.8rem;
- text-decoration: none;
- display: inline-block;
- text-transform: uppercase;
- letter-spacing: 0.08em;
- position: relative;
- }
-
- .terminal-button:hover {
- background: hsl(var(--foreground));
- color: hsl(var(--background));
- }
-
- .terminal-button:hover::after {
- content: "_";
+ .tty-rain-wrap {
position: absolute;
- margin-left: 0.2em;
- animation: caret-blink 1s step-end infinite;
- animation-name: caret-blink;
+ inset: 0;
+ overflow: hidden;
+ pointer-events: none;
+ }
+ .tty-rain-canvas {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ display: block;
+ opacity: 0.45;
+ }
+ .tty-rain-fallback {
+ position: absolute;
+ inset: 0;
+ background:
+ radial-gradient(120% 80% at 50% 0%, hsl(var(--primary) / 0.16), transparent 60%),
+ linear-gradient(to bottom, transparent, hsl(var(--background) / 0.7));
}
- /* --- Bracketed nav --- */
-
- .terminal-nav {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- font-family: var(--font-mono, ui-monospace, monospace);
- }
-
- .terminal-nav a {
- color: hsl(var(--foreground));
- text-decoration: none;
- white-space: nowrap;
- }
-
- .terminal-nav a:hover {
- color: hsl(var(--accent));
- }
-
- /* --- ASCII header block --- */
-
- .ascii-header {
- font-family: var(--font-mono, ui-monospace, monospace);
- color: hsl(var(--accent));
- padding: 1rem 0;
- border-bottom: 1px solid hsl(var(--border));
- white-space: pre;
- overflow-x: auto;
- }
-
- .ascii-header pre {
- margin: 0;
- font-family: inherit;
- line-height: 1.1;
- }
-
- .ascii-header .prompt-line {
- margin-top: 0.5rem;
- color: hsl(var(--foreground));
- }
-
- /* --- Man-page header --- */
+ /* --- Man-page header block -------------------------------- */
.manpage-header {
font-family: var(--font-mono, ui-monospace, monospace);
+ border-top: 1px solid hsl(var(--border));
border-bottom: 1px solid hsl(var(--border));
- padding: 0.75rem 0;
+ padding: 0.6rem 0;
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
@@ -245,17 +335,13 @@ css:
letter-spacing: 0.1em;
color: hsl(var(--foreground));
}
-
.manpage-header .left,
.manpage-header .center,
- .manpage-header .right {
- flex: 1;
- }
-
+ .manpage-header .right { flex: 1; }
.manpage-header .center { text-align: center; }
.manpage-header .right { text-align: right; }
- /* --- TOC (left rail on article) --- */
+ /* --- Section rail (article aside furniture) --------------- */
.terminal-toc {
font-family: var(--font-mono, ui-monospace, monospace);
@@ -263,8 +349,9 @@ css:
padding: 1rem;
border: 1px solid hsl(var(--border));
background: hsl(var(--card));
+ position: sticky;
+ top: 1rem;
}
-
.terminal-toc h4 {
text-transform: uppercase;
letter-spacing: 0.1em;
@@ -272,33 +359,13 @@ css:
margin: 0 0 0.5rem;
font-size: 0.875rem;
}
+ .terminal-toc ul { list-style: none; margin: 0; padding: 0; }
+ .terminal-toc li { margin: 0.2rem 0; }
+ .terminal-toc a { color: hsl(var(--muted-foreground)); text-decoration: none; }
+ .terminal-toc a:hover { color: hsl(var(--foreground)); }
+ @media (max-width: 768px) { .terminal-toc { display: none; } }
- .terminal-toc ul {
- list-style: none;
- margin: 0;
- padding: 0;
- }
-
- .terminal-toc li {
- margin: 0.2rem 0;
- }
-
- .terminal-toc a {
- color: hsl(var(--muted-foreground));
- text-decoration: none;
- }
-
- .terminal-toc a:hover {
- color: hsl(var(--foreground));
- }
-
- @media (max-width: 768px) {
- .terminal-toc {
- display: none;
- }
- }
-
- /* --- Code console block --- */
+ /* --- Code console block ----------------------------------- */
.code-console {
font-family: var(--font-mono, ui-monospace, monospace);
@@ -309,23 +376,9 @@ css:
overflow-x: auto;
color: hsl(var(--card-foreground));
}
-
- .code-console .cmd-line {
- display: flex;
- align-items: center;
- gap: 0.5em;
- color: hsl(var(--accent));
- margin: 0;
- }
-
- .code-console .cmd-prompt {
- color: hsl(var(--accent));
- }
-
- .code-console .cmd-text {
- color: hsl(var(--foreground));
- }
-
+ .code-console .cmd-line { display: flex; align-items: baseline; gap: 0.5em; margin: 0; }
+ .code-console .cmd-prompt { color: hsl(var(--accent)); }
+ .code-console .cmd-text { color: hsl(var(--foreground)); }
.code-console pre {
margin: 0.5rem 0 0;
white-space: pre-wrap;
@@ -333,7 +386,7 @@ css:
font-family: inherit;
}
- /* --- Keybinding table --- */
+ /* --- Keybinding table ------------------------------------- */
.keybind-table {
font-family: var(--font-mono, ui-monospace, monospace);
@@ -343,7 +396,6 @@ css:
max-width: 80ch;
color: hsl(var(--foreground));
}
-
.keybind-table th,
.keybind-table td {
border-bottom: 1px dashed hsl(var(--border));
@@ -351,14 +403,12 @@ css:
text-align: left;
vertical-align: top;
}
-
.keybind-table th {
text-transform: uppercase;
letter-spacing: 0.08em;
color: hsl(var(--accent));
font-weight: 600;
}
-
.keybind-table kbd {
font-family: inherit;
background: hsl(var(--muted));
@@ -369,7 +419,7 @@ css:
font-size: 0.875em;
}
- /* --- Boot log --- */
+ /* --- Boot log --------------------------------------------- */
.boot-log {
font-family: var(--font-mono, ui-monospace, monospace);
@@ -381,94 +431,54 @@ css:
white-space: pre-wrap;
overflow-x: auto;
}
-
.boot-log .boot-line {
display: block;
opacity: 0;
animation: boot-line-in 0.2s ease-out forwards;
}
-
@keyframes boot-line-in {
from { opacity: 0; transform: translateY(2px); }
to { opacity: 1; transform: translateY(0); }
}
-
.boot-log .boot-cursor {
display: inline-block;
color: hsl(var(--accent));
animation: caret-blink 1s step-end infinite;
- animation-name: caret-blink;
}
- /* --- Footer --- */
+ /* --- Layout helpers --------------------------------------- */
- .terminal-footer {
- font-family: var(--font-mono, ui-monospace, monospace);
- color: hsl(var(--muted-foreground));
- border-top: 1px solid hsl(var(--border));
- padding: 1rem 0;
- margin-top: 2rem;
- text-align: center;
- }
-
- .terminal-footer .terminal-footer-rule {
- color: hsl(var(--border));
- margin: 0.25rem 0;
- letter-spacing: 0.2em;
- }
-
- .terminal-footer .terminal-footer-motd {
- color: hsl(var(--foreground));
- margin: 0.5rem 0;
- }
-
- .terminal-footer .terminal-footer-links {
- margin: 0.5rem 0;
- display: flex;
- flex-wrap: wrap;
- gap: 1rem;
- justify-content: center;
- }
-
- .terminal-footer a {
- color: hsl(var(--muted-foreground));
- text-decoration: none;
- }
-
- .terminal-footer a:hover {
- color: hsl(var(--foreground));
- }
-
- .terminal-footer .terminal-footer-signup {
- margin: 0.75rem auto;
- max-width: 40ch;
- }
-
- .terminal-footer .terminal-footer-signup input[type="email"] {
- font-family: inherit;
- background: hsl(var(--input));
- color: hsl(var(--foreground));
- border: 1px solid hsl(var(--border));
- padding: 0.4rem 0.6rem;
- }
-
- /* --- Article layout grid --- */
+ .terminal-main-80 { max-width: 80ch; margin: 0 auto; }
.terminal-article-grid {
display: grid;
- grid-template-columns: 18ch 1fr;
+ grid-template-columns: 20ch 1fr;
gap: 2rem;
align-items: start;
}
+ @media (max-width: 768px) { .terminal-article-grid { grid-template-columns: 1fr; } }
- @media (max-width: 768px) {
- .terminal-article-grid {
- grid-template-columns: 1fr;
+ /* --- Reduced motion: kill all motion ---------------------- */
+
+ @media (prefers-reduced-motion: reduce) {
+ .caret-blink,
+ .tty-scanlines,
+ .terminal-status-dot,
+ .tty-rain-canvas,
+ .boot-log .boot-line,
+ .boot-log .boot-cursor {
+ animation: none !important;
+ animation-duration: 0.01ms !important;
}
+ .boot-log .boot-line { opacity: 1; transform: none; }
+ .tty-scanlines { opacity: 0; }
+ .tty-rain-canvas { display: none; }
}
- .terminal-main-80 {
- max-width: 80ch;
- margin: 0 auto;
- font-family: var(--font-body, ui-monospace, monospace);
+ /* --- High contrast: drop the CRT overlays ----------------- */
+
+ @media (prefers-contrast: more) {
+ .tty-scanlines,
+ .crt-scanlines::before,
+ .tty-rain-canvas { display: none; }
}
diff --git a/master_pages.json b/master_pages.json
index 7acd5c3..0b8a25c 100644
--- a/master_pages.json
+++ b/master_pages.json
@@ -1,25 +1,22 @@
[
{
- "key": "terminal:default-master",
- "title": "Terminal Default Master",
- "page_templates": ["default", "landing", "full-width"],
+ "key": "terminal:site-master",
+ "title": "Terminal Site Master",
+ "page_templates": ["default", "full-width", "landing", "article", "blog-index", "contact"],
"blocks": [
- { "block_key": "terminal:ascii_header", "title": "TTY Header", "content": { "title": "~/projects", "prompt": "$ " }, "slot": "header", "sort_order": 0 },
- { "block_key": "navbar", "title": "Main Nav", "content": { "menuName": "main", "style": "bracketed" }, "slot": "header", "sort_order": 1 },
- { "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "// content here" }, "slot": "main", "sort_order": 0 },
- { "block_key": "terminal:footer", "title": "TTY Footer", "content": { "motd": "connection closed.", "showSignup": true }, "slot": "footer", "sort_order": 0 }
+ { "block_key": "navbar", "title": "TTY Navbar", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "grid" }, "companyName": "grid", "stickyBehavior": "sticky", "borderBottom": true, "cta": { "text": "Install", "url": "/contact" } }, "slot": "header", "sort_order": 0 },
+ { "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "// page content" }, "slot": "main", "sort_order": 0 },
+ { "block_key": "footer", "title": "TTY Footer", "content": { "layout": "columns", "companyName": "grid", "tagline": "A fast parallel task runner and build cache for the terminal.", "copyright": "grid contributors" }, "slot": "footer", "sort_order": 0 }
]
},
{
- "key": "terminal:article-master",
- "title": "Terminal Article Master",
- "page_templates": ["article"],
+ "key": "terminal:auth-master",
+ "title": "Terminal Auth Master",
+ "page_templates": ["auth"],
"blocks": [
- { "block_key": "terminal:manpage_header", "title": "Man-page Header", "content": { "name": "WRITEUP", "section": 1, "version": "terminal v0.1.0" }, "slot": "header", "sort_order": 0 },
- { "block_key": "navbar", "title": "Main Nav", "content": { "menuName": "main", "style": "bracketed" }, "slot": "header", "sort_order": 1 },
- { "block_key": "terminal:toc", "title": "Section TOC", "content": { "heading": "Sections", "items": [] }, "slot": "toc", "sort_order": 0 },
- { "block_key": "slot", "title": "Article Body", "content": { "slotName": "main", "placeholder": "// article body" }, "slot": "main", "sort_order": 0 },
- { "block_key": "terminal:footer", "title": "TTY Footer", "content": { "motd": "connection closed.", "showSignup": false }, "slot": "footer", "sort_order": 0 }
+ { "block_key": "navbar", "title": "TTY Navbar", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "grid" }, "companyName": "grid" }, "slot": "header", "sort_order": 0 },
+ { "block_key": "slot", "title": "Auth Console", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
+ { "block_key": "footer", "title": "TTY Footer", "content": { "layout": "minimal", "companyName": "grid", "tagline": "A fast parallel task runner and build cache for the terminal.", "copyright": "grid contributors" }, "slot": "footer", "sort_order": 0 }
]
}
]
diff --git a/plugin.mod b/plugin.mod
index 36f482d..7ebf4d8 100644
--- a/plugin.mod
+++ b/plugin.mod
@@ -3,7 +3,8 @@ name = "terminal"
display_name = "Terminal"
scope = "@themes"
version = "0.2.0"
-description = "Phosphor green on black terminal aesthetic with ASCII art, monospace type, and man-page article layouts. For indie dev sites, CTF writeups, and CLI tool docs."
+description = "Phosphor-on-black TTY theme for dev tools and OSS docs. Window chrome, man-page article layouts, keybind tables, boot logs, monospace type, and a typed-terminal canvas hero. Paper-teletype light mode, phosphor dark mode."
kind = "theme"
categories = ["templates"]
-tags = ["terminal", "hacker", "monospace", "dev", "ctf", "security", "docs", "indie"]
+tags = ["terminal", "hacker", "monospace", "dev", "ctf", "docs", "devtools", "oss"]
+required_icon_packs = ["lucide", "simple-icons"]
diff --git a/presets.json b/presets.json
index d2942a7..a7c30b9 100644
--- a/presets.json
+++ b/presets.json
@@ -2,9 +2,30 @@
{
"id": "phosphor-green",
"name": "Phosphor Green",
- "description": "Classic green-on-black VT220 phosphor.",
+ "description": "Green printer-ribbon on paper for light, VT220 green phosphor for dark. The flagship terminal look.",
"theme": {
- "mode": "dark",
+ "mode": "both",
+ "lightColors": {
+ "background": "60 20% 97%",
+ "foreground": "120 30% 12%",
+ "card": "60 20% 99%",
+ "cardForeground": "120 30% 12%",
+ "popover": "60 20% 99%",
+ "popoverForeground": "120 30% 12%",
+ "primary": "130 60% 28%",
+ "primaryForeground": "60 20% 98%",
+ "secondary": "90 20% 90%",
+ "secondaryForeground": "120 30% 20%",
+ "muted": "60 15% 92%",
+ "mutedForeground": "120 12% 38%",
+ "accent": "140 65% 30%",
+ "accentForeground": "60 20% 98%",
+ "destructive": "0 70% 42%",
+ "destructiveForeground": "0 0% 100%",
+ "border": "90 15% 78%",
+ "input": "90 15% 84%",
+ "ring": "130 60% 28%"
+ },
"darkColors": {
"background": "0 0% 4%",
"foreground": "120 100% 50%",
@@ -29,11 +50,32 @@
}
},
{
- "id": "amber-mono",
- "name": "Amber Mono",
- "description": "Warm amber phosphor terminal.",
+ "id": "amber-crt",
+ "name": "Amber CRT",
+ "description": "Warm cream paper with amber ribbon for light, amber phosphor on warm black for dark.",
"theme": {
- "mode": "dark",
+ "mode": "both",
+ "lightColors": {
+ "background": "40 30% 96%",
+ "foreground": "30 25% 15%",
+ "card": "40 30% 99%",
+ "cardForeground": "30 25% 15%",
+ "popover": "40 30% 99%",
+ "popoverForeground": "30 25% 15%",
+ "primary": "30 80% 40%",
+ "primaryForeground": "40 30% 98%",
+ "secondary": "38 25% 90%",
+ "secondaryForeground": "30 25% 22%",
+ "muted": "40 20% 91%",
+ "mutedForeground": "30 15% 40%",
+ "accent": "25 85% 43%",
+ "accentForeground": "40 30% 98%",
+ "destructive": "0 70% 45%",
+ "destructiveForeground": "0 0% 100%",
+ "border": "38 20% 78%",
+ "input": "38 20% 84%",
+ "ring": "30 80% 40%"
+ },
"darkColors": {
"background": "30 20% 4%",
"foreground": "38 95% 60%",
@@ -60,9 +102,30 @@
{
"id": "paper-tty",
"name": "Paper TTY",
- "description": "Hard white on black; austere docs mode (no glow).",
+ "description": "Crisp black-on-white docs for light with a blue link accent, hard white on black for dark. Austere, no glow.",
"theme": {
- "mode": "dark",
+ "mode": "both",
+ "lightColors": {
+ "background": "0 0% 100%",
+ "foreground": "0 0% 10%",
+ "card": "0 0% 100%",
+ "cardForeground": "0 0% 10%",
+ "popover": "0 0% 100%",
+ "popoverForeground": "0 0% 10%",
+ "primary": "0 0% 12%",
+ "primaryForeground": "0 0% 98%",
+ "secondary": "0 0% 94%",
+ "secondaryForeground": "0 0% 12%",
+ "muted": "0 0% 95%",
+ "mutedForeground": "0 0% 40%",
+ "accent": "210 90% 42%",
+ "accentForeground": "0 0% 100%",
+ "destructive": "0 72% 46%",
+ "destructiveForeground": "0 0% 100%",
+ "border": "0 0% 85%",
+ "input": "0 0% 88%",
+ "ring": "210 90% 42%"
+ },
"darkColors": {
"background": "0 0% 5%",
"foreground": "0 0% 95%",
@@ -85,5 +148,105 @@
"ring": "0 0% 95%"
}
}
+ },
+ {
+ "id": "ice-terminal",
+ "name": "Ice Terminal",
+ "description": "Cool slate-on-white for light with a teal accent, cyan phosphor on blue-black for dark.",
+ "theme": {
+ "mode": "both",
+ "lightColors": {
+ "background": "200 30% 97%",
+ "foreground": "200 40% 14%",
+ "card": "200 30% 99%",
+ "cardForeground": "200 40% 14%",
+ "popover": "200 30% 99%",
+ "popoverForeground": "200 40% 14%",
+ "primary": "190 80% 30%",
+ "primaryForeground": "200 30% 98%",
+ "secondary": "195 25% 90%",
+ "secondaryForeground": "200 40% 20%",
+ "muted": "200 20% 92%",
+ "mutedForeground": "200 15% 40%",
+ "accent": "185 85% 32%",
+ "accentForeground": "200 30% 98%",
+ "destructive": "0 70% 45%",
+ "destructiveForeground": "0 0% 100%",
+ "border": "195 20% 78%",
+ "input": "195 20% 84%",
+ "ring": "190 80% 30%"
+ },
+ "darkColors": {
+ "background": "200 40% 5%",
+ "foreground": "185 90% 60%",
+ "card": "200 40% 8%",
+ "cardForeground": "185 90% 65%",
+ "popover": "200 40% 7%",
+ "popoverForeground": "185 90% 65%",
+ "primary": "185 90% 50%",
+ "primaryForeground": "200 40% 5%",
+ "secondary": "195 40% 15%",
+ "secondaryForeground": "185 90% 72%",
+ "muted": "200 30% 11%",
+ "mutedForeground": "185 40% 52%",
+ "accent": "175 95% 55%",
+ "accentForeground": "200 40% 5%",
+ "destructive": "0 85% 55%",
+ "destructiveForeground": "0 0% 100%",
+ "border": "190 60% 26%",
+ "input": "190 50% 18%",
+ "ring": "185 90% 50%"
+ }
+ }
+ },
+ {
+ "id": "magenta-hacker",
+ "name": "Magenta Hacker",
+ "description": "Ink-on-white with a magenta accent for light, magenta phosphor on black for dark.",
+ "theme": {
+ "mode": "both",
+ "lightColors": {
+ "background": "320 20% 98%",
+ "foreground": "320 30% 14%",
+ "card": "320 20% 99%",
+ "cardForeground": "320 30% 14%",
+ "popover": "320 20% 99%",
+ "popoverForeground": "320 30% 14%",
+ "primary": "320 70% 40%",
+ "primaryForeground": "320 20% 98%",
+ "secondary": "315 25% 92%",
+ "secondaryForeground": "320 30% 22%",
+ "muted": "320 15% 93%",
+ "mutedForeground": "320 12% 44%",
+ "accent": "300 75% 43%",
+ "accentForeground": "320 20% 98%",
+ "destructive": "0 70% 45%",
+ "destructiveForeground": "0 0% 100%",
+ "border": "315 18% 80%",
+ "input": "315 18% 85%",
+ "ring": "320 70% 40%"
+ },
+ "darkColors": {
+ "background": "300 25% 5%",
+ "foreground": "320 90% 65%",
+ "card": "300 25% 8%",
+ "cardForeground": "320 90% 70%",
+ "popover": "300 25% 7%",
+ "popoverForeground": "320 90% 70%",
+ "primary": "320 90% 55%",
+ "primaryForeground": "300 25% 5%",
+ "secondary": "310 35% 16%",
+ "secondaryForeground": "320 90% 76%",
+ "muted": "300 20% 12%",
+ "mutedForeground": "320 40% 55%",
+ "accent": "285 90% 62%",
+ "accentForeground": "300 25% 5%",
+ "destructive": "0 85% 58%",
+ "destructiveForeground": "0 0% 100%",
+ "border": "320 55% 28%",
+ "input": "320 45% 20%",
+ "ring": "320 90% 55%"
+ }
+ }
}
]
diff --git a/seed/seed.json b/seed/seed.json
new file mode 100644
index 0000000..1555349
--- /dev/null
+++ b/seed/seed.json
@@ -0,0 +1,240 @@
+{
+ "demo": true,
+ "settings": {
+ "merge": {
+ "companyName": "grid",
+ "tagline": "A fast parallel task runner and build cache for the terminal."
+ }
+ },
+ "data_tables": [
+ {
+ "key": "contact_submissions",
+ "name": "Contact Enquiries",
+ "description": "Seeded contact-form submissions for the grid demo. A submission becomes a row here and emails the site admins.",
+ "columns": [
+ { "key": "name", "label": "Name", "type": "text", "required": true },
+ { "key": "email", "label": "Email", "type": "email", "required": true },
+ { "key": "project", "label": "Project", "type": "text", "required": false },
+ { "key": "message", "label": "Message", "type": "text", "required": true }
+ ]
+ }
+ ],
+ "pages": [
+ {
+ "slug": "/",
+ "title": "grid",
+ "template_key": "landing",
+ "blocks": [
+ {
+ "block_key": "hero",
+ "title": "Hero",
+ "slot": "main",
+ "sort_order": 1,
+ "content": {
+ "template": "centered",
+ "minHeight": "screen",
+ "eyebrow": "v1.4.0",
+ "headline": "Run every task in parallel",
+ "subheadline": "grid is a task runner and build cache for the terminal.",
+ "description": "Define tasks once. grid runs the ones that changed, in parallel, and caches the rest. No config server, no daemon.",
+ "ctaPrimary": { "text": "Install", "url": "/contact" },
+ "ctaSecondary": { "text": "Read the docs", "url": "/blog" }
+ }
+ },
+ {
+ "block_key": "feature-grid",
+ "title": "Features",
+ "slot": "main",
+ "sort_order": 2,
+ "content": {
+ "eyebrow": "What it does",
+ "heading": "One binary, three jobs",
+ "intro": "Runs tasks, caches output, and watches files. Nothing else to install.",
+ "layout": "card",
+ "columns": 3,
+ "items": [
+ { "icon": "lucide:cpu", "title": "Parallel", "text": "Tasks that do not depend on each other run at the same time, up to your core count." },
+ { "icon": "lucide:database", "title": "Cached", "text": "Every task output is keyed by its inputs. Unchanged inputs skip the work." },
+ { "icon": "lucide:eye", "title": "Watch", "text": "Point grid at a file glob and it reruns the affected tasks on save." }
+ ]
+ }
+ },
+ {
+ "block_key": "terminal:code_console",
+ "title": "Install",
+ "slot": "main",
+ "sort_order": 3,
+ "content": {
+ "prompt": "$",
+ "language": "shell",
+ "command": "curl -fsSL https://grid.dev/install.sh | sh",
+ "output": "installing grid v1.4.0 ...\nok: /usr/local/bin/grid\nrun `grid init` in your project to get started"
+ }
+ },
+ {
+ "block_key": "terminal:boot_log",
+ "title": "Run log",
+ "slot": "main",
+ "sort_order": 4,
+ "content": {
+ "cursor": "block",
+ "lines": [
+ "grid run build test lint",
+ "build cached 0.00s",
+ "lint ok 0.31s",
+ "test ok 1.12s",
+ "3 tasks, 1 cached, 0 failed"
+ ]
+ }
+ },
+ {
+ "block_key": "cta",
+ "title": "CTA",
+ "slot": "main",
+ "sort_order": 5,
+ "content": {
+ "background": "band",
+ "heading": "Try it on your next build",
+ "text": "Install grid, run grid init, and watch your first cached run.",
+ "primaryLabel": "Install",
+ "primaryUrl": "/contact"
+ }
+ }
+ ]
+ },
+ {
+ "slug": "/about",
+ "title": "About grid",
+ "template_key": "default",
+ "blocks": [
+ { "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "About grid", "level": 1 } },
+ { "block_key": "rich-text", "title": "Story", "slot": "main", "sort_order": 2, "content": { "html": "grid started as a shell script that ran build steps in the background. The script grew until it needed a cache. Then it needed a dependency graph. At that point it became a real tool.
The goal has not changed. Run the work that changed, skip the work that did not, and stay out of the way. grid is open source and ships as a single static binary.
" } },
+ {
+ "block_key": "terminal:code_console",
+ "title": "Quickstart",
+ "slot": "main",
+ "sort_order": 3,
+ "content": { "prompt": "$", "language": "shell", "command": "grid init && grid run build", "output": "wrote grid.toml\nbuild ok 0.84s\n1 task, 0 cached, 0 failed" }
+ },
+ {
+ "block_key": "terminal:keybind_table",
+ "title": "Commands",
+ "slot": "main",
+ "sort_order": 4,
+ "content": {
+ "rows": [
+ { "keys": "grid run", "action": "Run the named tasks and their dependencies" },
+ { "keys": "grid watch", "action": "Rerun affected tasks when files change" },
+ { "keys": "grid graph", "action": "Print the task dependency graph" },
+ { "keys": "grid clean", "action": "Drop the local build cache" }
+ ]
+ }
+ },
+ { "block_key": "divider", "title": "Divider", "slot": "main", "sort_order": 5, "content": { "variant": "line", "weight": "medium" } },
+ { "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 6, "content": { "style": "pull", "quote": "Do the work once. Cache the rest.", "attribution": "grid README" } }
+ ]
+ },
+ {
+ "slug": "/blog",
+ "title": "Changelog",
+ "template_key": "blog-index",
+ "blocks": [
+ { "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "Changelog", "level": 1 } },
+ { "block_key": "card", "title": "Post: Cache keys", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "How grid keys the cache", "text": "Every task output is keyed by its inputs. Here is how the hash is built and why it is stable across machines.
", "link": "/blog/cache-keys", "linkLabel": "Read on" } },
+ { "block_key": "card", "title": "Post: Watch mode", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "Watch mode without a daemon", "text": "grid watch reruns affected tasks on save with no background process. This is how the file watcher stays cheap.
", "link": "/blog/watch-mode", "linkLabel": "Read on" } },
+ { "block_key": "card", "title": "Post: Parallel", "slot": "main", "sort_order": 4, "content": { "layout": "vertical", "title": "Scheduling tasks in parallel", "text": "Independent tasks run together, up to your core count. The scheduler is a topological sort with a worker pool.
", "link": "/blog/parallel-scheduling", "linkLabel": "Read on" } }
+ ]
+ },
+ {
+ "slug": "/blog/cache-keys",
+ "title": "How grid keys the cache",
+ "template_key": "article",
+ "parent_slug": "/blog",
+ "blocks": [
+ { "block_key": "terminal:manpage_header", "title": "Man header", "slot": "main", "sort_order": 1, "content": { "name": "CACHE-KEYS", "section": 7, "version": "grid v1.4.0" } },
+ { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 2, "content": { "text": "How grid keys the cache", "level": 1 } },
+ { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 3, "content": { "html": "A cache is only useful if the key is exact. grid hashes the task command, its input files, and the tool versions it declares. Same inputs, same key, cached output.
The key is content based, not time based. Two machines that check out the same commit build the same key. That makes the cache shareable across a team.
" } },
+ { "block_key": "terminal:code_console", "title": "Example", "slot": "main", "sort_order": 4, "content": { "prompt": "$", "language": "shell", "command": "grid run build --explain", "output": "build key: sha256:9f2c...\ninputs: src/**, grid.toml, go 1.26\nresult: cache hit" } }
+ ]
+ },
+ {
+ "slug": "/blog/watch-mode",
+ "title": "Watch mode without a daemon",
+ "template_key": "article",
+ "parent_slug": "/blog",
+ "blocks": [
+ { "block_key": "terminal:manpage_header", "title": "Man header", "slot": "main", "sort_order": 1, "content": { "name": "WATCH", "section": 1, "version": "grid v1.4.0" } },
+ { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 2, "content": { "text": "Watch mode without a daemon", "level": 1 } },
+ { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 3, "content": { "html": "Most watchers run a background process that never exits. grid does not. grid watch runs in the foreground and exits when you press q.
On each save it maps the changed file back to the tasks that read it, then reruns only those. The rest stay cached.
" } },
+ { "block_key": "terminal:code_console", "title": "Example", "slot": "main", "sort_order": 4, "content": { "prompt": "$", "language": "shell", "command": "grid watch 'src/**/*.go'", "output": "watching 42 files\nchanged: src/api/handler.go\ntest ok 0.90s" } }
+ ]
+ },
+ {
+ "slug": "/blog/parallel-scheduling",
+ "title": "Scheduling tasks in parallel",
+ "template_key": "article",
+ "parent_slug": "/blog",
+ "blocks": [
+ { "block_key": "terminal:manpage_header", "title": "Man header", "slot": "main", "sort_order": 1, "content": { "name": "SCHEDULER", "section": 7, "version": "grid v1.4.0" } },
+ { "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 2, "content": { "text": "Scheduling tasks in parallel", "level": 1 } },
+ { "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 3, "content": { "html": "grid builds a graph of tasks and their dependencies. It runs a task the moment its dependencies finish. Independent branches run at the same time.
The worker pool is sized to your core count by default. You can cap it with a flag when a task is memory heavy.
" } },
+ { "block_key": "terminal:code_console", "title": "Example", "slot": "main", "sort_order": 4, "content": { "prompt": "$", "language": "shell", "command": "grid run all --jobs 4", "output": "scheduled 9 tasks across 4 workers\nall ok 2.03s" } }
+ ]
+ },
+ {
+ "slug": "/contact",
+ "title": "Contact",
+ "template_key": "contact",
+ "blocks": [
+ {
+ "block_key": "contact-form",
+ "title": "Contact form",
+ "slot": "main",
+ "sort_order": 1,
+ "content": {
+ "heading": "Get in touch",
+ "description": "Ask a question or report a bug. We reply within one business day.",
+ "submitLabel": "Send",
+ "successMessage": "Message received. We will reply within one business day.",
+ "fields": [
+ { "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
+ { "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
+ { "name": "project", "label": "Project", "type": "text", "required": false, "width": "full" },
+ { "name": "message", "label": "Message", "type": "textarea", "required": true, "width": "full" }
+ ],
+ "formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
+ }
+ },
+ {
+ "block_key": "contact-card",
+ "title": "Reach us",
+ "slot": "main",
+ "sort_order": 2,
+ "content": {
+ "heading": "Other ways in",
+ "columns": 3,
+ "channels": [
+ { "icon": "lucide:mail", "label": "Email", "value": "team@grid.dev", "link": "mailto:team@grid.dev" },
+ { "icon": "simple-icons:github", "label": "Source", "value": "github.com/grid-dev/grid", "link": "https://github.com/grid-dev/grid" },
+ { "icon": "lucide:message-square", "label": "Chat", "value": "Join the Discord", "link": "#" }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "slug": "/login",
+ "title": "Sign in",
+ "template_key": "auth",
+ "blocks": [
+ { "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } }
+ ]
+ }
+ ],
+ "menu_items": [
+ { "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
+ { "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 2 },
+ { "menu": "main", "label": "Docs", "page_slug": "/blog", "sort_order": 3 },
+ { "menu": "main", "label": "Contact", "page_slug": "/contact", "sort_order": 4 }
+ ]
+}
diff --git a/seed/workflows.json b/seed/workflows.json
new file mode 100644
index 0000000..c277456
--- /dev/null
+++ b/seed/workflows.json
@@ -0,0 +1,19 @@
+{
+ "workflows": [
+ {
+ "key": "contact_notify_admins",
+ "name": "Contact enquiry to admins",
+ "description": "On a contact-form submission, email the site admins.",
+ "enabled": true,
+ "trigger": { "type": "row_inserted", "table": "contact_submissions" },
+ "steps": [
+ {
+ "type": "email",
+ "name": "Email admins",
+ "template": "Form Submission Notification",
+ "recipients": { "mode": "admins", "scope": "all" }
+ }
+ ]
+ }
+ ]
+}
diff --git a/templates/overrides/terminal/announcement-bar.ninjatpl b/templates/overrides/terminal/announcement-bar.ninjatpl
new file mode 100644
index 0000000..bc46c0e
--- /dev/null
+++ b/templates/overrides/terminal/announcement-bar.ninjatpl
@@ -0,0 +1,21 @@
+{# announcement_bar (WO-TF-006): dismissable banner; dismissal persists in localStorage keyed by a content hash (variant + slugified message) so it reappears when the message changes; no-JS fallback is a static non-dismissable strip. Terminal override: mono broadcast readout — data-* + script preserved. #}
+{% set anncls = "bg-primary/10 text-foreground" %}
+{% if variant == "promo" %}{% set anncls = "bg-primary text-primary-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
+
+
+
{{ message }}{% if linkText and linkUrl %} {{ linkText }} {% endif %}
+{% if dismissable %}
{% endif %}
+
+
+{% if dismissable %}{% endif %}
diff --git a/templates/overrides/terminal/audio-embed.ninjatpl b/templates/overrides/terminal/audio-embed.ninjatpl
new file mode 100644
index 0000000..6a9944c
--- /dev/null
+++ b/templates/overrides/terminal/audio-embed.ninjatpl
@@ -0,0 +1,16 @@
+{# audio-embed terminal override: neon panel, mono meta. Facade script + data-bn-* attributes + {% img %} preserved byte-for-byte. #}
+{% 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/terminal/auth-form.ninjatpl b/templates/overrides/terminal/auth-form.ninjatpl
new file mode 100644
index 0000000..bab56d7
--- /dev/null
+++ b/templates/overrides/terminal/auth-form.ninjatpl
@@ -0,0 +1,77 @@
+{# auth-form terminal override — reproduces the builtin's security-critical wiring: hardcoded HTMX endpoints /api/auth/login + /api/auth/register (target #auth-message), /forgot-password link, OAuth start URLs gated on context.site.*, and the exact Cap widget markup when captchaEnabled. Only the chrome is themed. #}
+{% if context.isPublicLoggedIn %}
+
+
Session active as {{ context.publicUsername }}
+
My Account
+
+{% else %}
+{% set loginActive = "border-primary text-primary" %}
+{% set registerActive = "border-transparent text-muted-foreground" %}
+{% set loginHidden = "" %}
+{% set registerHidden = "hidden" %}
+{% if default_tab == "register" %}{% set loginActive = "border-transparent text-muted-foreground" %}{% set registerActive = "border-primary text-primary" %}{% set loginHidden = "hidden" %}{% set registerHidden = "" %}{% endif %}
+{% set tabbase = "flex-1 py-2 text-center terminal-mono text-sm font-medium uppercase tracking-wider border-b-2 transition-colors" %}
+{% macro captcha() %}
+
+
+{% endmacro %}
+{% macro social() %}{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}{% endif %}{% endmacro %}
+
+
+
+ Login
+ Register
+
+
+
+
+
+{% endif %}
diff --git a/templates/overrides/terminal/auth-status.ninjatpl b/templates/overrides/terminal/auth-status.ninjatpl
new file mode 100644
index 0000000..0a302d2
--- /dev/null
+++ b/templates/overrides/terminal/auth-status.ninjatpl
@@ -0,0 +1,36 @@
+{# auth-status terminal override — reproduces the builtin's hardcoded wiring: /login + /login?tab=register when logged out; /account, /account/reviews, and hx-post /api/auth/logout dropdown when logged in; hx-post /api/auth/resend-verification banner when unverified. State from context.*. #}
+{% if not context.isPublicLoggedIn %}
+
+{% else %}
+{% if not context.publicEmailVerified %}
+ Verify your email address. Resend verification email
+
{% endif %}
+
+
+
+ {{ context.publicDisplayName|default:context.publicUsername }}
+
+
+
+
+
+{% endif %}
diff --git a/templates/overrides/terminal/author-bio-hero.ninjatpl b/templates/overrides/terminal/author-bio-hero.ninjatpl
new file mode 100644
index 0000000..ec376d8
--- /dev/null
+++ b/templates/overrides/terminal/author-bio-hero.ninjatpl
@@ -0,0 +1,22 @@
+{# author-bio-hero (terminal override) — author-page header from the `authors` provider. #}
+{% 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/terminal/author-bio.ninjatpl b/templates/overrides/terminal/author-bio.ninjatpl
new file mode 100644
index 0000000..63edf63
--- /dev/null
+++ b/templates/overrides/terminal/author-bio.ninjatpl
@@ -0,0 +1,13 @@
+{# author-bio (terminal override) — byline card sourced from the `post` provider's author_* fields. #}
+{% 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/terminal/blog-hero.ninjatpl b/templates/overrides/terminal/blog-hero.ninjatpl
new file mode 100644
index 0000000..8ba19b6
--- /dev/null
+++ b/templates/overrides/terminal/blog-hero.ninjatpl
@@ -0,0 +1,24 @@
+{# blog-hero (terminal override) — blog-index header + featured posts from the `posts` provider. #}
+
+
+
+
+{% if title %}
{{ title }} {% endif %}
+{% if subtitle %}
{{ subtitle }}
{% endif %}
+
+{% if posts %}
+
+{% endif %}
+
+
diff --git a/templates/overrides/terminal/breadcrumbs.ninjatpl b/templates/overrides/terminal/breadcrumbs.ninjatpl
new file mode 100644
index 0000000..e93509f
--- /dev/null
+++ b/templates/overrides/terminal/breadcrumbs.ninjatpl
@@ -0,0 +1,15 @@
+{# breadcrumbs (terminal override) — trail from the `breadcrumbs` provider (page ancestry). #}
+{# showHome prepends a Home link; separator is text ("/") for default or a chevron #}
+{# for the arrows template. The trailing is_current item renders as plain text. #}
+{% set arrows = template == "arrows" %}
+
+
+{% 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/terminal/button.ninjatpl b/templates/overrides/terminal/button.ninjatpl
index 115475b..d8079ec 100644
--- a/templates/overrides/terminal/button.ninjatpl
+++ b/templates/overrides/terminal/button.ninjatpl
@@ -1 +1,2 @@
-{% if text %}{% if target == "_blank" %}[ {{ text }} ] {% else %}[ {{ text }} ] {% endif %}{% endif %}
+{# terminal button — keep {% button %} tag intact; terminal chrome on hugging wrapper #}
+{% with align=align|default:"left" %}{% if newTab %}{% button style|default:"primary" text=label|default:"Learn more" href=link|default:"#" size=size|default:"md" icon=icon|default:"" icon_position=iconPosition|default:"left" target="_blank" %}{% else %}{% button style|default:"primary" text=label|default:"Learn more" href=link|default:"#" size=size|default:"md" icon=icon|default:"" icon_position=iconPosition|default:"left" %}{% endif %}
{% endwith %}
diff --git a/templates/overrides/terminal/card.ninjatpl b/templates/overrides/terminal/card.ninjatpl
new file mode 100644
index 0000000..a88420a
--- /dev/null
+++ b/templates/overrides/terminal/card.ninjatpl
@@ -0,0 +1,2 @@
+{# terminal card — dashed neon panel; fields layout/link/media/title/text/linkLabel #}
+{% 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/terminal/category-list.ninjatpl b/templates/overrides/terminal/category-list.ninjatpl
new file mode 100644
index 0000000..156271d
--- /dev/null
+++ b/templates/overrides/terminal/category-list.ninjatpl
@@ -0,0 +1,18 @@
+{# category-list (terminal override) — from the `categories` provider. list or pills. #}
+{% set pills = style == "pills" %}
+
+{% if heading %}{{ heading }} {% endif %}
+{% if pills %}
+
+{% else %}
+
+{% endif %}
+
diff --git a/templates/overrides/terminal/contact-card.ninjatpl b/templates/overrides/terminal/contact-card.ninjatpl
new file mode 100644
index 0000000..3cc7fa5
--- /dev/null
+++ b/templates/overrides/terminal/contact-card.ninjatpl
@@ -0,0 +1,18 @@
+{# contact-card terminal override: neon channel panels. All field reads + control flow preserved. #}
+
+
+ {% 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 %}
{% endif %}
+
+
+ {% endfor %}
+
+
+
diff --git a/templates/overrides/terminal/contact-form.ninjatpl b/templates/overrides/terminal/contact-form.ninjatpl
new file mode 100644
index 0000000..de44b05
--- /dev/null
+++ b/templates/overrides/terminal/contact-form.ninjatpl
@@ -0,0 +1,40 @@
+{# contact-form terminal override: terminal console chrome. The {% form %} block, honeypot, field loop, form.submitted/errors reads and _block_id/bid submit wiring are preserved byte-for-byte — only presentational classes change. #}
+{% with bid=_block_id|default:context.blockId %}
+
+
+ {% if form.submitted %}
+
+
+
{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}
+
+ {% 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:"Send message" }}
+
+ {% endform %}
+ {% endwith %}
+ {% endif %}
+
+
+{% endwith %}
diff --git a/templates/overrides/terminal/countdown.ninjatpl b/templates/overrides/terminal/countdown.ninjatpl
new file mode 100644
index 0000000..c3a888f
--- /dev/null
+++ b/templates/overrides/terminal/countdown.ninjatpl
@@ -0,0 +1,50 @@
+{# countdown terminal override: neon HUD digit boxes, heading. Timer script + data attributes preserved byte-for-byte. #}
+
+
+
+ {% 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/terminal/cta.ninjatpl b/templates/overrides/terminal/cta.ninjatpl
new file mode 100644
index 0000000..db7643b
--- /dev/null
+++ b/templates/overrides/terminal/cta.ninjatpl
@@ -0,0 +1,16 @@
+{# cta terminal override: scanline overlay, heading, CTAs. All field reads preserved. #}
+
+
+
+
+
+ {% if heading %}
{{ heading }} {% endif %}
+ {% if text %}
{{ text }}
{% endif %}
+
+
+
+
+
diff --git a/templates/overrides/terminal/divider.ninjatpl b/templates/overrides/terminal/divider.ninjatpl
new file mode 100644
index 0000000..2088b0f
--- /dev/null
+++ b/templates/overrides/terminal/divider.ninjatpl
@@ -0,0 +1,2 @@
+{# terminal divider — neon gradient rule / primary-tinted variants #}
+{% with variant=variant|default:"line" weight=weight|default:"thin" width=width|default:"full" %}{% if variant == "ornament" %}::{{ icon|default:"lucide:asterisk" }}:md text-primary::
{% elif variant == "spacer-rule" %}
{% else %} {% endif %}{% endwith %}
diff --git a/templates/overrides/terminal/event-list.ninjatpl b/templates/overrides/terminal/event-list.ninjatpl
new file mode 100644
index 0000000..2fa975e
--- /dev/null
+++ b/templates/overrides/terminal/event-list.ninjatpl
@@ -0,0 +1,41 @@
+{# event-list terminal override: neon cards, mono datetime. All field reads + control flow preserved. #}
+
+
+ {% 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/terminal/faq.ninjatpl b/templates/overrides/terminal/faq.ninjatpl
new file mode 100644
index 0000000..f3a8a9a
--- /dev/null
+++ b/templates/overrides/terminal/faq.ninjatpl
@@ -0,0 +1,18 @@
+{# faq terminal override: heading, neon-tinted dividers, display questions. Chevron svg, {% for %}, and answer prose preserved. #}
+
+
+ {% if heading %}
{{ heading }} {% endif %}
+ {% if intro %}
{{ intro }}
{% endif %}
+
+ {% for item in items %}
+
+
+ {{ item.question }}
+
+
+ {{ item.answer|safe }}
+
+ {% endfor %}
+
+
+
diff --git a/templates/overrides/terminal/feature-grid.ninjatpl b/templates/overrides/terminal/feature-grid.ninjatpl
new file mode 100644
index 0000000..638981e
--- /dev/null
+++ b/templates/overrides/terminal/feature-grid.ninjatpl
@@ -0,0 +1,19 @@
+{# feature_grid terminal override: scanline hero overlay, mono eyebrow, heading, neon HUD cards. All field reads + control flow preserved. #}
+
+
+
+ {% 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/terminal/featured-posts.ninjatpl b/templates/overrides/terminal/featured-posts.ninjatpl
new file mode 100644
index 0000000..db32552
--- /dev/null
+++ b/templates/overrides/terminal/featured-posts.ninjatpl
@@ -0,0 +1,23 @@
+{# featured-posts (terminal override) — grid of cards from the `posts` provider #}
+{# (params: limit, featured=true). Neon HUD card: image, title, excerpt, meta. #}
+
+
+{% if heading %}
{{ heading }} {% endif %}
+
+
+
diff --git a/templates/overrides/terminal/footer.ninjatpl b/templates/overrides/terminal/footer.ninjatpl
new file mode 100644
index 0000000..50ed4ff
--- /dev/null
+++ b/templates/overrides/terminal/footer.ninjatpl
@@ -0,0 +1,39 @@
+{# footer (WO-TF-006): layouts columns|minimal|centered; menu columns, legal bar, and social row from the `footer_menus` provider; newsletter section reuses the {% signup_form %} core (no duplicated form). Terminal override: mono column headers, neon top rule, display wordmark. #}
+{% set fl = layout %}
+{% if not fl %}{% if template == "centered" %}{% set fl = "centered" %}{% else %}{% set fl = "columns" %}{% endif %}{% endif %}
+{% set fbg = backgroundColor|default:"bg-muted/50" %}
+{% macro sociallinks(cls) %}{% if social_items %}{% endif %}{% endmacro %}
+{% macro newsletter() %}{% if showNewsletter %}{{ newsletterTitle|default:"Stay in the loop" }} {% 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/terminal/gallery.ninjatpl b/templates/overrides/terminal/gallery.ninjatpl
new file mode 100644
index 0000000..9ef3f15
--- /dev/null
+++ b/templates/overrides/terminal/gallery.ninjatpl
@@ -0,0 +1,52 @@
+{# gallery terminal override: bordered TTY tiles. Lightbox script + data-bn-* attributes + {% img %} preserved byte-for-byte. #}
+{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}{% if lightbox %}{% endif %}{% endwith %}
diff --git a/templates/overrides/terminal/heading.ninjatpl b/templates/overrides/terminal/heading.ninjatpl
index ce5c430..8ada4aa 100644
--- a/templates/overrides/terminal/heading.ninjatpl
+++ b/templates/overrides/terminal/heading.ninjatpl
@@ -1 +1,7 @@
-{% set lvl = level|default:2|integer %}{% if lvl < 1 or lvl > 6 %}{% set lvl = 2 %}{% endif %}{% set t = text|upper %}{% if lvl == 1 %}# {{ t }} {% elif lvl == 3 %}### {{ t }} {% elif lvl == 4 %}#### {{ t }} {% elif lvl == 5 %}##### {{ t }} {% elif lvl == 6 %}###### {{ t }} {% else %}## {{ t }} {% endif %}
+{# terminal heading — terminal-heading everywhere; on h1/h2 only #}
+{% 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/terminal/hero.ninjatpl b/templates/overrides/terminal/hero.ninjatpl
new file mode 100644
index 0000000..7135fb1
--- /dev/null
+++ b/templates/overrides/terminal/hero.ninjatpl
@@ -0,0 +1,95 @@
+{# hero terminal override (GO-BIG showpiece): canvas ASCII glyph-rain background, TTY prompt eyebrow, scanline overlay. All builtin field reads + macro + branch structure preserved from the builtin default. The rain shows when there is no backgroundImage; it lazy-inits on view, honors prefers-reduced-motion, degrades to a static phosphor gradient with JS off, and is dependency-free. #}
+{% 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" %}
+
+{% 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 %}
{% else %}{% endif %}
+
+{{ herocontent(align) }}
+
+{% endif %}
+
diff --git a/templates/overrides/terminal/hours-location.ninjatpl b/templates/overrides/terminal/hours-location.ninjatpl
new file mode 100644
index 0000000..51700b0
--- /dev/null
+++ b/templates/overrides/terminal/hours-location.ninjatpl
@@ -0,0 +1,31 @@
+{# hours-location terminal override: mono readout table, neon map panel. Map lazy-load button + inline handler preserved. #}
+
+
+ {% 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 %}
Get directions{% endif %}
+
+ {% if mapEmbedUrl %}
+
+ {{ mapButtonLabel|default:"Load map" }}
+
+ {% endif %}
+
+
+
diff --git a/templates/overrides/terminal/image.ninjatpl b/templates/overrides/terminal/image.ninjatpl
index 95d92ce..9390389 100644
--- a/templates/overrides/terminal/image.ninjatpl
+++ b/templates/overrides/terminal/image.ninjatpl
@@ -1 +1,2 @@
-{% if src %} {% else %}[ no image ]
{% endif %}[fig.{% if caption %} {{ caption }}{% endif %}]
+{# terminal image — bordered ASCII frame, mono caption; all size/fit branches preserved #}
+{% with size=size|default:"full" align=align|default:"center" aspectRatio=aspectRatio|default:"auto" objectFit=objectFit|default:"cover" rounded=rounded|default:"lg" shadow=shadow|default:"none" %}{% 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/terminal/logo-strip.ninjatpl b/templates/overrides/terminal/logo-strip.ninjatpl
new file mode 100644
index 0000000..706bc4d
--- /dev/null
+++ b/templates/overrides/terminal/logo-strip.ninjatpl
@@ -0,0 +1,16 @@
+{# logo_strip terminal override: mono kicker. Marquee classes, {% img %}, and keyframes {% endif %}
diff --git a/templates/overrides/terminal/menu-list.ninjatpl b/templates/overrides/terminal/menu-list.ninjatpl
new file mode 100644
index 0000000..4913029
--- /dev/null
+++ b/templates/overrides/terminal/menu-list.ninjatpl
@@ -0,0 +1,30 @@
+{# menu-list terminal override: mono price readouts, neon section rules. All field reads + control flow preserved. #}
+
+
+ {% if heading %}
{{ heading }} {% endif %}
+ {% if intro %}
{{ intro }}
{% endif %}
+
+ {% for section in sections %}
+
+ {% endfor %}
+
+ {% if footnote %}
{{ footnote }}
{% endif %}
+
+
diff --git a/templates/overrides/terminal/navbar.ninjatpl b/templates/overrides/terminal/navbar.ninjatpl
new file mode 100644
index 0000000..95d8c21
--- /dev/null
+++ b/templates/overrides/terminal/navbar.ninjatpl
@@ -0,0 +1,104 @@
+{# navbar (WO-TF-005): variants standard|centered-logo|split|transparent-over-hero; sticky none|sticky|shrink-on-scroll; dependency-free off-canvas drawer (pure-CSS baseline + progressive-JS focus-trap/Escape/scroll-lock); menu data from the `menus` provider. Terminal override: mono nav links, neon dropdown panels, CTA — drawer ids/style/script/data-bn-* preserved byte-for-byte. #}
+{% 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/terminal/newsletter-signup.ninjatpl b/templates/overrides/terminal/newsletter-signup.ninjatpl
new file mode 100644
index 0000000..e5863ca
--- /dev/null
+++ b/templates/overrides/terminal/newsletter-signup.ninjatpl
@@ -0,0 +1,12 @@
+{# newsletter-signup (terminal override) — the {% signup_form %} tag carries the #}
+{# untouchable subscribe wiring (/api/lists/subscribe HTMX post + honeypot); #}
+{# this template supplies only the terminal chrome around it. #}
+
+
+
+{% 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/terminal/page-suggestions.ninjatpl b/templates/overrides/terminal/page-suggestions.ninjatpl
new file mode 100644
index 0000000..7c1c9c1
--- /dev/null
+++ b/templates/overrides/terminal/page-suggestions.ninjatpl
@@ -0,0 +1,13 @@
+{# page-suggestions terminal override — themed 404. NOTE: the builtin builds its "did you mean" list from a Postgres full-text query (SearchSimilarPages), which a codeless template cannot run — so this override renders the on-theme 404 console (title / message / requested path / home CTA) without the dynamic suggestion list. Recorded as a known deviation in WO-TF-015. Content fields honored: title, emptyMessage. #}
+
+
+
+
Error 404 _
+
{{ title|default:"Signal lost" }}
+
{{ emptyMessage|default:"That route returned nothing. The page moved, shipped, or never existed." }}
+ {% if context.path %}
requested: {{ context.path }}
{% endif %}
+
+
+
diff --git a/templates/overrides/terminal/password-reset-form.ninjatpl b/templates/overrides/terminal/password-reset-form.ninjatpl
new file mode 100644
index 0000000..26b7d8c
--- /dev/null
+++ b/templates/overrides/terminal/password-reset-form.ninjatpl
@@ -0,0 +1,13 @@
+{# password-reset-form terminal override — reproduces the builtin's hardcoded wiring: hx-post /api/auth/request-password-reset (target #reset-message), back-to-login link /login. Only the chrome is themed. #}
+
+
+
+
Reset Password
+
Enter your email address and we will send you a link to reset your password.
+
+
Back to login
+
+
diff --git a/templates/overrides/terminal/popular-posts.ninjatpl b/templates/overrides/terminal/popular-posts.ninjatpl
new file mode 100644
index 0000000..458bb24
--- /dev/null
+++ b/templates/overrides/terminal/popular-posts.ninjatpl
@@ -0,0 +1,16 @@
+{# popular-posts (terminal override) — compact ranked list from the `posts` provider. #}
+
+{% 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/terminal/post-hero.ninjatpl b/templates/overrides/terminal/post-hero.ninjatpl
new file mode 100644
index 0000000..ce97464
--- /dev/null
+++ b/templates/overrides/terminal/post-hero.ninjatpl
@@ -0,0 +1,23 @@
+{# post-hero (terminal override) — 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" %}
+
+
+
{{ 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 %}
+
+
+
+
{{ 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/terminal/post-metadata.ninjatpl b/templates/overrides/terminal/post-metadata.ninjatpl
new file mode 100644
index 0000000..0f7029e
--- /dev/null
+++ b/templates/overrides/terminal/post-metadata.ninjatpl
@@ -0,0 +1,9 @@
+{# post-metadata (terminal override) — author/date/reading-time row from the `post` provider. #}
+{% 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 border border-primary/40 object-cover" %}{% 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/terminal/pricing.ninjatpl b/templates/overrides/terminal/pricing.ninjatpl
new file mode 100644
index 0000000..108923b
--- /dev/null
+++ b/templates/overrides/terminal/pricing.ninjatpl
@@ -0,0 +1,26 @@
+{# pricing terminal override: scanline overlay, heading, HUD neon tier panels (magenta highlight / cyan standard), CTAs. Field reads + control flow preserved. #}
+
+
+
+ {% 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 %}
{{ tier.ctaLabel }} {% endif %}
+
+ {% endfor %}
+
+ {% if billingNote %}
{{ billingNote }}
{% endif %}
+
+
diff --git a/templates/overrides/terminal/quote.ninjatpl b/templates/overrides/terminal/quote.ninjatpl
new file mode 100644
index 0000000..1e803a4
--- /dev/null
+++ b/templates/overrides/terminal/quote.ninjatpl
@@ -0,0 +1,2 @@
+{# terminal quote — dashed neon left border; terminal-heading for pull; mono figcaption #}
+{% with style=style|default:"block" align=align|default:"left" %}{{ 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/terminal/related-posts.ninjatpl b/templates/overrides/terminal/related-posts.ninjatpl
new file mode 100644
index 0000000..5c962af
--- /dev/null
+++ b/templates/overrides/terminal/related-posts.ninjatpl
@@ -0,0 +1,17 @@
+{# related-posts (terminal override) — grid from the `posts` provider. #}
+{% if posts %}
+
+
+
{{ heading|default:"Related posts" }}
+
+
+
+{% endif %}
diff --git a/templates/overrides/terminal/rich-section.ninjatpl b/templates/overrides/terminal/rich-section.ninjatpl
new file mode 100644
index 0000000..a696cc4
--- /dev/null
+++ b/templates/overrides/terminal/rich-section.ninjatpl
@@ -0,0 +1,14 @@
+{# rich_section terminal override: mono eyebrow, heading, HUD-framed media, CTA. Field reads + {% img %} preserved. #}
+
diff --git a/templates/overrides/terminal/rich-text.ninjatpl b/templates/overrides/terminal/rich-text.ninjatpl
new file mode 100644
index 0000000..f8aae0c
--- /dev/null
+++ b/templates/overrides/terminal/rich-text.ninjatpl
@@ -0,0 +1,2 @@
+{# terminal rich-text — body font via terminal-prose #}
+{% if html %}{{ html|safe }} {% endif %}
diff --git a/templates/overrides/terminal/sidebar-nav.ninjatpl b/templates/overrides/terminal/sidebar-nav.ninjatpl
new file mode 100644
index 0000000..3168491
--- /dev/null
+++ b/templates/overrides/terminal/sidebar-nav.ninjatpl
@@ -0,0 +1,15 @@
+{# sidebar-nav (terminal override) — vertical menu from the `menus` provider. Variants #}
+{# default | compact. highlight-current carried as a progressive-enhancement #}
+{# script keyed on data-bn-sidebar-link (marks the anchor matching location.pathname). #}
+{% set compact = template == "compact" %}
+
+{% if title %}{{ title }} {% endif %}
+
+
+{% if highlightCurrent %}{% endif %}
diff --git a/templates/overrides/terminal/social-links.ninjatpl b/templates/overrides/terminal/social-links.ninjatpl
new file mode 100644
index 0000000..89599e1
--- /dev/null
+++ b/templates/overrides/terminal/social-links.ninjatpl
@@ -0,0 +1,14 @@
+{# terminal social-links — mono label, sharp neon-bordered chips; icon sprites preserved #}
+
+ {% if heading %}
{{ heading }}
{% endif %}
+
+
diff --git a/templates/overrides/terminal/stats.ninjatpl b/templates/overrides/terminal/stats.ninjatpl
new file mode 100644
index 0000000..a7050e1
--- /dev/null
+++ b/templates/overrides/terminal/stats.ninjatpl
@@ -0,0 +1,50 @@
+{# stats terminal override: scanline overlay, heading, HUD stat tiles, mono labels. data-stats + countUp script/attrs preserved byte-for-byte. #}
+
+
+
+ {% 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/terminal/team.ninjatpl b/templates/overrides/terminal/team.ninjatpl
new file mode 100644
index 0000000..2ccc8cb
--- /dev/null
+++ b/templates/overrides/terminal/team.ninjatpl
@@ -0,0 +1,22 @@
+{# team terminal override: heading, dashed-neon HUD member panels (lime), mono roles. {% img %} + social icon svgs + control flow preserved. #}
+
+
+ {% 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/terminal/testimonial.ninjatpl b/templates/overrides/terminal/testimonial.ninjatpl
new file mode 100644
index 0000000..8e4ca41
--- /dev/null
+++ b/templates/overrides/terminal/testimonial.ninjatpl
@@ -0,0 +1,27 @@
+{# testimonial terminal override: heading, dashed-neon HUD quote cards. Rating svgs, {% img %}, and control flow preserved. #}
+
+
+ {% 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/terminal/text.ninjatpl b/templates/overrides/terminal/text.ninjatpl
index 7d0a8fb..410277d 100644
--- a/templates/overrides/terminal/text.ninjatpl
+++ b/templates/overrides/terminal/text.ninjatpl
@@ -1 +1,2 @@
-{{ text|safe }}
+{# terminal text — body font via terminal-prose #}
+{{ text|safe }}
diff --git a/templates/overrides/terminal/timeline.ninjatpl b/templates/overrides/terminal/timeline.ninjatpl
new file mode 100644
index 0000000..6660e13
--- /dev/null
+++ b/templates/overrides/terminal/timeline.ninjatpl
@@ -0,0 +1,22 @@
+{# timeline terminal override: heading, mono step labels, neon markers. All field reads + control flow preserved. #}
+
+
+ {% 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/terminal/utility-bar.ninjatpl b/templates/overrides/terminal/utility-bar.ninjatpl
new file mode 100644
index 0000000..4e6cc80
--- /dev/null
+++ b/templates/overrides/terminal/utility-bar.ninjatpl
@@ -0,0 +1,10 @@
+{# utility_bar (WO-TF-006): thin top contact strip; left/right item lists {icon,label,link}; icons render through the sprite pipeline via the ::pack:name:: shorthand. Terminal override: mono terminal readout, neon underline. #}
+{% set ubcls = "bg-muted text-muted-foreground border-b border-primary/30" %}
+{% if variant == "primary" %}{% set ubcls = "bg-primary text-primary-foreground" %}{% elif variant == "dark" %}{% set ubcls = "bg-foreground text-background" %}{% endif %}
+{% macro ubitem(it) %}{% if it.link %}{% 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/terminal/video-embed.ninjatpl b/templates/overrides/terminal/video-embed.ninjatpl
new file mode 100644
index 0000000..0111384
--- /dev/null
+++ b/templates/overrides/terminal/video-embed.ninjatpl
@@ -0,0 +1,23 @@
+{# video-embed terminal override: neon HUD frame, play button. Facade script + data-bn-* attributes + {% img %} preserved byte-for-byte. #}
+{% 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 %}
diff --git a/templates/terminal/article.ninjatpl b/templates/terminal/article.ninjatpl
index 747e2f3..7032ba4 100644
--- a/templates/terminal/article.ninjatpl
+++ b/templates/terminal/article.ninjatpl
@@ -1,22 +1,23 @@
-
-
+
+
{{ 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 %}
-
+
+
+
+
+ {% if slots.aside %}
+
+
+
{% if slots.main %}{{ slots.main|safe }}{% else %}// article body
{% endif %}
+
+ {% else %}
+
{% if slots.main %}{{ slots.main|safe }}{% else %}// article body
{% endif %}
+ {% endif %}
-
-
- {{ slots.footer|safe }}
-
+
+
{{ body_end_html|safe }}
diff --git a/templates/terminal/auth.ninjatpl b/templates/terminal/auth.ninjatpl
new file mode 100644
index 0000000..c43ca0f
--- /dev/null
+++ b/templates/terminal/auth.ninjatpl
@@ -0,0 +1,21 @@
+
+
+{{ head_html|safe }}
+
+ {{ admin_banner_html|safe }}
+
+
+
+
+
+
login
+
+ {% if slots.main %}{{ slots.main|safe }}{% else %}
// auth console
{% endif %}
+
+
+
+
+
+ {{ body_end_html|safe }}
+
+
diff --git a/templates/terminal/blog-index.ninjatpl b/templates/terminal/blog-index.ninjatpl
new file mode 100644
index 0000000..4959993
--- /dev/null
+++ b/templates/terminal/blog-index.ninjatpl
@@ -0,0 +1,16 @@
+
+
+{{ head_html|safe }}
+
+ {{ admin_banner_html|safe }}
+
+
+
+
+ {% if slots.main %}{{ slots.main|safe }}{% else %}
{% endif %}
+
+
+
+ {{ body_end_html|safe }}
+
+
diff --git a/templates/terminal/contact.ninjatpl b/templates/terminal/contact.ninjatpl
new file mode 100644
index 0000000..6a7c145
--- /dev/null
+++ b/templates/terminal/contact.ninjatpl
@@ -0,0 +1,21 @@
+
+
+{{ head_html|safe }}
+
+ {{ admin_banner_html|safe }}
+
+
+
+
+
+
contact --send
+
+ {% if slots.main %}{{ slots.main|safe }}{% else %}
// add a contact-form block
{% endif %}
+
+
+
+
+
+ {{ body_end_html|safe }}
+
+
diff --git a/templates/terminal/default.ninjatpl b/templates/terminal/default.ninjatpl
index 4cd03ce..79c4682 100644
--- a/templates/terminal/default.ninjatpl
+++ b/templates/terminal/default.ninjatpl
@@ -1,17 +1,21 @@
-
-
+
+
{{ 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 %}
+
+
+
+
+
+
man page
+
+ {% if slots.main %}{{ slots.main|safe }}{% else %}
// no content blocks assigned to this page
{% endif %}
+
+
+
-
- {{ slots.footer|safe }}
-
+
{{ body_end_html|safe }}
diff --git a/templates/terminal/full-width.ninjatpl b/templates/terminal/full-width.ninjatpl
index 5c400b9..165acbf 100644
--- a/templates/terminal/full-width.ninjatpl
+++ b/templates/terminal/full-width.ninjatpl
@@ -1,13 +1,14 @@
-
-
+
+
{{ head_html|safe }}
{{ admin_banner_html|safe }}
-
-
- {% if slots.main %}{{ slots.main|safe }}{% else %}// no content blocks assigned to this page
{% endif %}
+
+
+
+ {% if slots.main %}{{ slots.main|safe }}{% else %}// no content blocks assigned to this page
{% endif %}
-
+
{{ body_end_html|safe }}
diff --git a/templates/terminal/landing.ninjatpl b/templates/terminal/landing.ninjatpl
index 97bf031..dfe3eba 100644
--- a/templates/terminal/landing.ninjatpl
+++ b/templates/terminal/landing.ninjatpl
@@ -1,20 +1,14 @@
-
-
+
+
{{ head_html|safe }}
{{ admin_banner_html|safe }}
-
- {{ slots.hero|safe }}
-
+
+
- {% if slots.main %}{{ slots.main|safe }}
{% endif %}
+ {% if slots.main %}{{ slots.main|safe }}{% else %}// add a hero block to start the boot sequence
{% endif %}
-
-
- {{ slots.footer|safe }}
-
+
{{ body_end_html|safe }}