feat(terminal): Wave A full theme build (WO-TF-025)

Bring the terminal dev-tool/OSS-docs theme to full Wave A OVERRIDE-CONTRACT
compliance.

- 49 mandatory builtin overrides in templates/overrides/terminal/ (TTY chrome,
  re-skinned from the frozen builtin defaults; every content field, provider var,
  custom tag, data-bn-* attr and inline script preserved verbatim).
- 7 page templates (default, full-width, landing, article, blog-index, contact,
  auth) with TTY-window chrome, man-page article layout, and theme_mode-driven
  light/dark switching.
- GO BIG motion: dependency-free ASCII glyph-rain canvas on the hero override
  only (lazy-init, prefers-reduced-motion honored, static phosphor fallback).
- 4 persona blocks (manpage_header, code_console, keybind_table, boot_log);
  ascii_header/toc/footer blocks retired in favour of overrides + template
  furniture.
- 5 dual-mode presets (mode "both", all 19 tokens) with intentional paper-teletype
  light and phosphor dark sides.
- Bundled OFL mono fonts (JetBrains Mono, IBM Plex Mono) with LICENSES record;
  all font-family via var(--font-heading|body|mono, fallback).
- Themed email-safe wrapper (retained), master pages, and a seeded "grid" OSS
  dev-tool demo site (home, about, changelog + 3 posts, contact with
  row_inserted -> email-admins workflow, login).
- Declared required_icon_packs = [lucide, simple-icons].

Verified: make build exit 0, make verify exit 0, check-safety exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-05 12:41:04 +08:00
parent f6e2c1dccc
commit daab6750fe
76 changed files with 2090 additions and 487 deletions

View File

@ -1,31 +1,43 @@
# Recommended fonts — Terminal theme
# Recommended fonts — Terminal
This theme ships `fonts.json = []` per the wave-1 implementation policy in `docs/FONTS.md`. The intended typography is **all monospace, no exceptions**. The CSS fallback chain ends in `monospace` so the theme remains legible before an admin picks fonts; the recommendations below match the spec's intended look.
Every family flows through the admin typography picker via CSS variables. Templates never
hardcode a `font-family`; they read `var(--font-heading, ...)`, `var(--font-body, ...)`,
and `var(--font-mono, ...)`, each with a bundled fallback so the intended look survives
before the admin assigns a slot.
Fonts are admin-configured through the typography panel. Open the Google Fonts tab of the picker, add each family, then assign it to the appropriate slot.
## Bundled (ship as-is)
## Heading & mono — JetBrains Mono
The theme bundles two OFL monospace families (latin subset, ~69 KB total) as
`source=template` rows, already selectable in the picker. They back the fallbacks:
- **Source recommendation:** `google:JetBrains Mono`
- **Slots:** Heading, Mono
- **How:** Open the typography panel → Google Fonts tab → search "JetBrains Mono" → click Add. Assign it to the **Heading** slot and the **Mono** slot.
| Slot | Fallback family | File |
|---|---|---|
| Heading | JetBrains Mono | `assets/fonts/jetbrains-mono-latin.woff2` |
| Mono | JetBrains Mono | `assets/fonts/jetbrains-mono-latin.woff2` |
| Body | IBM Plex Mono | `assets/fonts/ibm-plex-mono-400.woff2`, `ibm-plex-mono-500.woff2` |
## Body — IBM Plex Mono
A terminal theme is monospace top to bottom, so all three slots default to a mono family.
License record: `assets/fonts/LICENSES.md`.
- **Source recommendation:** `google:IBM Plex Mono`
- **Slot:** Body
- **How:** Open the typography panel → Google Fonts tab → search "IBM Plex Mono" → click Add. Assign it to the **Body** slot.
## Recommended admin assignments (Google Fonts tab)
Assign these from Admin -> Appearance -> Typography if you want the full family (extended
glyph coverage) or the display accent the bundled subset omits:
| Slot | Assign | Why |
|---|---|---|
| Heading | JetBrains Mono or IBM Plex Mono | Sharp, even monospace headings that read as terminal chrome. |
| Body | IBM Plex Mono | Slightly wider, highly readable for long docs and man pages. |
| Mono | JetBrains Mono | Code blocks, prompts, keybind tables. |
| Display accent (optional) | VT323 | Chunky bitmap-CRT face for big phosphor hero display type. Not bundled; assign from the Google Fonts tab if you want the retro-CRT accent. |
All four are SIL Open Font License 1.1. Nothing here hardcodes a family, so any admin
assignment overrides the bundled fallback cleanly.
## Fallback chain (already in theme CSS)
If the admin has not yet picked fonts, the theme degrades through:
Before any admin selection the theme degrades through:
```
"JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
```
This keeps the all-monospace aesthetic intact even without any admin font selection.
## Wave-2 follow-up
Bundling JetBrains Mono and IBM Plex Mono `.woff2` files (both OFL-licensed) is deferred to wave-2 along with the `LICENSES.md` entry. In this pass, fonts are admin-side via the Google Fonts picker.

24
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,24 @@
# Recommended icons — Terminal
Icons render through the CMS sprite pipeline: `<svg><use href="/icons/<pack>.svg#<name>"/></svg>`
(or the `::pack:name:size::` shorthand). The theme's overrides reference two bundled packs.
## Required packs
Declared in `plugin.mod` as `required_icon_packs = ["lucide", "simple-icons"]`
(forward-declared — the loader does not yet auto-install; install manually until it does).
| Pack | Used for | Install |
|---|---|---|
| **lucide** | UI glyphs the builtin/override templates use: `arrow-right`, `check`, `chevron-down`, `chevron-left`, `chevron-right`, `x`, `terminal`, `cpu`, `mail`, `map-pin`, `download`, `play`, `radio`, `message-square` | Ships bundled with the CMS — no action needed. |
| **simple-icons** | Author/social brand glyphs: `github`, `x`, `linkedin`, `discord` | Ships bundled with the CMS — no action needed. |
Both packs ship bundled with the CMS, so a fresh instance renders every override icon with
no manual step. If you author blocks that reference other packs (e.g. `tabler`, `phosphor`),
install them from `/admin/icons` (Browse -> install -> enable) before activation.
## Note
Icon values in block content are stored as `"pack:name"` strings and split with
`|split:":"|first` / `|last`. Never inline raw SVG path data for content icons — that
bypasses the sprite system and the admin pack manager.

21
assets/fonts/LICENSES.md Normal file
View File

@ -0,0 +1,21 @@
# Bundled font licenses — Terminal theme
Both families are bundled as latin-subset `.woff2` and are licensed under the SIL Open
Font License 1.1 (OFL). Redistribution inside this theme artifact is permitted.
| File | Family | Weights | License | Source |
|---|---|---|---|---|
| `jetbrains-mono-latin.woff2` | JetBrains Mono (variable) | 400 / 500 / 700 | OFL 1.1 | Google Fonts (fonts.gstatic.com), latin subset |
| `ibm-plex-mono-400.woff2` | IBM Plex Mono | 400 | OFL 1.1 | Google Fonts (fonts.gstatic.com), latin subset |
| `ibm-plex-mono-500.woff2` | IBM Plex Mono | 500 | OFL 1.1 | Google Fonts (fonts.gstatic.com), latin subset |
OFL 1.1 full text: https://openfontlicense.org
- JetBrains Mono (c) 2020 The JetBrains Mono Project Authors.
- IBM Plex Mono (c) 2017 IBM Corp., the IBM Plex Project Authors.
These are latin subsets (Latin + Latin-1 punctuation), chosen to keep the bundle small
(~69 KB total). They cover English content. Sites needing extended Latin, Cyrillic, Greek,
or Vietnamese glyphs should assign the full family from the admin Google Fonts picker.
The bundled families back the theme's `--font-heading`, `--font-body`, and `--font-mono`
fallbacks; the admin typography picker overrides them per slot.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
{% set eff_title = title %}{% if not title and not asciiArt %}{% set eff_title = "~" %}{% endif %}<div class="ascii-header" data-block="terminal:ascii_header">{% if asciiArt %}<pre>{{ asciiArt }}</pre>{% else %}<pre>/*===========================================*/
/* {{ eff_title|slice:":41"|ljust:41 }} */
/*===========================================*/</pre>{% endif %}<div class="prompt-line"><span class="terminal-mono">{{ prompt|default:"$ " }}</span>{% if eff_title %}<span class="terminal-mono">{{ eff_title }}</span>{% endif %}</div></div>

View File

@ -1,29 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ASCII Header",
"description": "TTY-style figlet banner with a prompt line",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"description": "Title shown in the banner (e.g. ~/projects)",
"default": "~/projects",
"x-editor": "text"
},
"prompt": {
"type": "string",
"title": "Prompt",
"description": "Shell prompt prefix (e.g. $ )",
"default": "$ ",
"x-editor": "text"
},
"asciiArt": {
"type": "string",
"title": "ASCII Art",
"description": "Optional pre-baked figlet block. Leave blank to use the auto-generated banner.",
"default": "",
"x-editor": "textarea"
}
}
}

View File

@ -1,27 +1,15 @@
# Terminal theme block definitions (codeless .bnp, WO-WZ-021).
# Keys are fully qualified (terminal:<key>) because the codeless loader
# registers definitions verbatim — there is no PluginBlockRegistry prefixing
# in the codeless path (unlike the wasm/templ path). The qualified keys match
# the hardcoded data-block attributes and the master_pages.json references.
# Terminal theme block definitions (codeless .bnp, WO-TF-025).
# Only true persona furniture stays a theme block; blocks that duplicated a
# builtin (ascii masthead, footer) became template overrides, and the section
# rail (toc) became article page-template furniture. Keys are fully qualified
# (terminal:<key>) 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

View File

@ -1 +0,0 @@
{% set m = motd|default:"connection closed." %}<div class="terminal-footer" data-block="terminal:footer"><div class="terminal-footer-rule">===========================================</div>{% if m %}<div class="terminal-footer-motd">-- {{ m }}</div>{% endif %}{% if links %}<div class="terminal-footer-links">{% for l in links %}<a href="{% if l.url %}{{ l.url }}{% else %}#{% endif %}">[ {{ l.label }} ]</a>{% endfor %}</div>{% endif %}{% if showSignup == true or showSignup == "true" or showSignup == "yes" or showSignup == "1" %}<form class="terminal-footer-signup" method="post" action="#"><label class="terminal-mono">$ subscribe <input type="email" name="email" placeholder="you@example.com" required></label></form>{% endif %}<div class="terminal-footer-rule">===========================================</div><div class="terminal-mono">EOF</div></div>

View File

@ -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"]
}
}
}

View File

@ -1 +0,0 @@
<nav class="terminal-toc" data-block="terminal:toc"><h4>{{ heading|default:"Sections" }}</h4>{% if items %}<ul>{% for item in items %}<li><a href="{% if item.anchor %}#{{ item.anchor }}{% else %}#{% endif %}">§ {{ item.label }}</a></li>{% endfor %}</ul>{% else %}<p class="terminal-mono">// no sections yet</p>{% endif %}</nav>

View File

@ -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"]
}
}
}
}

View File

@ -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" }
]
}
]

View File

@ -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 <span class="caret-blink">_</span>) --- */
@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; }
}

View File

@ -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 }
]
}
]

View File

@ -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"]

View File

@ -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%"
}
}
}
]

240
seed/seed.json Normal file
View File

@ -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": "<p>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.</p><p>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.</p>" } },
{
"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": "<p>Every task output is keyed by its inputs. Here is how the hash is built and why it is stable across machines.</p>", "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": "<p>grid watch reruns affected tasks on save with no background process. This is how the file watcher stays cheap.</p>", "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": "<p>Independent tasks run together, up to your core count. The scheduler is a topological sort with a worker pool.</p>", "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": "<p>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.</p><p>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.</p>" } },
{ "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": "<p>Most watchers run a background process that never exits. grid does not. grid watch runs in the foreground and exits when you press q.</p><p>On each save it maps the changed file back to the tasks that read it, then reruns only those. The rest stay cached.</p>" } },
{ "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": "<p>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.</p><p>The worker pool is sized to your core count by default. You can cap it with a flag when a task is memory heavy.</p>" } },
{ "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 }
]
}

19
seed/workflows.json Normal file
View File

@ -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" }
}
]
}
]
}

View File

@ -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 %}
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full {{ anncls }} {{ class }}">
<div class="max-w-7xl mx-auto px-10 py-2 relative flex items-center justify-center gap-2 text-sm text-center">
<p class="terminal-mono uppercase tracking-wide font-medium">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 rounded hover:bg-black/10 transition-colors"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
</div>
</div>
{% if dismissable %}<script>
(function(){
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
bars.forEach(function(bar){
bar.setAttribute('data-bn-ann-wired','1');
var key='bnann:'+bar.getAttribute('data-bn-annkey');
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
var btn=bar.querySelector('[data-bn-ann-dismiss]');
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
});
})();
</script>{% endif %}

View File

@ -0,0 +1,16 @@
{# audio-embed terminal override: neon panel, mono meta. Facade script + data-bn-* attributes + {% img %} preserved byte-for-byte. #}
<figure class="bn-audio my-6 overflow-hidden rounded-md border border-primary/40 bg-card text-card-foreground tty-frame{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden rounded-md border border-primary/30 bg-muted">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="terminal-heading truncate font-medium text-foreground">{{ title }}</p>{% endif %}{% if artist %}<p class="terminal-mono truncate text-sm text-muted-foreground">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="terminal-mono inline-flex items-center gap-2 rounded-md bg-primary px-4 py-1.5 text-sm font-medium uppercase tracking-wide text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label="Play{% if title %}: {{ title }}{% endif %}">::lucide:play:sm:: Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="terminal-mono inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">::lucide:download:sm:: Download</a>{% endif %}</div></div></div></figure><script>
(function(){
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
e.preventDefault();
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
if(!src)return;
var slot=btn.closest('[data-bn-audio-slot]');
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
btn.remove();slot.insertBefore(a,slot.firstChild);
});
})();
</script>

View File

@ -0,0 +1,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 %}
<div class="auth-form-logged-in mx-auto max-w-md text-center py-6">
<p class="terminal-mono text-sm uppercase tracking-wide text-muted-foreground">Session active as <strong class="text-primary">{{ context.publicUsername }}</strong></p>
<a href="/account" class="terminal-mono mt-3 inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium uppercase tracking-wide text-primary-foreground hover:bg-primary/90">My Account</a>
</div>
{% 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() %}<script>
window.CAP_CUSTOM_WASM_URL = "/assets/captcha/cap_wasm_bg.wasm";
window.CAP_PAKO_URL = "/assets/captcha/pako_inflate.min.js";
</script>
<cap-widget data-cap-api-endpoint="/api/captcha/" data-cap-worker-count="2"></cap-widget>
<script src="/assets/captcha/cap.min.js" defer></script>
<script>
(function(){
var s = document.currentScript;
var form = s && s.closest ? s.closest('form') : null;
if (!form) return;
form.addEventListener('htmx:afterRequest', function(){
var w = form.querySelector('cap-widget');
if (w && typeof w.reset === 'function') w.reset();
});
})();
</script>{% endmacro %}
{% macro social() %}{% if show_social != false and (context.site.google_oauth_client_id or context.site.facebook_app_id) %}<div class="auth-social my-4">
<div class="relative my-4"><hr class="border-border"/><span class="terminal-mono absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-3 text-xs uppercase tracking-widest text-muted-foreground">or</span></div>
{% if context.site.google_oauth_client_id %}<a href="/api/auth/google/start" class="terminal-mono mb-2 flex w-full items-center justify-center gap-3 rounded-md border border-border px-4 py-2.5 uppercase tracking-wide transition-colors hover:bg-accent/10"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#google"></use></svg><span class="text-sm font-medium text-foreground/80">Continue with Google</span></a>{% endif %}
{% if context.site.facebook_app_id %}<a href="/api/auth/facebook/start" class="terminal-mono flex w-full items-center justify-center gap-3 rounded-md border border-border px-4 py-2.5 uppercase tracking-wide transition-colors hover:bg-accent/10"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#facebook"></use></svg><span class="text-sm font-medium text-foreground/80">Continue with Facebook</span></a>{% endif %}
</div>{% endif %}{% endmacro %}
<div class="auth-form tty-corners mx-auto max-w-md rounded-md border border-primary/40 bg-card p-6 text-card-foreground tty-frame">
<div id="auth-message"></div>
<div class="mb-5 flex border-b border-border">
<button type="button" data-auth-tab="login" data-tabbase="{{ tabbase }}" class="{{ tabbase }} {{ loginActive }}">Login</button>
<button type="button" data-auth-tab="register" data-tabbase="{{ tabbase }}" class="{{ tabbase }} {{ registerActive }}">Register</button>
</div>
<div id="login-panel" class="{{ loginHidden }}">
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Email</label><input type="email" name="email" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<div class="mb-4"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Password</label><input type="password" name="password" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
{% if captchaEnabled %}{{ captcha() }}{% endif %}
<button type="submit" class="terminal-mono w-full rounded-md bg-primary px-4 py-2.5 font-semibold uppercase tracking-wider text-primary-foreground hover:bg-primary/90" hx-disabled-elt="this">Login</button>
<p class="mt-3 text-center text-sm"><a href="/forgot-password" class="terminal-mono uppercase tracking-wide text-primary hover:underline">Forgot password?</a></p>
</form>
{{ social() }}
</div>
<div id="register-panel" class="{{ registerHidden }}">
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<div class="mb-3"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Email</label><input type="email" name="email" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<div class="mb-4"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Password</label><input type="password" name="password" required minlength="8" class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
{% if captchaEnabled %}{{ captcha() }}{% endif %}
<button type="submit" class="terminal-mono w-full rounded-md bg-primary px-4 py-2.5 font-semibold uppercase tracking-wider text-primary-foreground hover:bg-primary/90" hx-disabled-elt="this">Create Account</button>
</form>
{{ social() }}
</div>
</div>
<script>
(function(){
var f=document.currentScript.closest('.auth-form')||document.querySelector('.auth-form');
if(!f)return;
var tabs=f.querySelectorAll('[data-auth-tab]');
tabs.forEach(function(btn){btn.addEventListener('click',function(){
var t=btn.getAttribute('data-auth-tab');
var lp=f.querySelector('#login-panel'),rp=f.querySelector('#register-panel');
if(lp)lp.classList.toggle('hidden',t!=='login');
if(rp)rp.classList.toggle('hidden',t!=='register');
tabs.forEach(function(b){var on=b===btn;b.className=b.getAttribute('data-tabbase')+(on?' border-primary text-primary':' border-transparent text-muted-foreground');});
});});
})();
</script>
{% endif %}

View File

@ -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 %}
<div class="auth-status flex items-center gap-2">
<a href="/login" class="terminal-mono rounded-md px-3 py-1.5 text-sm font-medium uppercase tracking-wide opacity-80 transition-colors hover:bg-primary/10 hover:opacity-100">Login</a>
<a href="/login?tab=register" class="terminal-mono rounded-md bg-accent px-3 py-1.5 text-sm font-medium uppercase tracking-wide text-accent-foreground hover:bg-accent/90">Sign Up</a>
</div>
{% else %}
{% if not context.publicEmailVerified %}<div class="fixed top-16 inset-x-0 z-40 border-y border-destructive/50 bg-destructive/15 px-4 py-2 text-center text-sm text-destructive">
<span class="terminal-mono uppercase tracking-wide">Verify your email address.</span> <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="underline font-medium">Resend verification email</button>
</div>{% endif %}
<div class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium opacity-90 transition-colors hover:bg-primary/10 hover:opacity-100">
<span class="flex h-7 w-7 items-center justify-center rounded-md bg-accent text-accent-foreground"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#user"></use></svg></span>
<span class="terminal-mono uppercase tracking-wide">{{ context.publicDisplayName|default:context.publicUsername }}</span>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div id="auth-dropdown-menu" class="tty-corners hidden absolute right-0 top-full mt-1 w-48 rounded-md border border-primary/40 bg-card py-1 text-card-foreground tty-frame z-50">
<a href="/account" class="terminal-mono block px-4 py-2 text-sm uppercase tracking-wide text-card-foreground hover:bg-primary/10">My Profile</a>
<a href="/account/reviews" class="terminal-mono block px-4 py-2 text-sm uppercase tracking-wide text-card-foreground hover:bg-primary/10">My Reviews</a>
<hr class="my-1 border-border"/>
<button hx-post="/api/auth/logout" hx-swap="none" class="terminal-mono block w-full px-4 py-2 text-left text-sm uppercase tracking-wide text-destructive hover:bg-primary/10">Logout</button>
</div>
<script>
(function(){
document.addEventListener('click', function(e) {
var menu = document.getElementById('auth-dropdown-menu');
var btn = document.querySelector('.auth-status > button');
if (!menu || !btn) return;
if (!btn.contains(e.target) && !menu.contains(e.target)) {
menu.classList.add('hidden');
}
});
})();
</script>
</div>
{% endif %}

View File

@ -0,0 +1,22 @@
{# author-bio-hero (terminal override) — author-page header from the `authors` provider. #}
{% if author %}
<section class="relative overflow-hidden border-b border-primary/30 bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="tty-scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden rounded-full bg-muted ring-2 ring-primary tty-frame">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<h1 class="terminal-heading text-3xl font-bold tracking-tight md:text-4xl" data-text="{{ author.display_name }}">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="terminal-mono mt-1 text-sm uppercase tracking-wide text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
{% if author.bio %}<p class="terminal-page mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
<div class="mt-5 flex flex-wrap items-center justify-center gap-4 terminal-mono text-sm uppercase tracking-wide text-accent">
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
</div>
{% if showSocial %}<div class="mt-5 flex items-center justify-center gap-3">
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Website"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="Twitter"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="LinkedIn"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-primary" title="GitHub"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,13 @@
{# author-bio (terminal override) — byline card sourced from the `post` provider's author_* fields. #}
{% if post.author_name %}
<aside class="tty-corners rounded-md border border-dashed border-primary/40 bg-card p-6 text-card-foreground tty-frame{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="terminal-eyebrow terminal-mono mb-4 text-xs font-semibold uppercase tracking-widest text-accent">{{ heading }}</p>{% endif %}
<div class="flex items-start gap-4">
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full border border-primary/40 bg-muted">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="terminal-heading text-base font-semibold">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-primary">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
{% if post.author_bio %}<p class="terminal-page mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -0,0 +1,24 @@
{# blog-hero (terminal override) — blog-index header + featured posts from the `posts` provider. #}
<section class="relative overflow-hidden bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="tty-scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto max-w-6xl px-4">
<div class="max-w-2xl">
{% if title %}<h1 class="terminal-heading text-4xl font-bold tracking-tight md:text-5xl" data-text="{{ title }}">{{ title }}</h1>{% endif %}
{% if subtitle %}<p class="terminal-page mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
</div>
{% if posts %}
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
{% for post in posts %}
<article class="tty-corners group flex flex-col rounded-md border border-dashed border-primary/40 bg-card p-4 text-card-foreground tty-frame{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-6{% endif %}">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="mb-3 block overflow-hidden rounded-md bg-muted{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
<h2 class="terminal-heading {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} font-semibold leading-snug"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h2>
{% if showExcerpt and post.excerpt %}<p class="terminal-page mt-2 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
<div class="mt-3 flex flex-wrap items-center gap-x-3 terminal-mono text-xs uppercase tracking-wide text-accent">{% if post.author_name %}<span class="font-medium text-foreground">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
</section>

View File

@ -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" %}
<nav aria-label="Breadcrumb" class="py-3{% if class %} {{ class }}{% endif %}">
<ol class="flex flex-wrap items-center gap-1 terminal-mono text-sm uppercase tracking-wide">
{% if showHome %}<li class="flex items-center"><a href="/" class="text-accent transition-colors hover:text-primary">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
{% for item in breadcrumbs %}
<li class="flex items-center">
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>{% else %}<span class="mx-2 text-primary">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
{% if item.is_current %}{% if showCurrent %}<span class="font-medium text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-accent transition-colors hover:text-primary">{{ item.label }}</a>{% endif %}
</li>
{% endfor %}
</ol>
</nav>

View File

@ -1 +1,2 @@
{% if text %}{% if target == "_blank" %}<a class="terminal-button caret-blink" href="{% if url %}{{ url }}{% else %}#{% endif %}" target="_blank" rel="noopener noreferrer">[ {{ text }} ]</a>{% else %}<a class="terminal-button caret-blink" href="{% if url %}{{ url }}{% else %}#{% endif %}">[ {{ text }} ]</a>{% endif %}{% endif %}
{# terminal button — keep {% button %} tag intact; terminal chrome on hugging wrapper #}
{% with align=align|default:"left" %}<div class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}"><span class="terminal-mono terminal-mono inline-flex uppercase tracking-wider font-semibold">{% 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 %}</span></div>{% endwith %}

View File

@ -0,0 +1,2 @@
{# terminal card — dashed neon panel; fields layout/link/media/title/text/linkLabel #}
{% with layout=layout|default:"vertical" %}<article class="bn-card tty-frame overflow-hidden rounded-md border border-dashed border-primary/40 bg-card text-card-foreground tty-frame {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">{% if link %}<a href="{{ link }}" class="contents">{% endif %}{% if media %}<div class="overflow-hidden bg-muted {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">{% if title %}<h3 class="text-lg font-semibold tracking-tight text-foreground terminal-heading">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed text-muted-foreground terminal-page">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="mt-2 inline-flex items-center gap-1 text-sm font-medium text-primary terminal-mono uppercase tracking-wide">{{ linkLabel }}::lucide:arrow-right:sm::</span>{% endif %}</div>{% if link %}</a>{% endif %}</article>{% endwith %}

View File

@ -0,0 +1,18 @@
{# category-list (terminal override) — from the `categories` provider. list or pills. #}
{% set pills = style == "pills" %}
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="terminal-eyebrow terminal-mono mb-4 text-xs font-semibold uppercase tracking-widest text-accent">{{ heading }}</h2>{% endif %}
{% if pills %}
<div class="flex flex-wrap gap-2">
{% for cat in categories %}
<a href="{{ cat.url }}" class="inline-flex items-center gap-1.5 border border-primary/50 bg-card px-3 py-1 terminal-mono text-sm uppercase tracking-wide text-card-foreground tty-frame transition-colors hover:text-primary">{{ cat.name }}{% if showCount %}<span class="text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a>
{% endfor %}
</div>
{% else %}
<ul class="space-y-1">
{% for cat in categories %}
<li><a href="{{ cat.url }}" class="flex items-center justify-between rounded-md px-3 py-2 terminal-mono text-sm uppercase tracking-wide text-muted-foreground transition-colors hover:bg-accent hover:text-primary"><span>{{ cat.name }}</span>{% if showCount %}<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a></li>
{% endfor %}
</ul>
{% endif %}
</section>

View File

@ -0,0 +1,18 @@
{# contact-card terminal override: neon channel panels. All field reads + control flow preserved. #}
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4">
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if description %}<p class="mt-3 max-w-2xl text-lg text-muted-foreground">{{ description }}</p>{% endif %}
<div class="mt-10 grid grid-cols-1 gap-4 {% if columns == 3 %}sm:grid-cols-2 lg:grid-cols-3{% elif columns == 1 %}{% else %}sm:grid-cols-2{% endif %}">
{% for channel in channels %}
<div class="tty-corners flex items-start gap-4 rounded-md border border-primary/40 bg-card p-5 text-card-foreground tty-frame">
{% if channel.icon %}<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"></use></svg></span>{% endif %}
<div class="min-w-0">
{% if channel.label %}<div class="terminal-mono text-sm font-medium uppercase tracking-wide text-accent">{{ channel.label }}</div>{% endif %}
{% if channel.value %}<div class="mt-0.5 break-words font-medium">{% if channel.link %}<a href="{{ channel.link }}" class="transition-colors hover:text-primary">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,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 %}
<section id="contact-form-{{ bid }}" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-2xl px-4">
{% if form.submitted %}
<div class="tty-corners rounded-md border border-primary/50 bg-card p-8 text-center text-card-foreground tty-frame">
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-md bg-primary/10 text-primary"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg></div>
<p class="terminal-heading text-lg font-medium">{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}</p>
</div>
{% else %}
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if description %}<p class="mt-3 text-lg text-muted-foreground">{{ description }}</p>{% endif %}
{% if form.errors._form %}<div class="mt-6 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">{{ form.errors._form }}</div>{% endif %}
{% with formAction="/api/blocks/"|add:bid|add:"/submit" formTarget="#contact-form-"|add:bid %}
{% form action=formAction hx_target=formTarget hx_swap="outerHTML" class="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2" %}
{% for field in fields %}
<div class="{% if field.width == "half" %}sm:col-span-1{% else %}sm:col-span-2{% endif %}">
{% if field.label %}<label for="cf-{{ bid }}-{{ field.name }}" class="terminal-mono mb-1.5 block text-sm font-medium uppercase tracking-wide text-accent">{{ field.label }}{% if field.required %} <span class="text-destructive">*</span>{% endif %}</label>{% endif %}
{% if field.type == "textarea" %}
<textarea id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" rows="5"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"></textarea>
{% elif field.type == "select" %}
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
{% if field.placeholder %}<option value="">{{ field.placeholder }}</option>{% endif %}
{% for opt in field.options %}<option value="{{ opt.value }}">{{ opt.label|default:opt.value }}</option>{% endfor %}
</select>
{% else %}
<input id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" type="{{ field.type|default:"text" }}"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring">
{% endif %}
</div>
{% endfor %}
<div class="hidden" aria-hidden="true"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
<div class="sm:col-span-2">
<button type="submit" class="terminal-mono inline-flex items-center justify-center rounded-md bg-primary px-6 py-2.5 text-sm font-medium uppercase tracking-wider text-primary-foreground transition-colors hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">{{ submitLabel|default:"Send message" }}</button>
</div>
{% endform %}
{% endwith %}
{% endif %}
</div>
</section>
{% endwith %}

View File

@ -0,0 +1,50 @@
{# countdown terminal override: neon HUD digit boxes, heading. Timer script + data attributes preserved byte-for-byte. #}
<section class="relative overflow-hidden py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto max-w-4xl px-4 text-center">
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-10" data-countdown>
<div class="flex items-start justify-center gap-3 sm:gap-6" data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining">
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] rounded-md border border-primary/50 bg-card px-3 py-4 text-card-foreground tty-frame{% endif %}"><span class="terminal-mono text-4xl font-bold tabular-nums sm:text-5xl text-primary" data-unit="days">--</span><span class="terminal-mono mt-1 text-xs font-medium uppercase tracking-widest text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] rounded-md border border-primary/50 bg-card px-3 py-4 text-card-foreground tty-frame{% endif %}"><span class="terminal-mono text-4xl font-bold tabular-nums sm:text-5xl text-primary" data-unit="hours">--</span><span class="terminal-mono mt-1 text-xs font-medium uppercase tracking-widest text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] rounded-md border border-primary/50 bg-card px-3 py-4 text-card-foreground tty-frame{% endif %}"><span class="terminal-mono text-4xl font-bold tabular-nums sm:text-5xl text-primary" data-unit="minutes">--</span><span class="terminal-mono mt-1 text-xs font-medium uppercase tracking-widest text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}min-w-[4.5rem] rounded-md border border-primary/50 bg-card px-3 py-4 text-card-foreground tty-frame{% endif %}"><span class="terminal-mono text-4xl font-bold tabular-nums sm:text-5xl text-primary" data-unit="seconds">--</span><span class="terminal-mono mt-1 text-xs font-medium uppercase tracking-widest text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
</div>
<div class="mt-8 hidden" data-countdown-expired>
<p class="terminal-heading text-xl font-medium">{{ expiredMessage|default:"This has started." }}</p>
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="terminal-mono mt-4 inline-flex items-center gap-2 rounded-md bg-primary px-5 py-3 font-semibold uppercase tracking-wider text-primary-foreground transition-opacity hover:opacity-90">{{ expiredLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
</div>
<noscript><p class="mt-6 text-muted-foreground">Counting down to <time datetime="{{ target }}">{{ target }}</time>.</p></noscript>
</div>
</section>
<script>
(function(){
var timers = document.querySelectorAll('[data-countdown-timer]');
timers.forEach(function(el){
var target = new Date(el.getAttribute('data-target')).getTime();
if (isNaN(target)) return;
var wrap = el.closest('[data-countdown]');
var expired = wrap ? wrap.querySelector('[data-countdown-expired]') : null;
function pad(n){ return (n < 10 ? '0' : '') + n; }
function set(unit, value){ var node = el.querySelector('[data-unit="' + unit + '"]'); if (node) node.textContent = value; }
var interval;
function tick(){
var diff = target - Date.now();
if (diff <= 0){
el.style.display = 'none';
if (expired) expired.classList.remove('hidden');
if (interval) clearInterval(interval);
return;
}
set('days', Math.floor(diff / 86400000));
set('hours', pad(Math.floor(diff % 86400000 / 3600000)));
set('minutes', pad(Math.floor(diff % 3600000 / 60000)));
set('seconds', pad(Math.floor(diff % 60000 / 1000)));
}
tick();
interval = setInterval(tick, 1000);
});
})();
</script>

View File

@ -0,0 +1,16 @@
{# cta terminal override: scanline overlay, heading, CTAs. All field reads preserved. #}
<section class="relative overflow-hidden py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto max-w-6xl px-4">
<div class="tty-corners {% if background == "band" %}rounded-md bg-primary px-6 py-14 text-center text-primary-foreground md:px-12{% elif background == "card" %}rounded-md border border-primary/50 bg-card px-6 py-14 text-center text-card-foreground tty-frame md:px-12{% else %}flex flex-col items-center gap-8 rounded-md border border-primary/30 bg-muted px-6 py-12 text-foreground md:flex-row md:justify-between md:px-12 md:text-left{% endif %}">
<div class="{% if background != "split" %}mx-auto max-w-2xl{% endif %}">
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if text %}<p class="mt-3 text-lg {% if background == "band" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}">{{ text }}</p>{% endif %}
</div>
<div class="flex flex-wrap items-center justify-center gap-3 {% if background != "split" %}mt-8{% else %}shrink-0{% endif %}">
{% if primaryUrl and primaryLabel %}<a href="{{ primaryUrl }}" class="terminal-mono inline-flex items-center gap-2 rounded-md px-6 py-3 font-semibold uppercase tracking-wider transition-opacity hover:opacity-90 {% if background == "band" %}bg-background text-foreground{% else %}bg-primary text-primary-foreground{% endif %}">{{ primaryLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
{% if secondaryUrl and secondaryLabel %}<a href="{{ secondaryUrl }}" class="terminal-mono inline-flex items-center rounded-md border px-6 py-3 font-semibold uppercase tracking-wider transition-colors {% if background == "band" %}border-primary-foreground/40 text-primary-foreground hover:bg-primary-foreground/10{% else %}border-accent/60 text-accent hover:bg-accent/10{% endif %}">{{ secondaryLabel }}</a>{% endif %}
</div>
</div>
</div>
</section>

View File

@ -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" %}<div class="bn-divider mx-auto my-8 flex items-center gap-4 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><span class="h-px flex-1 bg-gradient-to-r from-transparent via-primary to-transparent"></span><span class="shrink-0 text-primary">::{{ icon|default:"lucide:asterisk" }}:md text-primary::</span><span class="h-px flex-1 bg-gradient-to-l from-transparent via-primary to-transparent"></span></div>{% elif variant == "spacer-rule" %}<div class="bn-divider mx-auto py-10 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><hr class="mx-auto w-16 border-0 border-t {% if weight == "thick" %}border-t-4{% elif weight == "medium" %}border-t-2{% endif %} border-primary/60"></div>{% else %}<hr class="bn-divider mx-auto my-8 border-0 border-t {% if weight == "thick" %}border-t-4{% elif weight == "medium" %}border-t-2{% endif %} border-primary/60 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator">{% endif %}{% endwith %}

View File

@ -0,0 +1,41 @@
{# event-list terminal override: neon cards, mono datetime. All field reads + control flow preserved. #}
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
{% if layout == "cards" %}
<div class="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{% for event in events %}{% if not (hidePast and event.past) %}
<article class="tty-corners flex flex-col overflow-hidden rounded-md border border-primary/40 bg-card text-card-foreground tty-frame{% if event.past %} opacity-60{% endif %}">
{% if event.media %}<div class="aspect-video overflow-hidden bg-muted">{% img event.media alt=event.title class="h-full w-full object-cover" %}</div>{% endif %}
<div class="flex flex-1 flex-col gap-2 p-5">
<div class="terminal-mono text-sm font-medium uppercase tracking-wide text-primary"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} &ndash; {{ event.end }}{% endif %}</time>{% if event.timezone %} <span class="text-muted-foreground">{{ event.timezone }}</span>{% endif %}</div>
<h3 class="terminal-heading text-lg font-semibold">{{ event.title }}</h3>
{% if event.venue %}<p class="text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
{% if event.link %}<a href="{{ event.link }}" class="terminal-mono mt-auto inline-flex items-center gap-1 pt-2 text-sm font-medium uppercase tracking-wide text-primary hover:underline">{{ event.linkLabel|default:"Details" }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
</article>
{% endif %}{% endfor %}
</div>
{% else %}
<ul class="mt-10 divide-y divide-primary/30 border-y border-primary/30">
{% for event in events %}{% if not (hidePast and event.past) %}
<li class="flex flex-col gap-3 py-5 sm:flex-row sm:items-center sm:gap-6{% if event.past %} opacity-60{% endif %}">
<div class="shrink-0 sm:w-52">
<div class="terminal-mono text-sm font-semibold text-primary"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
{% if event.end %}<div class="terminal-mono text-xs text-muted-foreground">to {{ event.end }}</div>{% endif %}
{% if event.timezone %}<div class="terminal-mono text-xs text-muted-foreground">{{ event.timezone }}</div>{% endif %}
</div>
<div class="min-w-0 flex-1">
<h3 class="terminal-heading text-base font-semibold">{{ event.title }}</h3>
{% if event.venue %}<p class="text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="mt-1 text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
</div>
{% if event.link %}<a href="{{ event.link }}" class="terminal-mono inline-flex shrink-0 items-center rounded-md border border-primary/50 px-3 py-1.5 text-sm font-medium uppercase tracking-wide text-primary hover:bg-primary/10">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
</li>
{% endif %}{% endfor %}
</ul>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,18 @@
{# faq terminal override: heading, neon-tinted dividers, display questions. Chevron svg, {% for %}, and answer prose preserved. #}
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4">
{% if heading %}<h2 class="terminal-heading text-center text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-10 divide-y divide-primary/20 border-y border-primary/30">
{% for item in items %}
<details class="group py-2"{% if variant == "open-list" %} open{% endif %}>
<summary class="flex cursor-pointer list-none items-center justify-between gap-4 py-3 text-lg font-medium marker:content-none [&::-webkit-details-marker]:hidden">
<span class="terminal-heading">{{ item.question }}</span>
<svg class="h-5 w-5 shrink-0 text-primary transition-transform duration-200 group-open:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
</summary>
<div class="pb-4 text-muted-foreground [&_a]:text-primary [&_a]:underline">{{ item.answer|safe }}</div>
</details>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,19 @@
{# feature_grid terminal override: scanline hero overlay, mono eyebrow, heading, neon HUD cards. All field reads + control flow preserved. #}
<section class="relative overflow-hidden py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto max-w-6xl px-4">
{% if eyebrow %}<p class="terminal-mono text-center text-xs font-semibold uppercase tracking-widest text-accent">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="terminal-heading mt-2 text-center text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-12 grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
{% for item in items %}
<div class="{% if layout == "card" %}tty-corners rounded-md border border-primary/40 bg-card p-6 text-card-foreground tty-frame transition-transform hover:-translate-y-0.5{% elif layout == "centered" %}text-center{% endif %}">
{% if item.icon %}<div class="mb-4 inline-flex h-11 w-11 items-center justify-center rounded-md bg-primary/10 text-primary{% if layout == "centered" %} mx-auto{% endif %}"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg></div>{% endif %}
<h3 class="terminal-heading text-lg font-semibold">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" class="transition-colors hover:text-primary">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>
{% if item.text %}<p class="mt-2 text-muted-foreground">{{ item.text }}</p>{% endif %}
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="terminal-mono uppercase tracking-wide mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline">{{ item.linkLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,23 @@
{# featured-posts (terminal override) — grid of cards from the `posts` provider #}
{# (params: limit, featured=true). Neon HUD card: image, title, excerpt, meta. #}
<section class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="terminal-heading mb-8 text-2xl font-semibold tracking-tight md:text-3xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
{% for post in posts %}
<article class="tty-corners group flex flex-col overflow-hidden rounded-md border border-dashed border-primary/40 bg-card text-card-foreground tty-frame transition-transform hover:-translate-y-0.5">
{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-1 flex-col p-5">
<h3 class="terminal-heading text-lg font-semibold leading-snug"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h3>
{% if showExcerpt and post.excerpt %}<p class="terminal-page mt-2 flex-1 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
<div class="mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 terminal-mono text-xs uppercase tracking-wide text-accent">
{% if showAuthor and post.author_name %}<span class="font-medium text-foreground">{{ post.author_name }}</span>{% endif %}
{% if showDate and post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}
{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
</div>
</div>
</article>
{% endfor %}
</div>
</div>
</section>

View File

@ -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 %}<div class="flex items-center gap-3 {{ cls }}">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="text-muted-foreground hover:text-primary transition-colors"><span class="sr-only">{{ s.label }}</span>{% if s.icon %}::{{ s.icon }}::{% else %}{{ s.label }}{% endif %}</a>{% endfor %}</div>{% endif %}{% endmacro %}
{% macro newsletter() %}{% if showNewsletter %}<div class="max-w-sm"><h3 class="terminal-mono uppercase tracking-widest text-xs font-semibold text-accent">{{ newsletterTitle|default:"Stay in the loop" }}</h3>{% if newsletterDescription %}<p class="mt-1 text-sm text-muted-foreground">{{ newsletterDescription }}</p>{% endif %}<div class="mt-3">{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div></div>{% endif %}{% endmacro %}
{% macro legalbar() %}<div class="mt-10 pt-6 border-t border-primary/30 flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-muted-foreground"><p class="terminal-mono">{% if copyright %}&copy; {{ copyright }}{% else %}&copy; {{ companyName }}{% endif %}</p>{% if legal_items %}<nav class="flex flex-wrap items-center gap-x-4 gap-y-1">{% for l in legal_items %}<a href="{{ l.url|default:'#' }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="hover:text-primary transition-colors">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
<footer class="w-full border-t border-primary/30 {{ fbg }} {{ class }}">
<div class="{% if fullWidth %}w-full px-4 sm:px-6 lg:px-8{% else %}max-w-7xl mx-auto px-4 sm:px-6 lg:px-8{% endif %} py-16">
{% if fl == "minimal" %}
<div class="flex flex-col sm:flex-row items-center justify-between gap-6">
<div class="flex items-center gap-3">{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-8 w-auto"/>{% elif companyName %}<span class="terminal-heading text-lg font-bold text-foreground">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="text-sm text-muted-foreground hidden sm:inline">{{ tagline }}</span>{% endif %}</div>
{{ sociallinks("") }}
</div>
{{ legalbar() }}
{% elif fl == "centered" %}
<div class="flex flex-col items-center text-center gap-5">
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-10 w-auto"/>{% elif companyName %}<span class="terminal-heading text-xl font-bold text-foreground">{{ companyName }}</span>{% endif %}
{% if tagline %}<p class="max-w-md text-sm text-muted-foreground">{{ tagline }}</p>{% endif %}
{% if columns %}<nav class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">{% for col in columns %}{% for item in col.items %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="terminal-mono uppercase tracking-wide text-xs text-muted-foreground hover:text-primary transition-colors">{{ item.label }}</a>{% endfor %}{% endfor %}</nav>{% endif %}
{{ newsletter() }}
{{ sociallinks("justify-center") }}
</div>
{{ legalbar() }}
{% else %}
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8">
<div class="col-span-2 lg:col-span-2">
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-9 w-auto"/>{% elif companyName %}<span class="terminal-heading text-xl font-bold text-foreground">{{ companyName }}</span>{% endif %}
{% if tagline %}<p class="mt-3 max-w-xs text-sm text-muted-foreground">{{ tagline }}</p>{% endif %}
{% if address or email or phone %}<address class="mt-4 not-italic text-sm text-muted-foreground space-y-1">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="hover:text-primary transition-colors">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="hover:text-primary transition-colors">{{ phone }}</a></div>{% endif %}</address>{% endif %}
{{ sociallinks("mt-5") }}
</div>
{% for col in columns %}<div><h3 class="terminal-mono uppercase tracking-widest text-xs font-semibold text-accent">{{ col.header }}</h3><ul class="mt-3 space-y-2">{% for item in col.items %}<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="text-sm text-muted-foreground hover:text-primary transition-colors">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
{% if showNewsletter %}<div class="col-span-2 lg:col-span-1">{{ newsletter() }}</div>{% endif %}
</div>
{{ legalbar() }}
{% endif %}
</div>
</footer>

View File

@ -0,0 +1,52 @@
{# gallery 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 %}<div class="bn-gallery my-6{% if class %} {{ class }}{% endif %}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}"><div class="{% if layout == "carousel" %}flex snap-x snap-mandatory overflow-x-auto pb-2 {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% elif layout == "masonry" %}columns-2 {% if cols == 4 %}md:columns-4{% elif cols == 2 %}md:columns-2{% else %}md:columns-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% else %}grid grid-cols-2 {% if cols == 4 %}md:grid-cols-4{% elif cols == 2 %}md:grid-cols-2{% else %}md:grid-cols-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% endif %}">{% for image in images %}<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:"" }}" class="group relative block overflow-hidden rounded-md border border-primary/30 bg-muted tty-frame focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 {% if layout == "carousel" %}w-64 shrink-0 snap-start sm:w-80{% elif layout == "masonry" %}mb-4 w-full break-inside-avoid{% endif %}">{% if layout == "grid" %}{% img image.src alt=image.alt|default:"" class="aspect-square w-full object-cover" %}{% else %}{% img image.src alt=image.alt|default:"" class="h-auto w-full" %}{% endif %}{% if image.caption %}<span class="sr-only">{{ image.caption }}</span>{% endif %}</a>{% endfor %}</div></div>{% if lightbox %}<script>
(function(){
if(window.__bnLightbox)return;window.__bnLightbox=true;
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var ov,imgEl,capEl,items=[],idx=0,lastFocus=null;
function build(){
ov=document.createElement('div');
ov.setAttribute('role','dialog');ov.setAttribute('aria-modal','true');ov.setAttribute('aria-label','Image viewer');ov.tabIndex=-1;
ov.style.cssText='position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.9);padding:2rem;';
var btn='position:absolute;width:2.75rem;height:2.75rem;display:inline-flex;align-items:center;justify-content:center;border-radius:9999px;border:none;background:hsl(var(--background));color:hsl(var(--foreground));cursor:pointer;';
ov.innerHTML=
'<button type="button" data-lb-close aria-label="Close" style="'+btn+'top:1rem;right:1rem;"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#x"/></svg></button>'+
'<button type="button" data-lb-prev aria-label="Previous image" style="'+btn+'left:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-left"/></svg></button>'+
'<figure style="margin:0;max-width:92vw;max-height:92vh;display:flex;flex-direction:column;align-items:center;gap:0.75rem;"><img data-lb-img alt="" style="max-width:92vw;max-height:82vh;object-fit:contain;border-radius:0.5rem;'+(reduce?'':'transition:opacity .2s ease;')+'"><figcaption data-lb-cap style="color:hsl(var(--background));opacity:.85;font-size:.875rem;text-align:center;max-width:60ch;"></figcaption></figure>'+
'<button type="button" data-lb-next aria-label="Next image" style="'+btn+'right:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"/></svg></button>';
document.body.appendChild(ov);
imgEl=ov.querySelector('[data-lb-img]');capEl=ov.querySelector('[data-lb-cap]');
ov.addEventListener('click',function(e){if(e.target===ov||e.target.closest('[data-lb-close]'))close();});
ov.querySelector('[data-lb-prev]').addEventListener('click',function(e){e.stopPropagation();show(idx-1);});
ov.querySelector('[data-lb-next]').addEventListener('click',function(e){e.stopPropagation();show(idx+1);});
}
function show(i){
if(!items.length)return;
idx=(i+items.length)%items.length;
var a=items[idx];var im=a.querySelector('img');
imgEl.src=(im&&(im.currentSrc||im.src))||a.getAttribute('href');
imgEl.alt=im?im.alt:'';
var cap=a.getAttribute('data-caption')||'';capEl.textContent=cap;capEl.style.display=cap?'block':'none';
}
function open(g,link){
if(!ov)build();
items=Array.prototype.slice.call(g.querySelectorAll('[data-bn-gallery-item]'));
lastFocus=document.activeElement;ov.style.display='flex';
var multi=items.length>1;
ov.querySelector('[data-lb-prev]').style.display=multi?'inline-flex':'none';
ov.querySelector('[data-lb-next]').style.display=multi?'inline-flex':'none';
show(items.indexOf(link));ov.focus();
}
function close(){if(ov)ov.style.display='none';if(lastFocus&&lastFocus.focus)lastFocus.focus();}
document.addEventListener('click',function(e){
var link=e.target.closest('[data-bn-gallery][data-lightbox="true"] [data-bn-gallery-item]');
if(!link)return;e.preventDefault();open(link.closest('[data-bn-gallery]'),link);
});
document.addEventListener('keydown',function(e){
if(!ov||ov.style.display==='none')return;
if(e.key==='Escape'){e.preventDefault();close();}
else if(e.key==='ArrowLeft')show(idx-1);
else if(e.key==='ArrowRight')show(idx+1);
});
})();
</script>{% endif %}{% endwith %}

View File

@ -1 +1,7 @@
{% set lvl = level|default:2|integer %}{% if lvl < 1 or lvl > 6 %}{% set lvl = 2 %}{% endif %}{% set t = text|upper %}{% if lvl == 1 %}<h1 class="terminal-heading terminal-heading-1 text-3xl font-bold {{ textClass }}" style="text-transform: uppercase;"># {{ t }}</h1>{% elif lvl == 3 %}<h3 class="terminal-heading terminal-heading-3 text-xl font-semibold {{ textClass }}" style="text-transform: uppercase;">### {{ t }}</h3>{% elif lvl == 4 %}<h4 class="terminal-heading terminal-heading-4 text-lg font-semibold {{ textClass }}" style="text-transform: uppercase;">#### {{ t }}</h4>{% elif lvl == 5 %}<h5 class="terminal-heading terminal-heading-5 text-base font-medium {{ textClass }}" style="text-transform: uppercase;">##### {{ t }}</h5>{% elif lvl == 6 %}<h6 class="terminal-heading terminal-heading-6 text-base font-medium {{ textClass }}" style="text-transform: uppercase;">###### {{ t }}</h6>{% else %}<h2 class="terminal-heading terminal-heading-2 text-2xl font-bold {{ textClass }}" style="text-transform: uppercase;">## {{ t }}</h2>{% endif %}
{# terminal heading — terminal-heading everywhere; on h1/h2 only #}
{% if level == 1 %}<h1 class="text-4xl font-bold tracking-tight terminal-heading {{ textClass }} flex-1">{{ text }}</h1>
{% elif level == 3 %}<h3 class="text-2xl font-semibold terminal-heading {{ textClass }} flex-1">{{ text }}</h3>
{% elif level == 4 %}<h4 class="text-xl font-semibold terminal-heading {{ textClass }} flex-1">{{ text }}</h4>
{% elif level == 5 %}<h5 class="text-lg font-medium terminal-heading {{ textClass }} flex-1">{{ text }}</h5>
{% elif level == 6 %}<h6 class="text-base font-medium terminal-heading {{ textClass }} flex-1">{{ text }}</h6>
{% else %}<h2 class="text-3xl font-semibold tracking-tight terminal-heading {{ textClass }} flex-1">{{ text }}</h2>{% endif %}

View File

@ -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 %}<span class="terminal-eyebrow inline-block mb-4 border border-border bg-muted/40 px-3 py-1">$ {{ eyebrowText }} <span aria-hidden="true" class="caret-blink">_</span></span>{% endif %}
{% if headlineText %}<h1 class="terminal-heading text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight text-foreground">{{ headlineText }}</h1>{% endif %}
{% if subText %}<h2 class="mt-4 text-xl sm:text-2xl text-muted-foreground max-w-2xl{% if align == "center" %} mx-auto{% endif %}">{{ subText }}</h2>{% endif %}
{% if descText %}<p class="mt-4 max-w-xl leading-relaxed text-muted-foreground{% if align == "center" %} mx-auto{% endif %}">{{ descText }}</p>{% endif %}
{% if pText or sText %}<div class="mt-8 flex flex-col sm:flex-row gap-4{% if align == "center" %} justify-center{% endif %}">
{% if pText and pUrl %}<a href="{{ pUrl }}" class="terminal-mono inline-flex items-center justify-center gap-2 border border-primary bg-primary px-6 py-3 font-semibold uppercase tracking-wider text-primary-foreground transition-colors hover:bg-transparent hover:text-primary">{{ pText }}</a>{% endif %}
{% if sText and sUrl %}<a href="{{ sUrl }}" class="terminal-mono inline-flex items-center justify-center border border-border px-6 py-3 font-semibold uppercase tracking-wider text-foreground transition-colors hover:border-primary hover:text-primary">{{ sText }}</a>{% endif %}
</div>{% endif %}
{% if trustLogos %}<div class="mt-10">
<p class="terminal-mono text-xs uppercase tracking-widest text-muted-foreground mb-3">Trusted by</p>
<div class="flex flex-wrap items-center gap-6{% if align == "center" %} justify-center{% endif %}">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}" target="_blank" rel="noopener noreferrer" class="opacity-60 transition-opacity hover:opacity-100">{% img logo.image alt=logo.alt class="h-8 w-auto" %}</a>{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-60" %}{% endif %}{% endfor %}</div>
</div>{% endif %}
{% endmacro %}
{% if variant == "split" %}
<section class="hero grid grid-cols-1 lg:grid-cols-2 overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="relative min-h-[320px] bg-muted {% if mediaPosition == "left" %}order-1 lg:order-1{% else %}order-1 lg:order-2{% endif %}">{% if hasImage %}{% img backgroundImage alt=headlineText class="absolute inset-0 h-full w-full object-cover" layout="hero" %}{% else %}<div class="tty-rain-wrap"><div class="tty-rain-fallback"></div><canvas class="tty-rain-canvas" aria-hidden="true"></canvas></div>{% endif %}<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div></div>
<div class="flex flex-col justify-center px-6 py-16 lg:px-12 {% if mediaPosition == "left" %}order-2 lg:order-2{% else %}order-2 lg:order-1{% endif %}"><div class="max-w-lg text-left">{{ herocontent("left") }}</div></div>
</section>
{% else %}
{% set align = "center" %}
{% if variant == "left-aligned" %}{% set align = "left" %}{% endif %}
<section class="hero relative flex flex-col overflow-hidden bg-background text-foreground {% if minHeight == "screen" %}min-h-screen{% elif minHeight == "50vh" %}min-h-[50vh]{% elif minHeight == "auto" %}min-h-[400px]{% else %}min-h-[75vh]{% endif %} {% if verticalAlign == "top" %}justify-start pt-20{% elif verticalAlign == "bottom" %}justify-end pb-20{% else %}justify-center{% endif %}{% if class %} {{ class }}{% endif %}">
{% if hasImage %}<div class="absolute inset-0 z-0">{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}{% if showOverlay %}<div class="absolute inset-0 {% if overlayColor == "light" %}bg-background/60{% elif overlayColor == "primary" %}bg-primary/50{% elif overlayOpacity <= 30 %}bg-black/30{% elif overlayOpacity <= 50 %}bg-black/50{% elif overlayOpacity <= 70 %}bg-black/70{% else %}bg-black/80{% endif %}"></div>{% endif %}</div>{% else %}<div class="tty-rain-wrap z-0"><div class="tty-rain-fallback"></div><canvas class="tty-rain-canvas" aria-hidden="true"></canvas></div>{% endif %}
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0 z-0"></div>
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 sm:px-6 lg:px-8 {% if variant == "left-aligned" %}text-left{% else %}text-center{% endif %}">{{ herocontent(align) }}</div>
</section>
{% endif %}
<script>
(function(){
if(window.__terminalRain)return;window.__terminalRain=1;
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function initCanvas(cv){
if(cv.__init)return;cv.__init=1;
var ctx=cv.getContext&&cv.getContext('2d');if(!ctx)return;
var wrap=cv.parentElement;
var cs=getComputedStyle(document.documentElement);
function raw(n){return cs.getPropertyValue(n).trim();}
var fg=raw('--foreground');
var col1='hsl('+(raw('--primary')||fg)+')';
var col2='hsl('+(raw('--accent')||fg)+')';
var fade='hsl('+(raw('--background')||fg)+' / 0.12)';
var glyphs='01<>[]{}#$%&*+=/|_~^:;.,abcdefghijklmnopqrstuvwxyz'.split('');
var fontSize=14,cols=0,drops=[],W=0,H=0,dpr=1;
function resize(){
dpr=Math.min(window.devicePixelRatio||1,2);
W=wrap.clientWidth;H=wrap.clientHeight;
if(W===0||H===0)return;
cv.width=W*dpr;cv.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);
cols=Math.max(1,Math.floor(W/fontSize));
drops=[];for(var i=0;i<cols;i++)drops[i]=Math.random()*-50;
}
resize();
var last=0,fps=1000/18;
function frame(t){
requestAnimationFrame(frame);
if(t-last<fps)return;last=t;
if(W===0||H===0){resize();return;}
ctx.fillStyle=fade;ctx.fillRect(0,0,W,H);
ctx.font=fontSize+'px monospace';
for(var i=0;i<cols;i++){
var ch=glyphs[Math.floor(Math.random()*glyphs.length)];
var x=i*fontSize,y=drops[i]*fontSize;
ctx.fillStyle=Math.random()<0.85?col1:col2;
ctx.fillText(ch,x,y);
if(y>H&&Math.random()>0.975)drops[i]=0;
drops[i]++;
}
}
if(window.ResizeObserver){var ro=new ResizeObserver(resize);ro.observe(wrap);}else{window.addEventListener('resize',resize);}
requestAnimationFrame(frame);
}
function boot(){
if(reduce)return;
var cvs=document.querySelectorAll('.tty-rain-canvas');
if(!cvs.length)return;
if(!('IntersectionObserver' in window)){cvs.forEach(initCanvas);return;}
var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){initCanvas(e.target);io.unobserve(e.target);}});},{threshold:0.05});
cvs.forEach(function(c){io.observe(c);});
}
if(document.readyState!='loading')boot();else document.addEventListener('DOMContentLoaded',boot);
})();
</script>

View File

@ -0,0 +1,31 @@
{# hours-location terminal override: mono readout table, neon map panel. Map lazy-load button + inline handler preserved. #}
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-5xl px-4">
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
<div class="mt-10 grid grid-cols-1 gap-10 md:grid-cols-2">
<div>
{% if hours %}
<table class="terminal-mono w-full text-sm">
<tbody class="divide-y divide-primary/30">
{% for row in hours %}
<tr>
<th scope="row" class="py-2 pr-4 text-left font-medium uppercase tracking-wide">{{ row.day }}</th>
<td class="py-2 text-right {% if row.closed %}text-muted-foreground{% else %}text-primary{% endif %} tabular-nums">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if specialHoursNote %}<p class="mt-4 rounded-md border border-primary/40 bg-muted px-3 py-2 text-sm text-muted-foreground">{{ specialHoursNote }}</p>{% endif %}
{% if address %}<div class="mt-6"><div class="terminal-mono text-sm font-medium uppercase tracking-wide text-accent">Address</div><address class="mt-1 whitespace-pre-line not-italic">{{ address }}</address></div>{% endif %}
{% if phone %}<div class="mt-4"><div class="terminal-mono text-sm font-medium uppercase tracking-wide text-accent">Phone</div><a href="tel:{{ phone }}" class="terminal-mono mt-1 inline-block font-medium transition-colors hover:text-primary">{{ phone }}</a></div>{% endif %}
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener noreferrer" class="terminal-mono mt-6 inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium uppercase tracking-wide text-primary-foreground transition-colors hover:bg-primary/90"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#navigation"></use></svg>Get directions</a>{% endif %}
</div>
{% if mapEmbedUrl %}
<div class="tty-corners aspect-square w-full overflow-hidden rounded-md border border-primary/40 bg-muted tty-frame md:aspect-auto">
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('class','h-full w-full border-0');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" class="terminal-mono flex h-full min-h-[16rem] w-full items-center justify-center gap-2 text-sm font-medium uppercase tracking-wide transition-colors hover:bg-accent/20 hover:text-foreground"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#map-pin"></use></svg>{{ mapButtonLabel|default:"Load map" }}</button>
</div>
{% endif %}
</div>
</div>
</section>

View File

@ -1 +1,2 @@
<figure class="ascii-frame">{% if src %}<img src="{{ src }}" alt="{{ alt|default:caption }}">{% else %}<div class="terminal-mono" style="padding: 1rem;">[ no image ]</div>{% endif %}<figcaption>[fig.{% if caption %} {{ caption }}{% endif %}]</figcaption></figure>
{# 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" %}<figure class="flex-1 {% if class %}{{ class }} {% endif %}{% if align == "left" %}flex justify-start{% elif align == "right" %}flex justify-end{% else %}flex justify-center{% endif %}"><div class="relative overflow-hidden tty-frame {% if size == "xs" %}max-w-[200px] w-full{% elif size == "sm" %}max-w-[400px] w-full{% elif size == "md" %}max-w-[600px] w-full{% elif size == "lg" %}max-w-[800px] w-full{% else %}w-full{% endif %}{% if aspectRatio == "square" %} aspect-square{% elif aspectRatio == "video" %} aspect-video{% elif aspectRatio == "portrait" %} aspect-[3/4]{% elif aspectRatio == "wide" %} aspect-[21/9]{% endif %}{% if rounded == "sm" %} rounded-sm{% elif rounded == "md" %} rounded-md{% elif rounded == "lg" %} rounded-lg{% elif rounded == "xl" %} rounded-xl{% elif rounded == "full" %} rounded-full{% endif %}{% if shadow == "sm" %} shadow-sm{% elif shadow == "md" %} shadow-md{% elif shadow == "lg" %} shadow-lg{% elif shadow == "xl" %} shadow-xl{% endif %}">{% if objectFit == "contain" %}{% img src alt=alt class="w-full h-full object-contain" layout=size %}{% elif objectFit == "fill" %}{% img src alt=alt class="w-full h-full object-fill" layout=size %}{% elif objectFit == "none" %}{% img src alt=alt class="w-full h-full object-none" layout=size %}{% else %}{% img src alt=alt class="w-full h-full object-cover" layout=size %}{% endif %}</div>{% if caption %}<figcaption class="mt-2 text-sm text-muted-foreground text-center terminal-mono">{{ caption }}</figcaption>{% endif %}</figure>{% endwith %}

View File

@ -0,0 +1,16 @@
{# logo_strip terminal override: mono kicker. Marquee classes, {% img %}, and keyframes <style> preserved byte-for-byte. #}
<section class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<p class="terminal-mono text-center text-sm font-semibold uppercase tracking-widest text-accent">{{ heading }}</p>{% endif %}
<div class="mt-8 {% if marquee %}group relative overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]{% endif %}">
<div class="flex flex-wrap items-center justify-center gap-x-10 gap-y-6 {% if marquee %}flex-nowrap [animation:logo-marquee_30s_linear_infinite] motion-reduce:[animation:none] group-hover:[animation-play-state:paused]{% endif %}">
{% for logo in logos %}
{% if logo.url %}<a href="{{ logo.url }}" class="block shrink-0 opacity-80 transition hover:opacity-100">{% endif %}
<span class="inline-flex h-8 items-center{% if grayscale %} grayscale transition-[filter] hover:grayscale-0{% endif %}">{% img logo.image alt=logo.alt class="h-8 w-auto object-contain" %}</span>
{% if logo.url %}</a>{% endif %}
{% endfor %}
</div>
</div>
</div>
</section>
{% if marquee %}<style>@keyframes logo-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}</style>{% endif %}

View File

@ -0,0 +1,30 @@
{# menu-list terminal override: mono price readouts, neon section rules. All field reads + control flow preserved. #}
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-5xl px-4">
{% if heading %}<h2 class="terminal-heading text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-10 grid grid-cols-1 gap-x-12 gap-y-10 {% if columns == 2 %}md:grid-cols-2{% endif %}">
{% for section in sections %}
<div class="menu-section">
{% if section.title %}<h3 class="terminal-heading border-b border-primary/40 pb-2 text-xl font-semibold">{{ section.title }}</h3>{% endif %}
{% if section.note %}<p class="mt-2 text-sm text-muted-foreground">{{ section.note }}</p>{% endif %}
<ul class="mt-4 space-y-5">
{% for item in section.items %}
<li class="flex items-baseline gap-4">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span class="font-medium">{{ item.name }}</span>
{% for badge in item.badges %}{% if badge.label %}<span class="terminal-mono inline-block rounded border border-accent/50 px-2 py-0.5 text-xs uppercase tracking-wide text-accent">{{ badge.label }}</span>{% endif %}{% endfor %}
</div>
{% if item.description %}<p class="mt-0.5 text-sm text-muted-foreground">{{ item.description }}</p>{% endif %}
</div>
{% if item.price %}<span class="terminal-mono shrink-0 font-medium tabular-nums text-primary">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% if footnote %}<p class="terminal-mono mt-10 text-xs text-muted-foreground">{{ footnote }}</p>{% endif %}
</div>
</section>

View File

@ -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) %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} class="terminal-mono uppercase tracking-wider px-3 py-2 text-xs font-medium text-foreground/80 hover:text-primary transition-colors {{ item.css_class }}">{{ item.label }}</a>{% endmacro %}
{% macro dropdown(item) %}<div class="relative group">
<button type="button" class="terminal-mono uppercase tracking-wider px-3 py-2 text-xs font-medium text-foreground/80 hover:text-primary transition-colors inline-flex items-center gap-1">{{ item.label }}<svg class="h-4 w-4 transition-transform group-hover:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg></button>
{% if use_mega %}<div class="absolute left-1/2 -translate-x-1/2 mt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50"><div class="p-6 bg-card border border-primary/40 tty-frame rounded-md shadow-xl grid gap-8" style="grid-template-columns:repeat({{ item.children|length|default:1 }},minmax(10rem,1fr));min-width:min(90vw,42rem)">{% for child in item.children %}<div class="space-y-1">{% if child.item_type == "header" %}<div class="terminal-mono px-2 py-1 text-xs font-semibold text-accent uppercase tracking-widest border-b border-primary/40 mb-2">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-2 py-2 rounded hover:bg-accent transition-colors {{ child.css_class }}">{% if child.icon %}<span class="mr-1.5 text-primary">::{{ child.icon }}::</span>{% endif %}<span class="text-sm font-medium text-foreground">{{ child.label }}</span>{% if child.description %}<span class="block text-xs text-muted-foreground mt-0.5">{{ child.description }}</span>{% endif %}</a>{% endif %}</div>{% endfor %}</div></div>
{% else %}<div class="absolute left-0 mt-1 w-56 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50"><div class="py-2 bg-card border border-primary/40 tty-frame rounded-md shadow-lg">{% for child in item.children %}{% if child.item_type == "header" %}<div class="terminal-mono px-4 py-2 text-xs font-semibold text-accent uppercase tracking-widest">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-4 py-2 text-sm text-foreground hover:bg-accent transition-colors {{ child.css_class }}">{{ child.label }}</a>{% endif %}{% endfor %}</div></div>{% endif %}
</div>{% endmacro %}
{% macro brand(size) %}<a href="/" class="flex-shrink-0 flex items-center">{% if logoType == "text" and logoText.text %}<span class="terminal-heading text-xl font-bold text-foreground {{ logoText.color }}">{{ logoText.text }}</span>{% elif logo %}<img src="{{ logo }}" alt="{{ alt|default:companyName }}" class="{{ size }} w-auto"{% if logoMaxWidth or logoMaxHeight %} style="{% if logoMaxWidth %}max-width:{{ logoMaxWidth }};{% endif %}{% if logoMaxHeight %}max-height:{{ logoMaxHeight }};{% endif %}"{% endif %}/>{% elif companyName %}<span class="terminal-heading text-xl font-bold text-foreground">{{ companyName }}</span>{% endif %}</a>{% endmacro %}
{% macro ctabtn(extra) %}{% if cta_text %}<a href="{{ cta_url|default:'#' }}" class="terminal-mono uppercase tracking-wider inline-flex items-center rounded-none bg-primary px-4 py-2 text-xs font-semibold text-primary-foreground hover:bg-primary/90 transition-colors {{ extra }}">{{ cta_text }}</a>{% endif %}{% endmacro %}
<nav data-bn-navbar data-bn-nav-variant="{{ nv }}" data-bn-sticky="{{ sb }}"{% if is_transparent %} data-bn-transparent="1"{% endif %} class="bn-navbar w-full {% if sb == "sticky" or sb == "shrink-on-scroll" %}sticky top-0 z-40 {% endif %}{% if is_transparent %}absolute top-0 left-0 z-40 text-white{% else %}{{ bg }}{% if borderBottom %} border-b border-primary/30{% endif %}{% endif %} {{ class }}">
{% if showUtilityBar %}<div class="terminal-mono w-full text-xs {% if not showOnMobile %}hidden sm:block{% endif %} bg-muted/60 text-muted-foreground border-b border-primary/20"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between h-8"><div>{{ utilityBarText|safe }}</div><div class="flex items-center gap-4">{% if utilityBarPhone %}<a href="tel:{{ utilityBarPhone }}" class="hover:text-primary transition-colors">{{ utilityBarPhone }}</a>{% endif %}{% if utilityBarEmail %}<a href="mailto:{{ utilityBarEmail }}" class="hover:text-primary transition-colors">{{ utilityBarEmail }}</a>{% endif %}</div></div></div>{% endif %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bn-navbar-row flex items-center justify-between h-16">
{% if nv == "centered-logo" %}
<div class="hidden md:flex items-center gap-1 flex-1">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}</div>
<div class="flex items-center justify-center">{{ brand("h-9") }}</div>
<div class="hidden md:flex items-center gap-3 flex-1 justify-end">{{ ctabtn("") }}</div>
{% elif nv == "split" %}
<div class="hidden md:flex items-center gap-1 flex-1">{% for item in menu.items %}{% if forloop.Counter0 * 2 < menu.items|length %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endif %}{% endfor %}</div>
<div class="flex items-center justify-center">{{ brand("h-9") }}</div>
<div class="hidden md:flex items-center gap-1 flex-1 justify-end">{% for item in menu.items %}{% if forloop.Counter0 * 2 >= menu.items|length %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}</div>
{% else %}
<div class="flex items-center">{{ brand("h-8") }}</div>
<div class="hidden md:flex items-center gap-1">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}</div>
{% endif %}
<div class="flex items-center md:hidden">
<label for="bn-navbar-toggle" data-bn-nav-open aria-controls="bn-navbar-drawer" aria-expanded="false" class="inline-flex items-center justify-center p-2 rounded-md cursor-pointer hover:bg-accent/50 transition-colors"><span class="sr-only">Open menu</span><svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></label>
</div>
</div>
</div>
{# --- Mobile off-canvas drawer: pure-CSS checkbox baseline (works with no JS) --- #}
<input type="checkbox" id="bn-navbar-toggle" class="bn-navbar-toggle" aria-hidden="true"/>
<label for="bn-navbar-toggle" class="bn-navbar-overlay fixed inset-0 bg-black/50 z-40 md:hidden" aria-hidden="true"></label>
<div id="bn-navbar-drawer" class="bn-navbar-drawer fixed top-0 right-0 h-full w-80 max-w-[85vw] bg-background text-foreground z-50 shadow-xl md:hidden flex flex-col" role="dialog" aria-modal="true" aria-label="Site menu">
<div class="flex items-center justify-between p-4 border-b border-primary/30">{{ brand("h-8") }}<label for="bn-navbar-toggle" data-bn-nav-close class="p-2 rounded-md cursor-pointer text-muted-foreground hover:text-primary hover:bg-accent"><span class="sr-only">Close menu</span><svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></label></div>
<nav class="flex-1 overflow-y-auto py-4" aria-label="Mobile">{% for item in menu.items %}{% if item.children %}<details class="group/acc"><summary class="terminal-mono uppercase tracking-wide flex items-center justify-between px-6 py-3 text-sm font-medium cursor-pointer hover:bg-accent transition-colors list-none">{{ item.label }}<svg class="h-4 w-4 transition-transform group-open/acc:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg></summary><div class="pl-8 pb-2">{% for child in item.children %}{% if child.item_type != "header" %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-2 py-2 text-sm text-muted-foreground hover:text-primary transition-colors">{{ child.label }}</a>{% endif %}{% endfor %}</div></details>{% else %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="terminal-mono uppercase tracking-wide block px-6 py-3 text-sm font-medium hover:bg-accent transition-colors">{{ item.label }}</a>{% endif %}{% endfor %}</nav>
{% if cta_text %}<div class="p-4 border-t border-primary/30"><a href="{{ cta_url|default:'#' }}" class="terminal-mono uppercase tracking-wider block w-full text-center rounded-none bg-primary px-4 py-3 text-base font-semibold text-primary-foreground hover:bg-primary/90 transition-colors">{{ cta_text }}</a></div>{% endif %}
</div>
</nav>
<style>
.bn-navbar-toggle{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;}
.bn-navbar-drawer{transform:translateX(100%);transition:transform .3s ease;}
.bn-navbar-overlay{opacity:0;pointer-events:none;transition:opacity .3s ease;}
.bn-navbar-toggle:checked ~ .bn-navbar-drawer{transform:translateX(0);}
.bn-navbar-toggle:checked ~ .bn-navbar-overlay{opacity:1;pointer-events:auto;}
.bn-navbar-drawer details > summary::-webkit-details-marker{display:none;}
[data-bn-sticky="shrink-on-scroll"].bn-navbar-shrunk .bn-navbar-row{height:3rem;}
[data-bn-sticky="shrink-on-scroll"] .bn-navbar-row{transition:height .2s ease;}
[data-bn-transparent].bn-navbar-solid{position:sticky;color:inherit;background:hsl(var(--background));border-bottom:1px solid hsl(var(--border));}
</style>
<script>
(function(){
if(window.__bnNavInit)return;window.__bnNavInit=1;
function ready(fn){if(document.readyState!='loading')fn();else document.addEventListener('DOMContentLoaded',fn);}
ready(function(){
document.querySelectorAll('[data-bn-navbar]').forEach(function(nav){
var toggle=nav.querySelector('.bn-navbar-toggle');
var drawer=nav.querySelector('.bn-navbar-drawer');
var opener=nav.querySelector('[data-bn-nav-open]');
if(toggle&&drawer){
function focusables(){return drawer.querySelectorAll('a[href],button,label[for],input,summary,[tabindex]:not([tabindex="-1"])');}
toggle.addEventListener('change',function(){
var open=toggle.checked;
document.documentElement.style.overflow=open?'hidden':'';
document.body.style.overflow=open?'hidden':'';
if(opener)opener.setAttribute('aria-expanded',open?'true':'false');
if(open){var f=focusables();if(f.length)f[0].focus();}
else if(opener&&opener.focus)opener.focus();
});
drawer.addEventListener('keydown',function(e){
if(e.key==='Escape'){toggle.checked=false;toggle.dispatchEvent(new Event('change'));return;}
if(e.key!=='Tab')return;
var f=focusables();if(!f.length)return;
var first=f[0],last=f[f.length-1];
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}
});
drawer.querySelectorAll('a[href]').forEach(function(a){a.addEventListener('click',function(){toggle.checked=false;toggle.dispatchEvent(new Event('change'));});});
}
var sb=nav.getAttribute('data-bn-sticky');
var transparent=nav.getAttribute('data-bn-transparent')==='1';
if(sb==='shrink-on-scroll'||transparent){
var overHero=false;
if(transparent){var next=nav.nextElementSibling;overHero=!!next&&next.getBoundingClientRect().height>=240;if(!overHero)nav.classList.add('bn-navbar-solid');}
var onScroll=function(){
var y=window.scrollY||window.pageYOffset;
if(sb==='shrink-on-scroll')nav.classList.toggle('bn-navbar-shrunk',y>40);
if(transparent&&overHero)nav.classList.toggle('bn-navbar-solid',y>40);
};
window.addEventListener('scroll',onScroll,{passive:true});onScroll();
}
});
});
})();
</script>

View File

@ -0,0 +1,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. #}
<section class="py-12 bg-background text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4">
<div class="tty-corners rounded-md {% if layout == "inline" %}bg-primary px-6 py-10 text-primary-foreground tty-frame md:px-12{% else %}border border-dashed border-primary/40 bg-card px-6 py-10 text-card-foreground tty-frame md:px-10{% endif %} text-center">
{% if title %}<h2 class="terminal-heading text-2xl font-semibold tracking-tight md:text-3xl" data-text="{{ title }}">{{ title }}</h2>{% endif %}
{% if description %}<p class="terminal-page mx-auto mt-3 max-w-xl {% if layout == "inline" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}">{{ description }}</p>{% endif %}
<div class="mx-auto mt-6 max-w-md">{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}</div>
</div>
</div>
</section>

View File

@ -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. #}
<section class="relative overflow-hidden bg-background text-foreground py-20 md:py-28{% if class %} {{ class }}{% endif %}">
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto max-w-2xl px-4 text-center">
<p class="terminal-mono text-sm font-semibold uppercase tracking-[0.3em] text-accent">Error 404 <span aria-hidden="true" class="caret-blink">_</span></p>
<h1 class="terminal-heading mt-3 text-4xl font-bold tracking-tight md:text-5xl" data-text="{{ title|default:'Signal lost' }}">{{ title|default:"Signal lost" }}</h1>
<p class="mt-4 text-lg text-muted-foreground">{{ emptyMessage|default:"That route returned nothing. The page moved, shipped, or never existed." }}</p>
{% if context.path %}<p class="terminal-mono mt-3 text-sm text-muted-foreground">requested: <span class="text-primary">{{ context.path }}</span></p>{% endif %}
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
<a href="/" class="terminal-mono inline-flex items-center gap-2 rounded-md bg-primary px-6 py-3 font-semibold uppercase tracking-wider text-primary-foreground transition-opacity hover:opacity-90"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg>Back to home</a>
</div>
</div>
</section>

View File

@ -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. #}
<div class="password-reset-form tty-corners mx-auto max-w-md rounded-md border border-primary/40 bg-card p-6 text-card-foreground tty-frame">
<div id="reset-message"></div>
<div id="request-reset-panel">
<h2 class="terminal-heading text-2xl font-bold mb-2" data-text="Reset Password">Reset Password</h2>
<p class="text-muted-foreground mb-4">Enter your email address and we will send you a link to reset your password.</p>
<form hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML">
<div class="mb-4"><label class="terminal-mono mb-1 block text-xs font-medium uppercase tracking-wide text-accent">Email</label><input type="email" name="email" required class="w-full rounded-md border border-primary/40 bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"/></div>
<button type="submit" class="terminal-mono w-full rounded-md bg-primary px-4 py-2.5 font-semibold uppercase tracking-wider text-primary-foreground hover:bg-primary/90" hx-disabled-elt="this">Send Reset Link</button>
</form>
<p class="mt-4 text-center text-sm"><a href="/login" class="terminal-mono uppercase tracking-wide text-primary hover:underline">Back to login</a></p>
</div>
</div>

View File

@ -0,0 +1,16 @@
{# popular-posts (terminal override) — compact ranked list from the `posts` provider. #}
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="terminal-eyebrow terminal-mono mb-4 text-xs font-semibold uppercase tracking-widest text-accent">{{ heading }}</h2>{% endif %}
<ol class="space-y-4">
{% for post in posts %}
<li class="flex items-start gap-3">
{% if showRank %}<span class="terminal-mono mt-0.5 text-lg font-bold tabular-nums text-primary" style="text-shadow: 0 0 8px hsl(var(--primary));">{{ forloop.Counter }}</span>{% endif %}
{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="h-12 w-12 flex-shrink-0 overflow-hidden rounded-md border border-primary/40 bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover" %}</a>{% endif %}
<div class="min-w-0">
<h3 class="text-sm font-medium leading-snug"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h3>
{% if post.published_at %}<p class="terminal-mono mt-0.5 text-xs uppercase tracking-wide text-accent">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
</div>
</li>
{% endfor %}
</ol>
</section>

View File

@ -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 %}
<section class="hero relative flex min-h-[50vh] flex-col justify-end overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="absolute inset-0 z-0">{% img post.featured_image_url alt=post.featured_image_alt class="h-full w-full object-cover" layout="hero" %}<div class="absolute inset-0 bg-black/60"></div></div>
<div class="tty-scanlines pointer-events-none absolute inset-0 z-[1]"></div>
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 py-12 sm:px-6 lg:px-8 {% if center %}text-center{% endif %}">
<h1 class="terminal-heading text-3xl font-bold leading-tight text-white sm:text-4xl lg:text-5xl" data-text="{{ post.title }}">{{ post.title }}</h1>
{% if showExcerpt and post.excerpt %}<p class="terminal-page mt-4 max-w-2xl text-lg text-white/80{% if center %} mx-auto{% endif %}">{{ post.excerpt }}</p>{% endif %}
{% if showMeta %}<div class="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 terminal-mono text-sm uppercase tracking-wide text-white/80{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="font-medium text-white">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}</div>{% endif %}
</div>
</section>
{% else %}
<section class="relative overflow-hidden bg-background py-14 text-foreground md:py-20{% if class %} {{ class }}{% endif %}">
<div class="tty-scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto w-full max-w-3xl px-4 {% if center %}text-center{% endif %}">
<h1 class="terminal-heading text-3xl font-bold leading-tight tracking-tight sm:text-4xl lg:text-5xl" data-text="{{ post.title }}">{{ post.title }}</h1>
{% if showExcerpt and post.excerpt %}<p class="terminal-page mt-4 text-lg text-muted-foreground{% if center %} mx-auto max-w-2xl{% endif %}">{{ post.excerpt }}</p>{% endif %}
{% if showMeta %}<div class="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 terminal-mono text-sm uppercase tracking-wide text-accent{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="font-medium text-foreground">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,9 @@
{# post-metadata (terminal override) — author/date/reading-time row from the `post` provider. #}
{% if post %}
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 terminal-mono text-sm uppercase tracking-wide text-accent{% if class %} {{ class }}{% endif %}">
{% if showAuthor and post.author_name %}<span class="flex items-center gap-2">{% if showAvatar and post.author_avatar %}{% img post.author_avatar alt=post.author_name class="h-7 w-7 rounded-full border border-primary/40 object-cover" %}{% endif %}<span class="font-medium text-foreground">{{ post.author_name }}</span></span>{% endif %}
{% if showDate and post.published_at %}<time datetime="{{ post.published_at|date:"Y-m-d" }}">{{ post.published_at|date:"F j, Y" }}</time>{% endif %}
{% if showReadingTime and post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
{% if post.categories %}<span class="flex flex-wrap gap-1.5">{% for cat in post.categories %}<a href="{{ cat.url|default:'#' }}" class="border border-primary/40 bg-card px-2 py-0.5 text-xs text-card-foreground transition-colors hover:text-primary">{{ cat.name }}</a>{% endfor %}</span>{% endif %}
</div>
{% endif %}

View File

@ -0,0 +1,26 @@
{# pricing terminal override: scanline overlay, heading, HUD neon tier panels (magenta highlight / cyan standard), CTAs. Field reads + control flow preserved. #}
<section class="relative overflow-hidden py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="terminal-heading text-center text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for tier in tiers %}
<div class="tty-corners relative flex flex-col rounded-md border p-6 {% if tier.highlighted %}border-primary bg-card text-card-foreground tty-frame ring-1 ring-primary{% else %}border-primary/40 bg-card text-card-foreground tty-frame{% endif %}">
{% if tier.badge %}<span class="terminal-mono absolute -top-3 left-1/2 -translate-x-1/2 rounded-none bg-primary px-3 py-1 text-xs font-semibold uppercase tracking-widest text-primary-foreground">{{ tier.badge }}</span>{% endif %}
<h3 class="terminal-heading text-lg font-semibold">{{ tier.name }}</h3>
<p class="mt-4 flex items-baseline gap-1">
<span class="terminal-heading text-4xl font-bold tracking-tight" style="font-family:var(--font-heading)">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>
{% if tier.period %}<span class="terminal-mono text-sm text-muted-foreground">{{ tier.period }}</span>{% endif %}
</p>
{% if tier.description %}<p class="mt-3 text-sm text-muted-foreground">{{ tier.description }}</p>{% endif %}
{% if tier.features %}<ul class="mt-6 flex-1 space-y-3 text-sm">
{% for feature in tier.features %}<li class="flex items-start gap-2"><svg class="mt-0.5 h-4 w-4 shrink-0 text-primary" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg><span>{{ feature }}</span></li>{% endfor %}
</ul>{% endif %}
{% if tier.ctaUrl and tier.ctaLabel %}<a href="{{ tier.ctaUrl }}" class="terminal-mono uppercase tracking-wider mt-8 inline-flex w-full items-center justify-center rounded-none px-5 py-3 font-semibold transition-opacity hover:opacity-90 {% if tier.highlighted %}bg-primary text-primary-foreground{% else %}border border-primary/40 bg-background text-foreground hover:bg-muted{% endif %}">{{ tier.ctaLabel }}</a>{% endif %}
</div>
{% endfor %}
</div>
{% if billingNote %}<p class="terminal-mono mt-8 text-center text-sm text-muted-foreground">{{ billingNote }}</p>{% endif %}
</div>
</section>

View File

@ -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" %}<figure class="bn-quote my-6 {% if style == "inline-card" %}rounded-md border border-dashed border-primary/40 bg-card p-6 md:p-8 tty-frame{% elif style == "pull" %}border-l-4 border-dashed border-primary pl-6 md:pl-8 py-2{% else %}border-l-2 border-dashed border-primary/50 pl-5 py-1{% endif %}{% if align == "center" %} text-center{% endif %}{% if class %} {{ class }}{% endif %}"><blockquote{% if cite %} cite="{{ cite }}"{% endif %} class="{% if style == "pull" %}text-2xl md:text-3xl font-semibold leading-snug tracking-tight terminal-heading{% else %}text-lg md:text-xl leading-relaxed{% endif %} text-foreground">{{ quote|safe }}</blockquote>{% if attribution or role or cite %}<figcaption class="mt-4 text-sm text-muted-foreground terminal-mono{% if align == "center" %} flex flex-col items-center{% endif %}">{% if attribution %}<span class="font-medium text-foreground">{{ attribution }}</span>{% endif %}{% if role %}<span class="{% if attribution %}block {% endif %}text-muted-foreground">{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" class="{% if attribution or role %}mt-1 {% endif %}inline-block underline underline-offset-2 hover:text-foreground">Source</a>{% endif %}</figcaption>{% endif %}</figure>{% endwith %}

View File

@ -0,0 +1,17 @@
{# related-posts (terminal override) — grid from the `posts` provider. #}
{% if posts %}
<section class="border-t border-primary/30 py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<h2 class="terminal-heading mb-8 text-2xl font-semibold tracking-tight" data-text="{{ heading|default:'Related posts' }}">{{ heading|default:"Related posts" }}</h2>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% else %}lg:grid-cols-3{% endif %}">
{% for post in posts %}
<article class="tty-corners group flex flex-col rounded-md border border-dashed border-primary/40 bg-card p-4 text-card-foreground tty-frame transition-transform hover:-translate-y-0.5">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="mb-3 block aspect-video overflow-hidden rounded-md bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<h3 class="terminal-heading text-base font-semibold leading-snug"><a href="{{ post.url }}" class="transition-colors hover:text-primary">{{ post.title }}</a></h3>
{% if post.published_at %}<p class="terminal-mono mt-1 text-xs uppercase tracking-wide text-accent">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
</article>
{% endfor %}
</div>
</div>
</section>
{% endif %}

View File

@ -0,0 +1,14 @@
{# rich_section terminal override: mono eyebrow, heading, HUD-framed media, CTA. Field reads + {% img %} preserved. #}
<section class="py-16 md:py-24 {% if background == "muted" %}bg-muted text-foreground{% elif background == "card" %}bg-card text-card-foreground{% else %}bg-background text-foreground{% endif %}{% if class %} {{ class }}{% endif %}">
<div class="mx-auto grid max-w-6xl items-center gap-10 px-4 md:grid-cols-2 md:gap-16">
<div class="{% if mediaPosition == "left" %}md:order-2{% else %}md:order-1{% endif %}">
{% if eyebrow %}<p class="terminal-mono text-xs font-semibold uppercase tracking-widest text-accent">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="terminal-heading mt-2 text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if body %}<div class="mt-4 space-y-4 text-lg text-muted-foreground [&_a]:text-primary [&_a]:underline [&_strong]:text-foreground">{{ body|safe }}</div>{% endif %}
{% if ctaUrl and ctaLabel %}<a href="{{ ctaUrl }}" class="terminal-mono uppercase tracking-wider mt-6 inline-flex items-center gap-2 rounded-none bg-primary px-5 py-3 font-semibold text-primary-foreground transition-opacity hover:opacity-90">{{ ctaLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
<div class="{% if mediaPosition == "left" %}md:order-1{% else %}md:order-2{% endif %}">
{% if media %}<div class="tty-corners overflow-hidden rounded-md border border-dashed border-primary/50 tty-frame">{% img media alt=mediaAlt class="w-full h-auto object-cover" %}</div>{% endif %}
</div>
</div>
</section>

View File

@ -0,0 +1,2 @@
{# terminal rich-text — body font via terminal-prose #}
{% if html %}<article class="rich-text prose prose-lg max-w-none terminal-prose terminal-page">{{ html|safe }}</article>{% endif %}

View File

@ -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" %}
<nav class="flex-1 {% if compact %}w-56 py-4 pr-3{% else %}w-64 py-6 pr-4{% endif %}{% if showBorder %} border-r border-primary/30{% endif %}{% if class %} {{ class }}{% endif %}">
{% if title %}<h3 class="terminal-eyebrow terminal-mono {% if compact %}px-2 mb-3 text-xs uppercase tracking-widest text-accent{% else %}px-3 mb-4 text-sm uppercase tracking-widest text-accent{% endif %} font-semibold">{{ title }}</h3>{% endif %}
<ul class="{% if compact %}space-y-0.5{% else %}space-y-1{% endif %}">
{% for item in menu.items %}
<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link class="flex items-center rounded-md terminal-mono text-sm uppercase tracking-wide text-muted-foreground transition-colors hover:text-primary hover:bg-accent {% if compact %}px-2 py-1.5{% else %}px-3 py-2{% endif %} {{ item.css_class }}">{% if item.icon %}<svg class="mr-2 h-4 w-4" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg>{% endif %}{{ item.label }}</a></li>
{% endfor %}
</ul>
</nav>
{% if highlightCurrent %}<script>
(function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.classList.remove('text-muted-foreground');a.classList.add('text-foreground','bg-accent','font-medium');}});})();
</script>{% endif %}

View File

@ -0,0 +1,14 @@
{# terminal social-links — mono label, sharp neon-bordered chips; icon sprites preserved #}
<div class="social-links{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="mb-3 text-sm font-medium text-muted-foreground terminal-mono uppercase tracking-widest">{{ heading }}</p>{% endif %}
<ul class="flex {% if style == "stack" %}flex-col gap-2{% else %}flex-wrap items-center gap-3{% endif %}">
{% for link in links %}{% if link.url %}
<li>
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" aria-label="{{ link.label|default:link.network }}" class="inline-flex items-center gap-2 rounded-none border border-primary/40 text-foreground transition-colors hover:bg-accent hover:text-accent-foreground {% if style == "stack" %}w-full px-4 py-2{% else %}p-2.5{% endif %}">
{% if link.network %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ link.network|split:":"|first }}.svg#{{ link.network|split:":"|last }}"></use></svg>{% endif %}
{% if style == "stack" and link.label %}<span class="text-sm font-medium terminal-mono">{{ link.label }}</span>{% endif %}
</a>
</li>
{% endif %}{% endfor %}
</ul>
</div>

View File

@ -0,0 +1,50 @@
{# stats terminal override: scanline overlay, heading, HUD stat tiles, mono labels. data-stats + countUp script/attrs preserved byte-for-byte. #}
<section class="relative overflow-hidden py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}"{% if countUp %} data-stats{% endif %}>
<div aria-hidden="true" class="tty-scanlines pointer-events-none absolute inset-0"></div>
<div class="relative z-10 mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="terminal-heading text-center text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<dl class="mt-12 grid grid-cols-2 gap-8 {% if columns == 2 %}md:grid-cols-2{% elif columns == 3 %}md:grid-cols-3{% else %}md:grid-cols-4{% endif %}">
{% for item in items %}
<div class="tty-corners text-center">
<dd class="terminal-heading text-4xl font-bold tracking-tight md:text-5xl" style="font-family:var(--font-heading)">{% if item.prefix %}{{ item.prefix }}{% endif %}<span{% if countUp %} data-countup-value="{{ item.value }}"{% endif %}>{{ item.value }}</span>{% if item.suffix %}{{ item.suffix }}{% endif %}</dd>
{% if item.label %}<dt class="terminal-mono mt-2 text-sm font-medium uppercase tracking-widest text-muted-foreground">{{ item.label }}</dt>{% endif %}
</div>
{% endfor %}
</dl>
</div>
</section>
{% if countUp %}<script>
(function(){
var sections = document.querySelectorAll('[data-stats]');
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function animate(section){
var nodes = section.querySelectorAll('[data-countup-value]');
nodes.forEach(function(node){
var raw = node.getAttribute('data-countup-value') || '';
var target = parseFloat(raw.replace(/[^0-9.\-]/g, ''));
if (isNaN(target)) return;
var decimals = (raw.split('.')[1] || '').length;
var start = null, dur = 1200;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
node.textContent = (target * eased).toFixed(decimals);
if (p < 1) requestAnimationFrame(step);
else node.textContent = target.toFixed(decimals);
}
requestAnimationFrame(step);
});
}
sections.forEach(function(section){
if (reduce || !('IntersectionObserver' in window)) return;
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting){ animate(section); io.disconnect(); }
});
}, { threshold: 0.3 });
io.observe(section);
});
})();
</script>{% endif %}

View File

@ -0,0 +1,22 @@
{# team terminal override: heading, dashed-neon HUD member panels (lime), mono roles. {% img %} + social icon svgs + control flow preserved. #}
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="terminal-heading text-center text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-12 {% if layout == "list" %}mx-auto max-w-3xl space-y-6{% elif layout == "single" %}mx-auto max-w-xl{% else %}grid grid-cols-1 gap-8 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}{% endif %}">
{% for member in members %}
<div class="{% if layout == "list" %}tty-corners flex items-start gap-5 rounded-md border border-dashed border-primary/50 bg-card p-5 text-card-foreground tty-frame{% else %}text-center{% endif %}">
{% if member.photo %}<span class="{% if layout == "list" %}h-20 w-20{% else %}mx-auto h-28 w-28{% endif %} block shrink-0 overflow-hidden rounded-full bg-muted">{% img member.photo alt=member.name class="h-full w-full object-cover" %}</span>{% endif %}
<div class="{% if layout != "list" %}mt-4{% endif %}">
{% if member.name %}<h3 class="terminal-heading text-lg font-semibold">{{ member.name }}</h3>{% endif %}
{% if member.role %}<p class="terminal-mono uppercase tracking-wide text-sm font-medium text-primary">{{ member.role }}</p>{% endif %}
{% if member.bio %}<p class="mt-2 text-sm text-muted-foreground">{{ member.bio }}</p>{% endif %}
{% if member.socials %}<div class="mt-3 flex gap-3 {% if layout != "list" %}justify-center{% endif %}">
{% for social in member.socials %}{% if social.url %}<a href="{{ social.url }}" class="text-muted-foreground transition-colors hover:text-primary" aria-label="Social link"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ social.icon|split:":"|first }}.svg#{{ social.icon|split:":"|last }}"></use></svg></a>{% endif %}{% endfor %}
</div>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,27 @@
{# testimonial terminal override: heading, dashed-neon HUD quote cards. Rating svgs, {% img %}, and control flow preserved. #}
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="terminal-heading text-center text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
<div class="mt-12 {% if layout == "single" %}mx-auto max-w-3xl{% elif layout == "carousel" %}flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4{% else %}grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3{% endif %}">
{% for q in quotes %}
<figure class="tty-corners flex flex-col rounded-md border border-dashed border-primary/50 bg-card p-6 text-card-foreground tty-frame{% if layout == "carousel" %} min-w-[18rem] max-w-sm shrink-0 snap-start{% endif %}">
{% if q.rating %}<div class="mb-3 flex gap-0.5" aria-label="Rated {{ q.rating }} out of 5">
<svg class="h-4 w-4 {% if q.rating >= 1 %}text-primary{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 2 %}text-primary{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 3 %}text-primary{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 4 %}text-primary{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 5 %}text-primary{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
</div>{% endif %}
<blockquote class="flex-1 text-lg leading-relaxed">{% if q.quote %}&ldquo;{{ q.quote }}&rdquo;{% endif %}</blockquote>
<figcaption class="mt-5 flex items-center gap-3">
{% if q.avatar %}<span class="h-10 w-10 shrink-0 overflow-hidden rounded-full bg-muted">{% img q.avatar alt=q.name class="h-full w-full object-cover" %}</span>{% endif %}
<span>
{% if q.name %}<span class="terminal-heading block font-semibold">{{ q.name }}</span>{% endif %}
{% if q.role %}<span class="terminal-mono uppercase tracking-wide block text-sm text-muted-foreground">{{ q.role }}</span>{% endif %}
</span>
</figcaption>
</figure>
{% endfor %}
</div>
</div>
</section>

View File

@ -1 +1,2 @@
<div class="terminal-prose {{ class }}">{{ text|safe }}</div>
{# terminal text — body font via terminal-prose #}
<div class="prose prose-gray max-w-none flex-1 terminal-prose terminal-page {{ proseClass }}">{{ text|safe }}</div>

View File

@ -0,0 +1,22 @@
{# timeline terminal override: heading, mono step labels, neon markers. All field reads + control flow preserved. #}
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4">
{% if heading %}<h2 class="terminal-heading text-center text-3xl font-semibold tracking-tight md:text-4xl" data-text="{{ heading }}">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<ol class="mt-12 {% if orientation == "horizontal" %}grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4{% else %}relative space-y-8 before:absolute before:left-4 before:top-2 before:h-[calc(100%-1rem)] before:w-px before:bg-primary/50{% endif %}">
{% for step in steps %}
<li class="{% if orientation == "horizontal" %}text-center{% else %}relative pl-14{% endif %}">
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-primary text-sm font-semibold text-primary-foreground terminal-mono {% if orientation == "horizontal" %}mx-auto{% else %}absolute left-0 top-0 ring-4 ring-background{% endif %}">
{% if marker == "numbered" %}{{ forloop.counter }}{% elif marker == "dot" %}<span class="h-2 w-2 rounded-full bg-primary-foreground"></span>{% else %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#flag"></use></svg>{% endif %}
</span>
<div class="{% if orientation == "horizontal" %}mt-4{% else %}pt-1{% endif %}">
{% if step.label %}<p class="terminal-mono text-xs font-semibold uppercase tracking-widest text-accent">{{ step.label }}</p>{% endif %}
{% if step.title %}<h3 class="terminal-heading mt-1 text-lg font-semibold">{{ step.title }}</h3>{% endif %}
{% if step.text %}<p class="mt-1 text-muted-foreground">{{ step.text }}</p>{% endif %}
{% if step.media %}<div class="mt-3 overflow-hidden rounded-md border border-primary/40">{% img step.media alt=step.title class="w-full h-auto object-cover" %}</div>{% endif %}
</div>
</li>
{% endfor %}
</ol>
</div>
</section>

View File

@ -0,0 +1,10 @@
{# utility_bar (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 %}<a href="{{ it.link }}" class="inline-flex items-center gap-1.5 hover:opacity-80 transition-opacity">{% if it.icon %}::{{ it.icon }}::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</a>{% else %}<span class="inline-flex items-center gap-1.5">{% if it.icon %}::{{ it.icon }}::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</span>{% endif %}{% endmacro %}
<div class="terminal-mono uppercase tracking-wide w-full text-xs {% if not showOnMobile %}hidden sm:block{% endif %} {{ ubcls }} {{ class }}">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-9 flex items-center justify-between gap-4">
<div class="flex items-center gap-4 flex-wrap">{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}</div>
<div class="flex items-center gap-4 flex-wrap justify-end">{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}</div>
</div>
</div>

View File

@ -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" %}<figure class="bn-video my-6{% if class %} {{ class }}{% endif %}"><div class="bn-video-frame tty-corners relative w-full overflow-hidden rounded-md border border-primary/40 bg-muted tty-frame {% if aspect == "4:3" %}aspect-[4/3]{% elif aspect == "1:1" %}aspect-square{% elif aspect == "21:9" %}aspect-[21/9]{% else %}aspect-video{% endif %}" data-bn-video data-video-url="{{ url|default:"" }}" data-video-media="{% if media %}{{ media|mediaURL }}{% endif %}"><button type="button" data-bn-video-play class="group absolute inset-0 flex h-full w-full items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" aria-label="Play video{% if title %}: {{ title }}{% endif %}">{% if poster %}{% img poster alt=title|default:"" class="absolute inset-0 h-full w-full object-cover" %}{% endif %}<span class="absolute inset-0 bg-primary/10 transition-colors group-hover:bg-primary/20" aria-hidden="true"></span><span class="relative inline-flex h-16 w-16 items-center justify-center rounded-md border border-primary/60 bg-background/90 text-primary shadow-lg transition-transform group-hover:scale-105 motion-reduce:transform-none">::lucide:play:xl::</span></button></div>{% if caption %}<figcaption class="terminal-mono mt-2 text-center text-sm text-muted-foreground">{{ caption }}</figcaption>{% endif %}</figure><script>
(function(){
if(window.__bnVideoFacade)return;window.__bnVideoFacade=true;
function yt(u){var m=u.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{6,})/);return m?m[1]:null;}
function vim(u){var m=u.match(/vimeo\.com\/(?:video\/)?(\d+)/);return m?m[1]:null;}
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-video-play]');if(!btn)return;
var frame=btn.closest('[data-bn-video]');if(!frame)return;
e.preventDefault();
var url=frame.getAttribute('data-video-url')||'',media=frame.getAttribute('data-video-media')||'',node;
var vstyle='position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:rgba(0,0,0,1);';
if(media){node=document.createElement('video');node.src=media;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
else{var y=yt(url),v=vim(url),embed;
if(y)embed='https://www.youtube-nocookie.com/embed/'+y+'?autoplay=1&rel=0';
else if(v)embed='https://player.vimeo.com/video/'+v+'?autoplay=1';
if(embed){node=document.createElement('iframe');node.src=embed;node.allow='autoplay; fullscreen; picture-in-picture';node.setAttribute('allowfullscreen','');node.setAttribute('title',btn.getAttribute('aria-label')||'Video');node.style.cssText='position:absolute;inset:0;width:100%;height:100%;border:0;';}
else if(url){node=document.createElement('video');node.src=url;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
}
if(node){btn.remove();frame.appendChild(node);}
});
})();
</script>{% endwith %}

View File

@ -1,22 +1,23 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full">
<div class="terminal-main-80 px-4">{{ slots.header|safe }}</div>
</header>
<div class="terminal-main-80 w-full px-4 py-6 flex-grow">
<div aria-hidden="true" class="tty-scanlines pointer-events-none fixed inset-0 z-50"></div>
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full px-4 py-10">
<div class="mx-auto w-full max-w-5xl">
{% if slots.aside %}
<div class="terminal-article-grid">
<aside class="w-full">{{ slots.toc|safe }}</aside>
<main class="w-full">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono">// no content blocks assigned to this page</p>{% endif %}
<aside class="w-full">{{ slots.aside|safe }}</aside>
<article class="w-full terminal-prose">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono text-muted-foreground">// article body</p>{% endif %}</article>
</div>
{% else %}
<article class="mx-auto w-full max-w-3xl terminal-prose">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono text-muted-foreground">// article body</p>{% endif %}</article>
{% endif %}
</div>
</main>
</div>
</div>
<footer class="w-full mt-auto">
<div class="terminal-main-80 px-4">{{ slots.footer|safe }}</div>
</footer>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<div aria-hidden="true" class="tty-scanlines pointer-events-none fixed inset-0 z-50"></div>
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full flex items-center justify-center px-4 py-16">
<div class="w-full max-w-md">
<div class="tty-window">
<div class="tty-titlebar"><span class="tty-dot"></span><span class="tty-dot"></span><span class="tty-dot"></span><span class="ml-1 terminal-mono">login</span></div>
<div class="p-6 md:p-8">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono text-center text-muted-foreground">// auth console</p>{% endif %}
</div>
</div>
</div>
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<div aria-hidden="true" class="tty-scanlines pointer-events-none fixed inset-0 z-50"></div>
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full px-4 py-10">
<div class="mx-auto w-full max-w-5xl">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="terminal-mono text-muted-foreground">// no posts yet</p></div>{% endif %}
</div>
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<div aria-hidden="true" class="tty-scanlines pointer-events-none fixed inset-0 z-50"></div>
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full px-4 py-12">
<div class="mx-auto w-full max-w-3xl space-y-8">
<div class="tty-window">
<div class="tty-titlebar"><span class="tty-dot"></span><span class="tty-dot"></span><span class="tty-dot"></span><span class="ml-1 terminal-mono">contact --send</span></div>
<div class="p-6 md:p-8">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono text-muted-foreground">// add a contact-form block</p>{% endif %}
</div>
</div>
</div>
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,17 +1,21 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full">
<div class="terminal-main-80 px-4">{{ slots.header|safe }}</div>
</header>
<main class="terminal-main-80 w-full px-4 py-8 flex-grow">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono">// no content blocks assigned to this page</p>{% endif %}
<div aria-hidden="true" class="tty-scanlines pointer-events-none fixed inset-0 z-50"></div>
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full px-4 py-10">
<div class="terminal-main-80">
<div class="tty-window">
<div class="tty-titlebar"><span class="tty-dot"></span><span class="tty-dot"></span><span class="tty-dot"></span><span class="ml-1 terminal-mono">man page</span></div>
<div class="terminal-prose p-6 md:p-8">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono text-muted-foreground">// no content blocks assigned to this page</p>{% endif %}
</div>
</div>
</div>
</main>
<footer class="w-full mt-auto">
<div class="terminal-main-80 px-4">{{ slots.footer|safe }}</div>
</footer>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,13 +1,14 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full px-4">{{ slots.header|safe }}</header>
<main class="flex-grow w-full px-4 py-6">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="terminal-mono">// no content blocks assigned to this page</p>{% endif %}
<div aria-hidden="true" class="tty-scanlines pointer-events-none fixed inset-0 z-50"></div>
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="px-4 py-16 text-center"><p class="terminal-mono text-muted-foreground">// no content blocks assigned to this page</p></div>{% endif %}
</main>
<footer class="w-full mt-auto px-4">{{ slots.footer|safe }}</footer>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,20 +1,14 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="terminal-page bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<section class="w-full">
<div class="terminal-main-80 px-4 py-8">{{ slots.hero|safe }}</div>
</section>
<div aria-hidden="true" class="tty-scanlines pointer-events-none fixed inset-0 z-50"></div>
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">
{% if slots.main %}<div class="terminal-main-80 px-4 py-8">{{ slots.main|safe }}</div>{% endif %}
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="px-4 py-16 text-center"><p class="terminal-mono text-muted-foreground">// add a hero block to start the boot sequence</p></div>{% endif %}
</main>
<section class="w-full">
<div class="terminal-main-80 px-4 py-6">{{ slots.cta|safe }}</div>
</section>
<footer class="w-full mt-auto">
<div class="terminal-main-80 px-4">{{ slots.footer|safe }}</div>
</footer>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>