Compare commits

...

24 Commits
v0.2.8 ... main

Author SHA1 Message Date
Alex Dunmow
4cc9f10bd1 bump to 0.5.2 2026-07-06 20:01:55 +08:00
Alex Dunmow
ff71b93717 feat(lcars): reshoot with Antonio/Share Tech Mono bundled faces live (dark-first preview)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:01:55 +08:00
Alex Dunmow
e3747c2cd8 bump to 0.5.1 2026-07-06 19:57:15 +08:00
Alex Dunmow
73a139e032 fix(lcars): bundle Antonio + Share Tech Mono via @font-face (host bundled_fonts inert)
- host does not consume bundled_fonts; declare @font-face against served
  /templates/lcars/fonts/*.woff2 paths so --font-* resolve to real faces
- all 5 presets set typography.fontHeading/body/mono to template:lcars:<Family>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:57:15 +08:00
Alex Dunmow
7c7f249fa4 bump to 0.5.0 2026-07-06 19:49:33 +08:00
Alex Dunmow
8419d717e3 feat(lcars): Wave B gallery reshoot (dark-first) — real hero art, styled empty-media fallback, longer log
Home/about/article x light+dark at 2880x1800. Dark home is the curated preview.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:49:33 +08:00
Alex Dunmow
4e54f71279 bump to 0.4.2 2026-07-06 19:47:39 +08:00
Alex Dunmow
8c1377d91b fix(lcars): paragraph rhythm in long-form text/prose bodies
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:47:39 +08:00
Alex Dunmow
f4cddccb3c bump to 0.4.1 2026-07-06 19:35:31 +08:00
Alex Dunmow
d2d27b8dd9 fix(lcars): seed hero+cover art, styled empty-media fallback, SVG geometry guard + shortcode sweep, longer episode log
- seed: wire 6 licensed cosmic images (hero split art, episode cover, episode-card + about slots); kills the empty grey hero/cover boxes with real art
- theme fix: rich-section + episode_console render a labeled LCARS console-art readout for unset image slots instead of a bare grey rectangle
- dead space: home hero -> split layout (image fills post-CTA void); lengthen first-contact episode log to ~11 paragraphs (starfleet-log voice), warp/terraforming to 5
- SVG geometry guard: explicit width/height on all <use> icon SVGs; full shortcode sweep (::pack:name:size:: -> sized <svg><use>) across every override

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:35:15 +08:00
Alex Dunmow
59bb1f453a bump to 0.4.0 2026-07-06 09:32:23 +08:00
Alex Dunmow
a5546afae5 feat: Wave B gallery screenshots (home/about/article × light+dark) 2026-07-06 09:32:07 +08:00
Alex Dunmow
ee48e06931 chore: rename dev host localdev.blockninjacms.com -> blockninja.dev 2026-07-05 20:50:30 +08:00
Alex Dunmow
d919a967cf fix(lcars): remediate hero blank, empty-media balloon, CTA void, band padding
Layout/whitespace remediation (no redesign; LCARS persona intact):
- hero: styled-text reads used `field.text|default:field`, which HARD-ERRORS
  in ninjatpl when the field is a bare string (resolveIdentifier errors on
  reflect.String, and nodeFilteredVariable returns before the default filter
  runs). The demo seed passes bare strings for eyebrow/headline/subheadline,
  so the hero aborted on its first {% set %} and rendered nothing — a dead
  band on the home page. Switch to `field|get:"text"|default:field` (the
  global jutsu `get` filter tolerates non-maps), which resolves both the
  {text,color} object and bare-string forms. Same defensive fix in navbar
  logoText.
- hero split: guard the empty media frame so an image-less split hero drops to
  a single column instead of leaving a blank muted panel.
- episode_console: empty cover rendered a full 1:1 square that ballooned to
  ~600px and, with align-items:stretch, stretched the text column to match,
  leaving a large void beside the copy. Switch to align-items:start and render
  a compact, capped, on-brand LCARS readout placeholder (.lcars-console-art)
  when no cover ships.
- cta: band variant left a large empty region right of the button; use
  justify-content:space-between and trim the oversized 4rem band padding to 3rem.
- manifest css: add .lcars-console-art (explicit CSS, semantic tokens) + cap
  .lcars-img-empty height.

Mobile responsiveness audited from CSS/templates: all grids/rails collapse via
explicit @media rules in manifest css.input_css_append (not Tailwind JIT-scanned
theme classes); no horizontal overflow at 375-390px. Content is never gated by
IntersectionObserver (canvas IO drives only the decorative hero background;
.lcars-reveal is a load-time CSS animation with a reduced-motion opacity:1
fallback), so no reveal-hidden dead bands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:26:12 +08:00
Alex Dunmow
800c8904d6 fix(lcars): collapse multi-line ninjatpl comments to single-line (load-fatal lexer error) 2026-07-05 17:06:50 +08:00
Alex Dunmow
ba23c3c453 bump to 0.3.2 2026-07-05 16:51:02 +08:00
Alex Dunmow
40348809a8 bump to 0.3.1 2026-07-05 16:44:48 +08:00
Alex Dunmow
b4891b95cf docs: publishing README 2026-07-05 13:43:43 +08:00
Alex Dunmow
885dd372e7 feat(lcars): Wave A theme rebuild — sci-fi podcast persona (WO-TF-020)
Full LCARS library-computer interface for a sci-fi podcast/media outfit.
Codeless .bnp. Re-skins every mandatory builtin inside LCARS framing and
ships the full Wave A surface.

- 49 mandatory template overrides (chrome, marketing, primitives, commerce,
  blog family, auth, page-suggestions), field-faithful re-skins of the frozen
  builtin defaults; identity carried by the lcars-* class system.
- 8 page templates (default+sidebar rail, full-width, full-display, landing,
  article, blog-index, contact, auth) with elbow-frame chrome.
- 4 persona blocks: episode_console, crew_roster, stardate_log,
  transmission_status. Legacy lcars_header/sidebar/panel removed (now overrides
  + page chrome).
- 5 dual-mode presets (canonical black + daylight-ops cream), all 19 tokens.
- Bundled Antonio + Share Tech Mono (OFL); email-safe LCARS wrapper.
- Seed "Subspace Signal": home, about, 3 episodes+articles, contact
  (hail_signals table + row_inserted email-admins workflow), login.
- GO BIG motion: dependency-free canvas panel-sweep on the hero (lazy-init,
  reduced-motion static frame, no-JS fallback).

Build + check-safety green. Visual UNVERIFIED until Wave B. Known frozen-cms
limits (contract §11 auth/404 non-dispatch, §12 provider-backed empty lists)
authored faithfully.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:22:22 +08:00
Alex Dunmow
6dbd76f2e8 feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:00:19 +08:00
Alex Dunmow
9fa99a8d40 chore: repo hygiene for the wasm era (WO-WZ-015 fix round)
Add the wasm/.bnp convention Makefile (build / verify / archive-check /
publish via ninja plugin build) for consistency with the theme fleet,
gitignore *.so and *.bnp artifacts, and refresh the stale
[compatibility] block_core constraint to the v0.15 era.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:15:18 +08:00
Alex Dunmow
36a4b97bb7 wasm port (WO-WZ-015): reactor-mode wasip1 guest, core v0.15.2
Add wasmguest.Serve(Registration) boilerplate and bump block/core to v0.15.2
for the .so->wasm migration. Built + published as .bnp to the dev registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:11:12 +08:00
Alex Dunmow
abe0b25e9d bump to 0.2.9 2026-07-03 22:10:05 +08:00
Alex Dunmow
25ac7dbde0 chore: ignore local Playwright/MCP test artifacts
Keeps stray .playwright-mcp/ runs and one-off debug screenshots out of
the working tree so the repo stays clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:20:14 +08:00
112 changed files with 1951 additions and 782 deletions

5
.gitignore vendored
View File

@ -1,2 +1,7 @@
*.so
.superpowers/
# local Playwright/MCP test + debug artifacts
.playwright-mcp/
login-page.png
*.bnp

55
Makefile Normal file
View File

@ -0,0 +1,55 @@
# lcars — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
#
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
# classifies the repo as codeless (no Go at the root) and packs the artifact
# with manifest.pb (codeless: true) and NO plugin.wasm.
#
# Usage:
# make build # pack lcars-<version>.bnp (codeless — no wasm)
# make verify # validate the built .bnp (loader-identical checks)
# make archive-check # prove a clean `git archive HEAD` still builds codeless
# make publish # ninja plugin publish --bnp (to the active registry)
# make clean # remove build artefacts
.PHONY: build verify archive-check publish clean help
PLUGIN_NAME := lcars
NINJA := ninja
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
build:
$(NINJA) plugin build --dir .
# Validate the built .bnp exactly as the registry/loader will.
verify:
$(NINJA) plugin verify $(BNP)
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
archive-check:
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
trap 'rm -rf "$$T"' EXIT && \
git archive HEAD | tar -x -C "$$T" && \
cd "$$T" && $(NINJA) plugin build --dir . && \
echo "archive-check OK"
# Publish the prebuilt .bnp to the active registry.
# Pass --host https://my.blockninja.dev for the dev orchestrator.
publish: build
$(NINJA) plugin publish --bnp $(BNP)
# Remove build artefacts.
clean:
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
help:
@echo "Targets:"
@echo " build Pack the codeless .bnp (no wasm)"
@echo " verify Validate the built .bnp (loader-identical checks)"
@echo " archive-check Prove a clean git archive HEAD packs codeless"
@echo " publish Publish the .bnp to the active registry"
@echo " clean Remove build artefacts"

25
README.md Normal file
View File

@ -0,0 +1,25 @@
# LCARS
A retro-futurist theme built as a full library-computer interface. Angular pill rails, numeric readouts, and stardate furniture frame every surface, so the chrome itself becomes the layout. It reads like the console of a 24th-century starship. Amber and lilac panels sit over true black in dark mode.
## Good for
We built LCARS for sci-fi media and fandom. Treat these as starting points, not limits. If the mood fits your brand, use it:
- Sci-fi podcasts and audio shows
- Fan sites and episode archives
- Tech and gaming communities
- Convention and event pages
## What you get
- Five color presets, each with a tuned light mode and a tuned dark mode: Ops Amber, Red Alert, Blue Console, Nebula, and Medbay Green.
- Two bundled fonts, ready to assign: Antonio for condensed panel headings and Share Tech Mono for readouts and labels. Fonts stay admin-controlled, so you can swap them from the typography settings.
- A GO BIG hero with an animated canvas panel-sweep. It honors reduced-motion settings and draws a single static frame when scripts are off.
- Four persona blocks: an episode console, a crew roster, a stardate log, and a transmission-status readout.
- A demo site that installs when you activate the theme: a home page, an about page, an episode feed with three articles, and a contact page whose form emails your admins.
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
## Install
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully dressed starting point.

39
RECOMMENDED_FONTS.md Normal file
View File

@ -0,0 +1,39 @@
# LCARS — recommended fonts
This theme **bundles** two libre families (OFL-1.1) so it renders on-brand with
no admin setup:
| Slot | Bundled family | Role |
|------|----------------|------|
| Heading | **Antonio** | Tall condensed grotesque for LCARS labels, headings, buttons, and nav pills. |
| Body | **Antonio** | Same family for panel copy; condensed and legible at reading sizes. |
| Mono | **Share Tech Mono** | Numeric readouts, stardate stamps, statistics, prices, metadata. Tabular by design. |
Bundled files live in `assets/fonts/` (latin subsets); licenses in
`assets/fonts/LICENSES.md`. The theme CSS resolves every family through
`var(--font-heading | --font-body | --font-mono, <fallback>)`, and the fallback
stacks name the bundled families, so the identity holds even before an admin
touches the typography panel.
## Optional upgrade — Roboto Condensed for body (Google Fonts tab)
The locked direction pairs Antonio labels with **Roboto Condensed** (Apache-2.0)
for longer readout copy. Roboto Condensed is not bundled here (no libre woff2 was
available in the workspace at build time), so the body fallback stack degrades to
Antonio then a system condensed sans (`"Arial Narrow", system-ui`). To match the
locked direction exactly, assign Roboto Condensed to the **Body** slot from the
Google Fonts tab:
| Slot | Source | Family | Why |
|------|--------|--------|-----|
| Body | `google:Roboto Condensed` | Roboto Condensed | The locked body face for readout copy. Wider aperture than Antonio at small sizes. |
| Mono | `google:Share Tech Mono` | Share Tech Mono | Already bundled; also available in the picker if you prefer the Google source. |
## How to apply an override
1. Open **Admin → Theme → Typography**.
2. Switch to the **Google Fonts** tab.
3. Search and pick the family.
4. Assign it to the matching slot (Heading / Body / Mono).
5. Save. The pick is stored as `google:<Family>` and binds the `--font-*`
variable, overriding the bundled fallback.

15
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,15 @@
# LCARS — required icon packs
The theme renders icons via the sprite system only
(`<svg><use href="/icons/<pack>.svg#<name>"/></svg>` and the `::pack:name::`
shorthand). It declares its packs in `manifest.yaml` `required_icon_packs`.
Until the loader auto-installs declared packs, install them by hand before
activation.
| Pack | Used for | Install |
|------|----------|---------|
| **lucide** | All UI accents: arrows, chevrons, checks, contact channels, nav toggle, play controls, mail/phone marks. Bundled with the CMS. | Ships with the CMS. Verify it is enabled at `/admin/icons`. |
lucide ships with BlockNinja, so no npm download is needed. If a block picks an
icon from a different pack, install that pack at `/admin/icons` first (`lucide`
is the safe default fallback for any icon-picker field).

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

@ -0,0 +1,21 @@
# Bundled font licenses
Both bundled families are libre and redistributable. Latin subsets only.
## Antonio
- Files: `antonio-latin.woff2`, `antonio-latin-ext.woff2` (variable weight axis; theme uses 300/400/600/700)
- Family: Antonio (Vernon Adams / Marcelo Magalhaes)
- License: SIL Open Font License 1.1 (OFL-1.1)
- Source: https://fonts.google.com/specimen/Antonio — latin and latin-ext subsets as distributed by google-webfonts-helper.
- Role: LCARS labels, headings, buttons, nav pills. A tall condensed grotesque that reads as library-computer type.
## Share Tech Mono
- File: `share-tech-mono-latin.woff2` (weight 400)
- Family: Share Tech Mono (Carrois Apostrophe)
- License: SIL Open Font License 1.1 (OFL-1.1)
- Source: https://fonts.google.com/specimen/Share+Tech+Mono — latin subset as distributed by google-webfonts-helper.
- Role: numeric readouts, stardate stamps, metadata, statistics, prices. Tabular by design.
The full OFL-1.1 text applies to both. See https://openfontlicense.org.

Binary file not shown.

View File

@ -1,53 +0,0 @@
package main
import (
"git.dev.alexdunmow.com/block/core/blocks"
)
// --- LCARS Header Block ---
var LCARSHeaderMeta = blocks.BlockMeta{
Key: "lcars_header",
Title: "LCARS Header",
Description: "Top bar with elbow bracket, status indicators, and stardate",
Category: blocks.CategoryNavigation,
Source: "lcars",
}
var headerDefaults = map[string]any{
"title": "LCARS",
"subtitle": "Library Computer Access/Retrieval System",
"stardate": "",
"status": "online",
"menu_name": "main",
}
// --- LCARS Sidebar Block ---
var LCARSSidebarMeta = blocks.BlockMeta{
Key: "lcars_sidebar",
Title: "LCARS Sidebar",
Description: "Left panel with rounded-rectangle navigation buttons",
Category: blocks.CategoryNavigation,
Source: "lcars",
}
var sidebarDefaults = map[string]any{
"items": []any{
map[string]any{"label": "Personnel", "url": "#", "color": "primary"},
map[string]any{"label": "Operations", "url": "#", "color": "secondary"},
map[string]any{"label": "Sciences", "url": "#", "color": "accent"},
map[string]any{"label": "Engineering", "url": "#", "color": "primary"},
map[string]any{"label": "Medical", "url": "#", "color": "secondary"},
},
}
// --- LCARS Panel Block ---
var LCARSPanelMeta = blocks.BlockMeta{
Key: "lcars_panel",
Title: "LCARS Panel",
Description: "Framed content area with LCARS border treatment",
Category: blocks.CategoryLayout,
Source: "lcars",
}

36
blocks/blocks.yaml Normal file
View File

@ -0,0 +1,36 @@
# LCARS theme block definitions (codeless .bnp, WO-TF-020).
# Keys are fully qualified (lcars:<key>) because the codeless loader registers
# definitions verbatim — there is no PluginBlockRegistry prefixing in the
# codeless path. The qualified keys match the data-block attributes and the
# master_pages.json / seed references.
#
# Only true persona furniture stays a theme block. Everything that duplicated a
# builtin (the old lcars_header navbar, lcars_panel card, lcars_sidebar rail) is
# now a template override of the builtin key or page-template chrome instead.
# All four blocks below are content-map-only (no data providers), so they render
# fully under the frozen codeless loader.
blocks:
- key: lcars:episode_console
title: Episode Console
description: Featured podcast episode as an LCARS playback console with duration readout, stardate, and platform channels.
category: content
schema: episode_console.schema.json
template: episode_console.ninjatpl
- key: lcars:crew_roster
title: Crew Roster
description: Roster of hosts and crew as LCARS ID panels with role and callsign.
category: content
schema: crew_roster.schema.json
template: crew_roster.ninjatpl
- key: lcars:stardate_log
title: Stardate Log
description: Dated log entries on a numeric stardate rail — a captain's-log timeline.
category: content
schema: stardate_log.schema.json
template: stardate_log.ninjatpl
- key: lcars:transmission_status
title: Transmission Status
description: Status board of channels and systems as LCARS readouts with online, standby, or alert states.
category: content
schema: transmission_status.schema.json
template: transmission_status.ninjatpl

View File

@ -0,0 +1,11 @@
<section class="lcars-section {{ class }}" data-block="lcars:crew_roster"><div class="lcars-well">
{% if eyebrow %}<p class="lcars-eyebrow">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="lcars-heading lcars-heading-2" style="margin-bottom:1.75rem;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if members %}<div class="lcars-leadership-grid">{% for m in members %}
<article class="lcars-card" style="padding:1.25rem;">
<div style="display:flex;align-items:center;gap:0.875rem;">{% if m.avatar %}<span class="lcars-avatar" style="width:3.25rem;height:3.25rem;flex:none;overflow:hidden;display:inline-block;">{% img m.avatar alt=m.name|default:"Crew" %}</span>{% else %}<span class="lcars-pill" style="width:3.25rem;height:3.25rem;border-radius:0 1.625rem 1.625rem 0;flex:none;"></span>{% endif %}<div style="min-width:0;">{% if m.name %}<div class="lcars-panel-title" style="font-size:1.0625rem;letter-spacing:0.08em;">{{ m.name }}</div>{% endif %}{% if m.role %}<div class="lcars-stat-label" style="margin-top:0.125rem;">{{ m.role }}</div>{% endif %}</div></div>
{% if m.callsign %}<div class="lcars-readout" style="margin-top:0.75rem;font-size:0.8125rem;">Callsign {{ m.callsign }}</div>{% endif %}
{% if m.bio %}<p class="lcars-text" style="font-size:0.9375rem;color:hsl(var(--muted-foreground));margin-top:0.5rem;">{{ m.bio }}</p>{% endif %}
</article>
{% endfor %}</div>{% endif %}
</div></section>

View File

@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Crew Roster",
"description": "Hosts and crew as LCARS ID panels.",
"type": "object",
"properties": {
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text" },
"heading": { "type": "string", "title": "Heading", "x-editor": "text" },
"members": {
"type": "array",
"title": "Crew",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"name": { "type": "string", "title": "Name", "x-editor": "text" },
"role": { "type": "string", "title": "Role", "x-editor": "text" },
"callsign": { "type": "string", "title": "Callsign", "x-editor": "text" },
"bio": { "type": "string", "title": "Bio", "x-editor": "textarea" },
"avatar": { "type": "string", "title": "Portrait", "x-editor": "media" }
}
}
}
}
}

View File

@ -0,0 +1,14 @@
<section class="lcars-section {{ class }}" data-block="lcars:episode_console"><div class="lcars-well">
<div class="lcars-panel lcars-panel-accent" style="border-radius:0 0.75rem 0.75rem 0;padding:1.5rem;">
<div class="lcars-hero-split" style="gap:1.75rem;align-items:start;">
{% if cover %}<div class="lcars-media-frame" style="aspect-ratio:1 / 1;max-height:22rem;align-self:start;">{% img cover alt=title|default:"Episode cover" %}</div>{% else %}<div class="lcars-console-art"><span class="lcars-console-art-label">No cover art</span><span class="lcars-console-art-scope" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span></div>{% endif %}
<div class="lcars-stack-sm" style="min-width:0;">
<div class="lcars-panel-header"><span class="lcars-pill lcars-pill-accent" aria-hidden="true"></span>{% if eyebrow %}<span class="lcars-panel-title">{{ eyebrow }}</span>{% else %}<span class="lcars-panel-title">Now Broadcasting</span>{% endif %}<span class="lcars-panel-header-bar"></span></div>
<div style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;">{% if episodeNumber %}<span class="lcars-badge">EP {{ episodeNumber }}</span>{% endif %}{% if stardate %}<span class="lcars-stardate">Stardate {{ stardate }}</span>{% endif %}{% if duration %}<span class="lcars-readout">{{ duration }}</span>{% endif %}</div>
{% if title %}<h2 class="lcars-heading lcars-heading-2">{{ title }}</h2>{% endif %}
{% if description %}<p class="lcars-text" style="color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
<div class="lcars-btn-row" style="margin-top:0.25rem;">{% if listenUrl %}<a class="lcars-btn-primary" href="{{ listenUrl }}"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#play"/></svg>{{ listenLabel|default:"Listen now" }}</a>{% endif %}{% for p in platforms %}<a class="lcars-btn-outline lcars-btn-sm" href="{{ p.url|default:'#' }}">{{ p.label }}</a>{% endfor %}</div>
</div>
</div>
</div>
</div></section>

View File

@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Episode Console",
"description": "Featured episode as an LCARS playback console.",
"type": "object",
"properties": {
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text" },
"title": { "type": "string", "title": "Episode title", "x-editor": "text" },
"episodeNumber": { "type": "string", "title": "Episode number", "description": "e.g. \"047\"", "x-editor": "text" },
"duration": { "type": "string", "title": "Duration readout", "description": "e.g. \"58:12\"", "x-editor": "text" },
"stardate": { "type": "string", "title": "Stardate", "description": "e.g. \"78412.3\"", "x-editor": "text" },
"description": { "type": "string", "title": "Description", "x-editor": "textarea" },
"cover": { "type": "string", "title": "Cover art", "x-editor": "media" },
"listenUrl": { "type": "string", "title": "Listen link", "x-editor": "text" },
"listenLabel": { "type": "string", "title": "Listen label", "x-editor": "text" },
"platforms": {
"type": "array",
"title": "Platform channels",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": { "type": "string", "title": "Label", "x-editor": "text" },
"url": { "type": "string", "title": "URL", "x-editor": "text" }
}
}
}
}
}

View File

@ -0,0 +1,10 @@
<section class="lcars-section {{ class }}" data-block="lcars:stardate_log"><div class="lcars-well">
{% if eyebrow %}<p class="lcars-eyebrow">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="lcars-heading lcars-heading-2" style="margin-bottom:1.75rem;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if entries %}<div class="lcars-stack">{% for e in entries %}
<div class="lcars-timeline-step" style="padding-bottom:1.25rem;">
<div class="lcars-timeline-rail"><span class="lcars-timeline-node" style="width:auto;min-width:4.5rem;padding:0 0.75rem;font-size:0.8125rem;">{{ e.stardate|default:"—" }}</span>{% if not forloop.Last %}<span class="lcars-timeline-line"></span>{% endif %}</div>
<div style="min-width:0;">{% if e.title %}<div class="lcars-panel-title" style="font-size:1.0625rem;">{{ e.title }}</div>{% endif %}{% if e.text %}<p class="lcars-text" style="font-size:0.9375rem;color:hsl(var(--muted-foreground));margin-top:0.375rem;">{{ e.text }}</p>{% endif %}</div>
</div>
{% endfor %}</div>{% endif %}
</div></section>

View File

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Stardate Log",
"description": "Dated log entries on a numeric stardate rail.",
"type": "object",
"properties": {
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text" },
"heading": { "type": "string", "title": "Heading", "x-editor": "text" },
"entries": {
"type": "array",
"title": "Log entries",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"stardate": { "type": "string", "title": "Stardate", "x-editor": "text" },
"title": { "type": "string", "title": "Entry title", "x-editor": "text" },
"text": { "type": "string", "title": "Entry", "x-editor": "textarea" }
}
}
}
}
}

View File

@ -0,0 +1,10 @@
<section class="lcars-section-tight {{ class }}" data-block="lcars:transmission_status"><div class="lcars-well">
{% if eyebrow %}<p class="lcars-eyebrow">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="lcars-heading lcars-heading-3" style="margin-bottom:1.25rem;">{{ heading }}</h2>{% endif %}
{% if systems %}<div class="lcars-grid lcars-grid-2">{% for s in systems %}
<div class="lcars-panel" style="padding:0.875rem 1.25rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;">
<div style="min-width:0;">{% if s.label %}<div class="lcars-label">{{ s.label }}</div>{% endif %}{% if s.detail %}<div class="lcars-readout" style="font-size:0.75rem;margin-top:0.25rem;">{{ s.detail }}</div>{% endif %}</div>
{% if s.status == "alert" %}<span class="lcars-status lcars-status-alert">Alert</span>{% elif s.status == "standby" %}<span class="lcars-status lcars-status-standby">Standby</span>{% elif s.status == "offline" %}<span class="lcars-status lcars-status-offline">Offline</span>{% else %}<span class="lcars-status lcars-status-online">Online</span>{% endif %}
</div>
{% endfor %}</div>{% endif %}
</div></section>

View File

@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Transmission Status",
"description": "Status board of channels and systems as LCARS readouts.",
"type": "object",
"properties": {
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text" },
"heading": { "type": "string", "title": "Heading", "x-editor": "text" },
"systems": {
"type": "array",
"title": "Systems",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": { "type": "string", "title": "Label", "x-editor": "text" },
"status": {
"type": "string",
"title": "Status",
"x-editor": "select",
"enum": ["online", "standby", "alert", "offline"],
"default": "online"
},
"detail": { "type": "string", "title": "Detail readout", "x-editor": "text" }
}
}
}
}
}

View File

@ -1,60 +0,0 @@
package main
import (
"embed"
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/plugin"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed templates/*
var templateFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
func SchemasFS() fs.FS {
sub, _ := fs.Sub(schemasFS, "schemas")
return sub
}
func TemplatesFS() fs.FS {
sub, _ := fs.Sub(templateFS, "templates")
return sub
}
func AssetsHandler() http.Handler {
return http.FileServer(http.FS(Assets()))
}
func ThemePresets() []byte { return presetsData }
func BundledFonts() []byte { return fontsData }
func ThemeCSSManifest() *plugin.CSSManifest {
css, err := assetsFS.ReadFile("assets/style.css")
if err != nil {
return &plugin.CSSManifest{}
}
return &plugin.CSSManifest{
InputCSSAppend: string(css),
}
}

View File

@ -3,26 +3,17 @@
"name": "Antonio",
"family": "Antonio",
"variants": [
{
"weight": "300",
"style": "normal",
"file": "fonts/antonio-latin.woff2"
},
{
"weight": "400",
"style": "normal",
"file": "fonts/antonio-latin.woff2"
},
{
"weight": "600",
"style": "normal",
"file": "fonts/antonio-latin.woff2"
},
{
"weight": "700",
"style": "normal",
"file": "fonts/antonio-latin.woff2"
}
{ "weight": "300", "style": "normal", "file": "fonts/antonio-latin.woff2" },
{ "weight": "400", "style": "normal", "file": "fonts/antonio-latin.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/antonio-latin.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/antonio-latin.woff2" }
]
},
{
"name": "Share Tech Mono",
"family": "Share Tech Mono",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/share-tech-mono-latin.woff2" }
]
}
]

19
go.mod
View File

@ -1,19 +0,0 @@
module git.dev.alexdunmow.com/block/lcars
go 1.26.4
require git.dev.alexdunmow.com/block/core v0.14.1
require (
connectrpc.com/connect v1.20.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/a-h/templ v0.3.1020 // indirect
github.com/flosch/pongo2/v6 v6.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

54
go.sum
View File

@ -1,54 +0,0 @@
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
git.dev.alexdunmow.com/block/core v0.14.0 h1:cO6QQQPndhxwh1zbDvDOzebwrFS8Ka3WsxD8JBJLw/Y=
git.dev.alexdunmow.com/block/core v0.14.0/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
git.dev.alexdunmow.com/block/core v0.14.1 h1:63b25yzWoqrhBd3wdDFhsJzyGNM99wYmdh4WkN35hDk=
git.dev.alexdunmow.com/block/core v0.14.1/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/flosch/pongo2/v6 v6.1.0 h1:A/NJbrQJJD2B2mbpw3DRFwBYG0xpCr3vwFlEr46y1HQ=
github.com/flosch/pongo2/v6 v6.1.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

484
manifest.yaml Normal file
View File

@ -0,0 +1,484 @@
# LCARS theme — codeless .bnp manifest (WO-TF-020, Wave A).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
#
# Identity: the full library-computer interface of a sci-fi podcast / media
# outfit. Pill rails, elbow brackets, live numeric readouts, stardate stamps
# and episode-console furniture frame every surface — the chrome IS the
# interface. Dark = canonical LCARS black with orange / amber / lilac panels;
# light = daylight-ops cream with the colour bars kept vivid. Motion tier
# GO BIG: one dependency-free canvas sweep on the hero / landing only.
#
# This theme was previously a pongo2 wasm guest; the codeless path renders the
# SAME .ninjatpl sources host-side via cms/backend/plugin/pongoengine, so no
# engine ships here. Every override maps its classes 1:1 onto the lcars-*
# vocabulary defined below; semantic colour tokens only (hsl(var(--token))).
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
required_icon_packs:
- lucide
system_templates:
- key: lcars
title: LCARS
description: "Library-computer interface theme for sci-fi podcast and media sites: elbow-rail chrome, numeric readouts, stardate stamps, dual daylight-ops cream and canonical-black modes."
page_templates:
- system: lcars
key: default
title: Standard Display
description: "Elbow header, left command rail, main panel, and footer bar."
slots: [header, sidebar, main, footer]
- system: lcars
key: full-width
title: Full Display
description: "Edge-to-edge LCARS frame with no command rail."
slots: [header, main, footer]
- system: lcars
key: full-display
title: Full Display (legacy)
description: "Pre-existing full-width key, retained for sites already using it."
slots: [header, main, footer]
- system: lcars
key: landing
title: Landing
description: "Hero readout band, main panel stack, closing CTA, and footer."
slots: [hero, main, cta, footer]
- system: lcars
key: article
title: Article
description: "Long-form transcript column with a data aside rail."
slots: [header, main, aside, footer]
- system: lcars
key: blog-index
title: Episode Index
description: "Title readout, featured lead, and an episode-panel grid."
slots: [header, featured, main, footer]
- system: lcars
key: contact
title: Hail Channel
description: "Contact console: form on the main panel, channel details in the aside rail."
slots: [header, main, aside, footer]
- system: lcars
key: auth
title: Access Terminal
description: "Centered access panel on a canonical-black terminal ground."
slots: [main, footer]
template_overrides:
# Chrome (4)
- { template: lcars, block: navbar }
- { template: lcars, block: footer }
- { template: lcars, block: utility-bar }
- { template: lcars, block: announcement-bar }
# Marketing (12)
- { template: lcars, block: hero }
- { template: lcars, block: feature-grid }
- { template: lcars, block: rich-section }
- { template: lcars, block: stats }
- { template: lcars, block: testimonial }
- { template: lcars, block: logo-strip }
- { template: lcars, block: pricing }
- { template: lcars, block: team }
- { template: lcars, block: faq }
- { template: lcars, block: timeline }
- { template: lcars, block: cta }
- { template: lcars, block: countdown }
# Primitives (11)
- { template: lcars, block: heading }
- { template: lcars, block: text }
- { template: lcars, block: rich-text }
- { template: lcars, block: image }
- { template: lcars, block: quote }
- { template: lcars, block: divider }
- { template: lcars, block: button }
- { template: lcars, block: card }
- { template: lcars, block: gallery }
- { template: lcars, block: video-embed }
- { template: lcars, block: audio-embed }
# Commerce / local (6)
- { template: lcars, block: menu-list }
- { template: lcars, block: event-list }
- { template: lcars, block: contact-form }
- { template: lcars, block: contact-card }
- { template: lcars, block: hours-location }
- { template: lcars, block: social-links }
# Blog family (12)
- { template: lcars, block: blog-hero }
- { template: lcars, block: featured-posts }
- { template: lcars, block: related-posts }
- { template: lcars, block: popular-posts }
- { template: lcars, block: category-list }
- { template: lcars, block: post-hero }
- { template: lcars, block: post-metadata }
- { template: lcars, block: author-bio }
- { template: lcars, block: author-bio-hero }
- { template: lcars, block: newsletter-signup }
- { template: lcars, block: breadcrumbs }
- { template: lcars, block: sidebar-nav }
# Auth surface (3)
- { template: lcars, block: auth-form }
- { template: lcars, block: password-reset-form }
- { template: lcars, block: auth-status }
# System (1)
- { template: lcars, block: page-suggestions }
email_wrappers:
- lcars
css:
input_css_append: |
/* ============================================================
LCARS — appended component CSS (WO-TF-020).
Library-computer interface: pill rails, elbow brackets,
numeric readouts, stardate stamps. Semantic tokens ONLY —
hsl(var(--token)); never literal colours. Class names map
1:1 onto the override vocabulary.
============================================================ */
/* Bundled display faces. The host does not consume bundled_fonts
(LoadedPlugin.BundledFonts is unconsumed), so the theme declares its own
@font-face against the served artifact paths (/templates/lcars/fonts/...).
Presets set typography.fontHeading/body/mono to template:lcars:<Family> so
--font-heading/--font-body/--font-mono resolve to these families. */
@font-face { font-family: 'Antonio'; src: url('/templates/lcars/fonts/antonio-latin.woff2') format('woff2'); font-weight: 100 700; font-style: normal; font-display: swap; }
@font-face { font-family: 'Antonio'; src: url('/templates/lcars/fonts/antonio-latin-ext.woff2') format('woff2'); font-weight: 100 700; font-style: normal; font-display: swap; unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: 'Share Tech Mono'; src: url('/templates/lcars/fonts/share-tech-mono-latin.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
/* --- Base / layout primitives --- */
.lcars-page { font-family: var(--font-body, "Antonio", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif); letter-spacing: 0.02em; }
.lcars-body { font-family: var(--font-body, "Antonio", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif); }
.lcars-mono { font-family: var(--font-mono, "Share Tech Mono", ui-monospace, "SFMono-Regular", monospace); font-variant-numeric: tabular-nums; letter-spacing: 0.05em; }
.numeric-tabular { font-variant-numeric: tabular-nums; }
.lcars-well { width: 100%; max-width: 1280px; margin-left: auto; margin-right: auto; padding-left: 1.5rem; padding-right: 1.5rem; box-sizing: border-box; }
.lcars-measure { max-width: 72ch; }
.lcars-section { padding-top: 3rem; padding-bottom: 3rem; }
.lcars-section-tight { padding-top: 1.75rem; padding-bottom: 1.75rem; }
.lcars-stack > * + * { margin-top: 1.5rem; }
.lcars-stack-sm > * + * { margin-top: 0.625rem; }
/* --- Typography --- */
.lcars-display, .lcars-heading { font-family: var(--font-heading, "Antonio", "Roboto Condensed", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: hsl(var(--primary)); line-height: 1.05; margin: 0; }
.lcars-heading-h2-accent { border-bottom: 0.1875rem solid hsl(var(--primary)); padding-bottom: 0.25rem; }
.lcars-heading-1 { font-size: clamp(2rem, 5vw, 2.75rem); }
.lcars-heading-2 { font-size: clamp(1.625rem, 3.5vw, 2rem); }
.lcars-heading-3 { font-size: 1.375rem; }
.lcars-heading-4 { font-size: 1.125rem; }
.lcars-eyebrow { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.22em; color: hsl(var(--secondary)); margin: 0 0 0.75rem 0; }
.lcars-kicker { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.75rem; letter-spacing: 0.18em; text-transform: uppercase; color: hsl(var(--accent)); }
.lcars-text { font-family: var(--font-body, "Antonio", "Roboto Condensed", system-ui, sans-serif); font-size: 1.0625rem; line-height: 1.7; color: hsl(var(--foreground)); }
.lcars-text a { color: hsl(var(--secondary)); text-decoration: underline; text-underline-offset: 0.2em; }
.lcars-text a:hover { color: hsl(var(--primary)); }
.lcars-text strong { color: hsl(var(--primary)); font-weight: 700; }
.lcars-text code, .lcars-prose code { font-family: var(--font-mono, "Share Tech Mono", monospace); background-color: hsl(var(--muted)); padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875em; color: hsl(var(--accent)); }
.lcars-prose { font-family: var(--font-body, "Antonio", "Roboto Condensed", system-ui, sans-serif); color: hsl(var(--foreground)); line-height: 1.7; font-size: 1.0625rem; }
.lcars-prose a { color: hsl(var(--secondary)); text-decoration: underline; text-underline-offset: 0.2em; }
.lcars-prose a:hover { color: hsl(var(--primary)); }
.lcars-prose strong { color: hsl(var(--primary)); }
/* Paragraph rhythm inside long-form text/prose (episode transcripts) so
multi-paragraph bodies separate cleanly instead of running together. */
.lcars-text p, .lcars-prose p { margin: 0 0 1.1rem 0; }
.lcars-text p:last-child, .lcars-prose p:last-child { margin-bottom: 0; }
.lcars-lede { font-family: var(--font-body, "Antonio", system-ui, sans-serif); font-size: 1.25rem; line-height: 1.6; color: hsl(var(--muted-foreground)); }
.lcars-label { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: hsl(var(--foreground)); }
.lcars-readout { font-family: var(--font-mono, "Share Tech Mono", monospace); font-variant-numeric: tabular-nums; letter-spacing: 0.1em; color: hsl(var(--accent)); text-transform: uppercase; }
.lcars-stardate { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.8125rem; font-weight: 400; letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--accent)); }
/* --- Pills / bars / marks (LCARS signature) --- */
.lcars-pill { display: inline-block; height: 1.25rem; min-width: 3rem; border-radius: 0.625rem; background-color: hsl(var(--primary)); flex: none; }
.lcars-pill-secondary { background-color: hsl(var(--secondary)); }
.lcars-pill-accent { background-color: hsl(var(--accent)); }
.lcars-cap { display: inline-block; height: 1.5rem; width: 1.5rem; background-color: hsl(var(--primary)); flex: none; }
.lcars-cap-left { border-radius: 0.75rem 0 0 0.75rem; }
.lcars-cap-right { border-radius: 0 0.75rem 0.75rem 0; }
.lcars-bar-accent { height: 0.5rem; border-radius: 0.25rem; background: linear-gradient(to right, hsl(var(--primary)) 0%, hsl(var(--primary)) 34%, hsl(var(--secondary)) 34%, hsl(var(--secondary)) 67%, hsl(var(--accent)) 67%, hsl(var(--accent)) 100%); }
.lcars-rule { border: 0; border-top: 0.1875rem solid hsl(var(--primary)); margin: 0; }
/* --- Buttons (pill, uppercase, right-rounded) --- */
.lcars-btn-primary { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; background-color: hsl(var(--primary)); color: hsl(var(--background)); border: 0; border-radius: 0 1.25rem 1.25rem 0; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; line-height: 1.3; box-shadow: none; background-image: none; transition: filter 150ms ease; cursor: pointer; }
.lcars-btn-primary:hover { filter: brightness(1.15); }
.lcars-btn-primary:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
.lcars-btn-secondary { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; background-color: hsl(var(--secondary)); color: hsl(var(--background)); border: 0; border-radius: 0 1.25rem 1.25rem 0; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; line-height: 1.3; transition: filter 150ms ease; cursor: pointer; }
.lcars-btn-secondary:hover { filter: brightness(1.15); }
.lcars-btn-outline { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; background-color: transparent; color: hsl(var(--secondary)); border: 0.125rem solid hsl(var(--secondary)); border-radius: 0 1.25rem 1.25rem 0; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; line-height: 1.3; transition: background-color 150ms ease, color 150ms ease; cursor: pointer; }
.lcars-btn-outline:hover { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
.lcars-btn-outline:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
.lcars-btn-ghost { display: inline-flex; align-items: center; gap: 0.375rem; color: hsl(var(--accent)); font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; line-height: 1.3; transition: gap 150ms ease; }
.lcars-btn-ghost:hover { gap: 0.625rem; }
.lcars-btn-sm { padding: 0.375rem 0.875rem; font-size: 0.8125rem; }
.lcars-btn-lg { padding: 0.75rem 1.75rem; font-size: 1.0625rem; }
.lcars-btn-row { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.lcars-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; }
/* Sized <use> icons carry explicit width/height attributes (SVG geometry
guard); this only handles inline flow/alignment, never sizing. */
.lcars-inline-icon { display: inline-block; vertical-align: -0.125em; flex: none; }
/* --- Panels / cards / surfaces --- */
.lcars-panel { background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border-left: 0.5rem solid hsl(var(--primary)); border-radius: 0 0.5rem 0.5rem 0; padding: 1.25rem 1.5rem; box-shadow: none; }
.lcars-panel-secondary { border-left-color: hsl(var(--secondary)); }
.lcars-panel-accent { border-left-color: hsl(var(--accent)); }
.lcars-panel-header { display: flex; align-items: center; gap: 0.625rem; margin-bottom: 0.75rem; }
.lcars-panel-title { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.9375rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; color: hsl(var(--foreground)); white-space: nowrap; }
.lcars-panel-header-bar { flex: 1; height: 0.125rem; background-color: hsl(var(--border)); }
.lcars-panel-body { }
.lcars-card { background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border-top: 0.375rem solid hsl(var(--primary)); border-radius: 0 0 0.75rem 0.75rem; padding: 1.5rem; box-shadow: none; overflow: hidden; }
.lcars-card-accent { border-top-color: hsl(var(--accent)); }
.lcars-surface-muted { background-color: hsl(var(--muted)); }
.lcars-surface-card { background-color: hsl(var(--card)); }
.lcars-media-frame { display: block; width: 100%; overflow: hidden; background-color: hsl(var(--muted)); border-radius: 0.5rem; }
.lcars-media-frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
.lcars-img-empty { display: block; width: 100%; aspect-ratio: 16 / 9; max-height: 20rem; background-color: hsl(var(--muted)); border-radius: 0.5rem; }
/* Graceful empty-media state: an on-brand LCARS readout panel with a status
label, shown instead of a bare grey rectangle when no image ships. Stays
compact instead of ballooning into a blank square. */
.lcars-console-art { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.9rem; align-self: start; width: 100%; aspect-ratio: 4 / 3; max-height: 15rem; padding: 1.5rem; background-color: hsl(var(--muted)); border-radius: 0.5rem; box-sizing: border-box; }
.lcars-console-art-label { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.6875rem; font-weight: 400; letter-spacing: 0.24em; text-transform: uppercase; color: hsl(var(--muted-foreground)); }
.lcars-console-art-scope { display: flex; flex-direction: column; gap: 0.5rem; width: 100%; max-width: 16rem; }
.lcars-console-art-scope i { display: block; height: 0.625rem; border-radius: 0.3125rem; opacity: 0.55; }
.lcars-console-art-scope i:nth-child(1) { width: 100%; background-color: hsl(var(--primary)); }
.lcars-console-art-scope i:nth-child(2) { width: 72%; background-color: hsl(var(--secondary)); }
.lcars-console-art-scope i:nth-child(3) { width: 88%; background-color: hsl(var(--accent)); }
.lcars-console-art-scope i:nth-child(4) { width: 60%; background-color: hsl(var(--secondary)); }
.lcars-console-art-scope i:nth-child(5) { width: 80%; background-color: hsl(var(--primary)); }
/* --- Responsive grids --- */
.lcars-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 640px) { .lcars-grid-2, .lcars-grid-3, .lcars-grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .lcars-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .lcars-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.lcars-leadership-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 640px) { .lcars-leadership-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .lcars-leadership-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* --- Page frame chrome (elbow rails, command sidebar) --- */
.lcars-frame { display: flex; flex-direction: column; min-height: 100vh; padding: 0.5rem; gap: 0.375rem; background-color: hsl(var(--background)); }
.lcars-elbow-top, .lcars-elbow-bottom { display: flex; align-items: stretch; height: 2.25rem; gap: 0.375rem; }
.lcars-elbow-corner { width: 12rem; min-width: 12rem; background-color: hsl(var(--primary)); }
.lcars-elbow-tl { border-radius: 0 0 2rem 0; }
.lcars-elbow-bl { border-radius: 0 2rem 0 0; }
.lcars-bar { flex: 1; }
.lcars-bar-top { background-color: hsl(var(--secondary)); border-radius: 0 0 1rem 0; }
.lcars-bar-bottom { background-color: hsl(var(--secondary)); border-radius: 0 1rem 0 0; }
.lcars-frame-header { display: flex; align-items: center; gap: 0.625rem; }
.lcars-frame-caps { display: flex; align-items: center; gap: 0.375rem; }
.lcars-content-row { display: grid; grid-template-columns: 12rem 1fr; gap: 0.375rem; flex: 1; }
.lcars-rail { display: flex; flex-direction: column; gap: 0.375rem; }
.lcars-rail-btn { display: flex; align-items: center; justify-content: flex-end; text-align: right; padding: 0.625rem 1rem; min-height: 3rem; border-radius: 0 0 0 1.5rem; background-color: hsl(var(--secondary)); color: hsl(var(--background)); font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; transition: filter 150ms ease; }
.lcars-rail-btn:nth-child(3n+1) { background-color: hsl(var(--primary)); }
.lcars-rail-btn:nth-child(3n+2) { background-color: hsl(var(--secondary)); }
.lcars-rail-btn:nth-child(3n) { background-color: hsl(var(--accent)); }
.lcars-rail-btn:hover { filter: brightness(1.15); }
.lcars-rail-cap { height: 2.25rem; background-color: hsl(var(--primary)); border-radius: 1rem 0 0 0; }
.lcars-rail-readout { padding: 0.5rem 1rem; text-align: right; }
.lcars-main-panel { min-width: 0; background-color: hsl(var(--background)); }
/* --- Chrome: navbar --- */
.lcars-navbar { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; padding: 0.5rem 0; }
.lcars-brand { display: inline-flex; align-items: center; gap: 0.625rem; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; font-size: 1.25rem; text-transform: uppercase; letter-spacing: 0.14em; color: hsl(var(--primary)); text-decoration: none; }
.lcars-brand-logo { height: 2rem; width: auto; }
.lcars-nav-links { display: none; align-items: center; gap: 0.375rem; }
@media (min-width: 768px) { .lcars-nav-links { display: flex; } }
.lcars-nav-link { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: hsl(var(--background)); background-color: hsl(var(--secondary)); padding: 0.375rem 1rem; border-radius: 0 1rem 1rem 0; text-decoration: none; transition: filter 150ms ease; }
.lcars-nav-link:hover { filter: brightness(1.15); }
.lcars-nav-toggle { display: inline-flex; }
@media (min-width: 768px) { .lcars-nav-toggle { display: none; } }
.lcars-nav-drawer { display: none; flex-direction: column; gap: 0.375rem; padding: 0.75rem 0 0.5rem; width: 100%; }
@media (min-width: 768px) { .lcars-nav-drawer { display: none !important; } }
.lcars-nav-drawer a { padding: 0.625rem 1rem; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: hsl(var(--background)); background-color: hsl(var(--secondary)); border-radius: 0 1rem 1rem 0; text-decoration: none; }
/* --- Chrome: utility bar + announcement bar --- */
.lcars-utility-bar { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.8125rem; letter-spacing: 0.08em; padding: 0.375rem 0; }
.lcars-utility-bar--muted { background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); }
.lcars-utility-bar--primary { background-color: hsl(var(--primary)); color: hsl(var(--background)); }
.lcars-utility-bar--dark { background-color: hsl(var(--foreground)); color: hsl(var(--background)); }
.lcars-utility-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.lcars-utility-group { display: flex; align-items: center; gap: 1.25rem; }
.lcars-utility-item { display: inline-flex; align-items: center; gap: 0.375rem; color: inherit; text-decoration: none; }
.lcars-announce { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; padding: 0.5rem 0; }
.lcars-announce--info { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
.lcars-announce--promo { background-color: hsl(var(--accent)); color: hsl(var(--background)); }
.lcars-announce--alert { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
.lcars-announce-row { display: flex; align-items: center; justify-content: center; gap: 0.75rem; }
.lcars-announce a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.lcars-announce-close { margin-left: auto; background: none; border: 0; color: inherit; cursor: pointer; line-height: 1; padding: 0 0.25rem; }
/* --- Chrome: footer --- */
.lcars-footer { background-color: hsl(var(--background)); color: hsl(var(--foreground)); border-top: 0.375rem solid hsl(var(--primary)); border-radius: 0.75rem 0.75rem 0 0; padding: 2.5rem 0 2rem; }
.lcars-footer-cols { display: grid; gap: 2rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 640px) { .lcars-footer-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .lcars-footer-cols { grid-template-columns: 2fr repeat(3, 1fr); } }
.lcars-footer-head { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em; color: hsl(var(--secondary)); margin: 0 0 0.875rem; }
.lcars-footer-link { display: block; font-size: 0.9375rem; color: hsl(var(--foreground)); text-decoration: none; padding: 0.25rem 0; }
.lcars-footer-link:hover { color: hsl(var(--primary)); }
.lcars-footer-legal { color: hsl(var(--muted-foreground)); font-size: 0.8125rem; font-family: var(--font-mono, "Share Tech Mono", monospace); letter-spacing: 0.06em; border-top: 1px solid hsl(var(--border)); padding-top: 1.5rem; margin-top: 2.5rem; display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; }
/* --- Marketing: hero --- */
.lcars-hero { padding: 4rem 0; }
.lcars-hero-headline { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; line-height: 1.02; font-size: clamp(2.5rem, 6vw, 4.5rem); color: hsl(var(--primary)); margin: 0; }
.lcars-hero-sub { font-size: clamp(1.125rem, 2vw, 1.5rem); line-height: 1.5; color: hsl(var(--foreground)); margin: 1.25rem 0 0; }
.lcars-hero-split { display: grid; gap: 2.5rem; grid-template-columns: repeat(1, minmax(0, 1fr)); align-items: center; }
@media (min-width: 1024px) { .lcars-hero-split { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* --- Marketing: logo strip --- */
.lcars-logo { filter: grayscale(1); opacity: 0.7; transition: filter 200ms ease, opacity 200ms ease; }
.lcars-logo:hover { filter: grayscale(0); opacity: 1; }
.lcars-logo-row { display: flex; flex-wrap: wrap; align-items: center; gap: 2.5rem; }
/* --- Marketing: feature grid --- */
.lcars-feature { border-top: 0.25rem solid hsl(var(--accent)); border-radius: 0 0 0.5rem 0.5rem; padding: 1rem 1rem 1.25rem; background-color: hsl(var(--card)); }
.lcars-feature-title { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; font-size: 1.0625rem; color: hsl(var(--foreground)); margin: 0.75rem 0 0.5rem; }
/* --- Marketing: stats / metrics --- */
.lcars-metric { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: clamp(2.5rem, 5vw, 3.5rem); line-height: 1; font-variant-numeric: tabular-nums; color: hsl(var(--primary)); letter-spacing: 0.02em; }
.lcars-stat-figure { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: clamp(2.25rem, 4vw, 3rem); line-height: 1; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); letter-spacing: 0.02em; }
.lcars-stat-label { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.8125rem; color: hsl(var(--muted-foreground)); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 0.5rem; }
.lcars-numeral-index { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 2rem; line-height: 1; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); }
/* --- Marketing: pull quote / testimonial --- */
.lcars-pull-quote { border-left: 0.375rem solid hsl(var(--accent)); border-radius: 0 0.5rem 0.5rem 0; padding: 1rem 1.5rem; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 1.5rem; line-height: 1.3; font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em; color: hsl(var(--foreground)); }
.lcars-quote-block { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 1.375rem; line-height: 1.4; font-weight: 700; color: hsl(var(--foreground)); }
.lcars-quote-cite { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; color: hsl(var(--muted-foreground)); margin-top: 0.75rem; }
.lcars-avatar { width: 2.75rem; height: 2.75rem; border-radius: 9999px; object-fit: cover; background-color: hsl(var(--muted)); }
/* --- Marketing: pricing --- */
.lcars-price { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 2.5rem; font-variant-numeric: tabular-nums; color: hsl(var(--primary)); letter-spacing: 0.02em; }
.lcars-price-period { font-family: var(--font-body, "Antonio", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--muted-foreground)); }
.lcars-price-tier { display: flex; flex-direction: column; }
.lcars-price-tier--featured { border-top: 0.5rem solid hsl(var(--accent)); }
.lcars-badge { display: inline-block; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; padding: 0.25rem 0.625rem; background-color: hsl(var(--accent)); color: hsl(var(--background)); border-radius: 0.625rem; }
.lcars-check { color: hsl(var(--accent)); flex: none; }
/* --- Marketing: faq --- */
.lcars-faq-item { border-top: 1px solid hsl(var(--border)); }
.lcars-faq-item summary { cursor: pointer; list-style: none; padding: 1.125rem 0; display: flex; align-items: center; justify-content: space-between; gap: 1rem; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: hsl(var(--foreground)); }
.lcars-faq-item summary::-webkit-details-marker { display: none; }
.lcars-faq-item[open] summary .lcars-faq-chevron { transform: rotate(180deg); }
.lcars-faq-chevron { transition: transform 150ms ease; color: hsl(var(--accent)); }
.lcars-faq-answer { padding: 0 0 1.125rem; color: hsl(var(--muted-foreground)); font-size: 1rem; line-height: 1.65; }
/* --- Marketing: timeline --- */
.lcars-timeline-step { display: grid; grid-template-columns: auto 1fr; gap: 1.25rem; padding-bottom: 2rem; }
.lcars-timeline-rail { position: relative; display: flex; flex-direction: column; align-items: center; }
.lcars-timeline-node { width: 2.75rem; height: 2.75rem; background-color: hsl(var(--accent)); border-radius: 0 1.375rem 1.375rem 0; display: flex; align-items: center; justify-content: center; font-family: var(--font-mono, "Share Tech Mono", monospace); color: hsl(var(--background)); flex: none; font-variant-numeric: tabular-nums; }
.lcars-timeline-line { flex: 1 1 auto; width: 0.1875rem; background-color: hsl(var(--border)); margin-top: 0.5rem; }
/* --- Marketing: cta strip --- */
.lcars-cta-strip { padding: 3rem 0; }
.lcars-cta-strip--band { background-color: hsl(var(--primary)); color: hsl(var(--background)); border-radius: 1rem; }
.lcars-cta-strip--band .lcars-cta-headline { color: hsl(var(--background)); }
.lcars-cta-strip--band .lcars-btn-primary { background-color: hsl(var(--background)); color: hsl(var(--primary)); }
.lcars-cta-strip--card { background-color: hsl(var(--muted)); border-radius: 1rem; }
.lcars-cta-headline { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; font-size: clamp(1.75rem, 3vw, 2.5rem); line-height: 1.1; margin: 0; color: hsl(var(--primary)); }
/* --- Marketing: countdown --- */
.lcars-count-unit { display: flex; flex-direction: column; align-items: center; }
.lcars-count-figure { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: clamp(2rem, 4vw, 3rem); font-variant-numeric: tabular-nums; color: hsl(var(--accent)); line-height: 1; }
.lcars-count-box { background-color: hsl(var(--card)); border-top: 0.25rem solid hsl(var(--primary)); padding: 1rem 1.25rem; border-radius: 0 0 0.5rem 0.5rem; min-width: 4.5rem; }
.lcars-count-label { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.1em; color: hsl(var(--muted-foreground)); margin-top: 0.375rem; }
/* --- Primitives: divider / gallery / media --- */
.lcars-divider-line { border: 0; border-top: 0.1875rem solid hsl(var(--border)); }
.lcars-divider-thick { border-top-width: 0.375rem; border-top-color: hsl(var(--primary)); }
.lcars-divider-ornament { display: flex; align-items: center; justify-content: center; gap: 0.5rem; color: hsl(var(--primary)); }
.lcars-gallery-grid { display: grid; gap: 0.5rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 768px) { .lcars-gallery-grid.lcars-gallery-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .lcars-gallery-grid.lcars-gallery-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.lcars-video-frame { position: relative; display: block; width: 100%; background-color: hsl(var(--muted)); border-radius: 0.5rem; overflow: hidden; }
.lcars-video-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: hsl(var(--background)); background: none; border: 0; cursor: pointer; }
.lcars-video-play-badge { width: 4rem; height: 4rem; border-radius: 9999px; background-color: hsl(var(--primary)); color: hsl(var(--background)); display: flex; align-items: center; justify-content: center; }
/* --- Commerce / local --- */
.lcars-menu-item { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid hsl(var(--border)); }
.lcars-menu-price { font-family: var(--font-mono, "Share Tech Mono", monospace); font-variant-numeric: tabular-nums; color: hsl(var(--accent)); }
.lcars-channel { display: flex; align-items: flex-start; gap: 0.75rem; }
.lcars-channel-mark { width: 2.75rem; height: 2.75rem; background-color: hsl(var(--primary)); border-radius: 0 1.375rem 1.375rem 0; display: flex; align-items: center; justify-content: center; color: hsl(var(--background)); flex: none; }
.lcars-hours-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.375rem 0; border-bottom: 1px solid hsl(var(--border)); }
.lcars-social-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.lcars-social-link { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.5rem 0.875rem; background-color: hsl(var(--secondary)); border-radius: 0 1rem 1rem 0; color: hsl(var(--background)); text-decoration: none; font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; transition: filter 150ms ease; }
.lcars-social-link:hover { filter: brightness(1.15); }
/* --- Forms --- */
.lcars-form { display: grid; gap: 1.25rem; }
.lcars-form-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.lcars-field { display: flex; flex-direction: column; gap: 0.375rem; }
.lcars-field--full { grid-column: 1 / -1; }
.lcars-input { width: 100%; box-sizing: border-box; padding: 0.625rem 0.875rem; background-color: hsl(var(--background)); color: hsl(var(--foreground)); border: 0.125rem solid hsl(var(--input)); border-radius: 0.5rem; font-family: var(--font-body, "Antonio", system-ui, sans-serif); font-size: 1rem; }
.lcars-input:focus { outline: 2px solid hsl(var(--ring)); outline-offset: 1px; border-color: hsl(var(--ring)); }
.lcars-form-note { font-size: 0.8125rem; color: hsl(var(--muted-foreground)); }
.lcars-alert-ok { border-left: 0.375rem solid hsl(var(--primary)); background-color: hsl(var(--muted)); padding: 1rem 1.25rem; color: hsl(var(--foreground)); border-radius: 0 0.5rem 0.5rem 0; }
.lcars-alert-err { border-left: 0.375rem solid hsl(var(--destructive)); background-color: hsl(var(--muted)); padding: 1rem 1.25rem; color: hsl(var(--foreground)); border-radius: 0 0.5rem 0.5rem 0; }
/* --- Blog family --- */
.lcars-post-card { display: flex; flex-direction: column; gap: 0.75rem; background-color: hsl(var(--card)); border-top: 0.375rem solid hsl(var(--secondary)); border-radius: 0 0 0.5rem 0.5rem; padding: 1rem 1.25rem 1.25rem; }
.lcars-post-thumb { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; background-color: hsl(var(--muted)); border-radius: 0.375rem; }
.lcars-post-title { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; font-size: 1.1875rem; line-height: 1.2; color: hsl(var(--primary)); text-decoration: none; }
.lcars-post-title:hover { color: hsl(var(--secondary)); }
.lcars-post-meta { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: hsl(var(--muted-foreground)); display: flex; flex-wrap: wrap; gap: 0.75rem; }
.lcars-post-excerpt { color: hsl(var(--muted-foreground)); font-size: 0.9375rem; line-height: 1.6; }
.lcars-post-grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 640px) { .lcars-post-grid-2, .lcars-post-grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .lcars-post-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.lcars-tag { display: inline-block; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.1875rem 0.625rem; background-color: hsl(var(--muted)); border-radius: 0.625rem; color: hsl(var(--secondary)); text-decoration: none; }
.lcars-tag:hover { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
.lcars-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.8125rem; letter-spacing: 0.06em; color: hsl(var(--muted-foreground)); }
.lcars-crumbs a { color: hsl(var(--secondary)); text-decoration: none; }
.lcars-crumbs a:hover { color: hsl(var(--primary)); }
.lcars-crumb-sep { color: hsl(var(--accent)); }
.lcars-sidebar-link { display: block; padding: 0.5rem 0.875rem; border-radius: 0 1rem 1rem 0; color: hsl(var(--foreground)); text-decoration: none; font-size: 0.9375rem; }
.lcars-sidebar-link:hover, .lcars-sidebar-link[aria-current="page"] { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
.lcars-rank { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 1.25rem; color: hsl(var(--accent)); font-variant-numeric: tabular-nums; width: 2rem; flex: none; }
/* --- Status readouts --- */
.lcars-status { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; padding: 0.25rem 0.75rem; border-radius: 0.75rem; }
.lcars-status-online { background-color: hsl(var(--primary)); color: hsl(var(--background)); animation: lcars-pulse 3s ease-in-out infinite; }
.lcars-status-standby { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
.lcars-status-alert { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); animation: lcars-blink 1s step-end infinite; }
.lcars-status-offline { background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); }
/* --- Auth --- */
.lcars-auth-radio { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.lcars-auth-panel { display: none; }
/* --- Motion (GO BIG: canvas sweep on hero/landing only; CSS elsewhere) --- */
.lcars-hero-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; z-index: 0; }
.lcars-hero-canvas-host { position: relative; overflow: hidden; }
.lcars-hero-canvas-host > *:not(.lcars-hero-canvas) { position: relative; z-index: 1; }
.lcars-reveal { opacity: 0; transform: translateY(0.5rem); animation: lcars-reveal 500ms ease forwards; }
@keyframes lcars-reveal { to { opacity: 1; transform: none; } }
@keyframes lcars-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
@keyframes lcars-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
@keyframes lcars-sweep { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) {
.lcars-reveal { animation: none; opacity: 1; transform: none; }
.lcars-status-online, .lcars-status-alert { animation: none; }
.lcars-btn-ghost, .lcars-logo, .lcars-nav-link, .lcars-faq-chevron { transition: none; }
}
/* --- Responsive: collapse the command rail on small screens --- */
@media (max-width: 768px) {
.lcars-content-row { grid-template-columns: 1fr; }
.lcars-rail { flex-direction: row; flex-wrap: wrap; }
.lcars-elbow-corner { width: 4rem; min-width: 4rem; }
.lcars-rail-btn { border-radius: 0 1rem 1rem 0; justify-content: center; }
.lcars-form-grid { grid-template-columns: 1fr; }
}
/* --- Compatibility layer: classes the overrides carry over from the
shared builtin re-skin. Styled LCARS-native so every class resolves. --- */
.lcars-content-well { width: 100%; max-width: 1280px; margin-left: auto; margin-right: auto; padding-left: 1.5rem; padding-right: 1.5rem; box-sizing: border-box; }
.lcars-swiss-12 { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: 1.25rem; row-gap: 1.25rem; }
.lcars-article-measure { max-width: 72ch; }
.lcars-hairline-top { border-top: 0.1875rem solid hsl(var(--border)); }
.lcars-hairline-bottom { border-bottom: 0.1875rem solid hsl(var(--border)); }
.lcars-accent-rule { border-top: 0.1875rem solid hsl(var(--primary)); }
.lcars-heading-h1 { font-size: clamp(2rem, 5vw, 2.75rem); }
.lcars-heading-h2 { font-size: clamp(1.625rem, 3.5vw, 2rem); }
.lcars-heading-h3 { font-size: 1.375rem; }
.lcars-heading-h4 { font-size: 1.125rem; }
.lcars-heading-h5 { font-size: 1rem; }
.lcars-heading-h6 { font-size: 0.875rem; }
/* LCARS marks: pills and caps replace the Bauhaus geometric marks. */
.lcars-mark { display: inline-block; width: 1.5rem; height: 0.75rem; flex: none; background-color: hsl(var(--primary)); border-radius: 0.375rem; }
.lcars-mark-circle { border-radius: 9999px; width: 0.75rem; background-color: hsl(var(--primary)); }
.lcars-mark-square { border-radius: 0 0.375rem 0.375rem 0; background-color: hsl(var(--secondary)); }
.lcars-mark-triangle { border-radius: 0.375rem 0 0 0.375rem; background-color: hsl(var(--accent)); }
.lcars-mark-ring { border-radius: 9999px; width: 0.75rem; background: none; border: 2px solid hsl(var(--accent)); }

View File

@ -1,57 +0,0 @@
package main
import "git.dev.alexdunmow.com/block/core/plugin"
func DefaultMasterPages() []plugin.MasterPageDefinition {
return []plugin.MasterPageDefinition{
{
Key: "lcars:standard-display",
Title: "LCARS Standard Display",
PageTemplates: []string{"default"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "lcars:lcars_header",
Title: "LCARS Header",
Content: map[string]any{"title": "LCARS", "subtitle": "Library Computer Access/Retrieval System"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "lcars:lcars_sidebar",
Title: "LCARS Sidebar",
Content: map[string]any{},
Slot: "sidebar",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Main Content Slot",
Content: map[string]any{"slotName": "main", "placeholder": "Enter display data…"},
Slot: "main",
SortOrder: 0,
},
},
},
{
Key: "lcars:full-display",
Title: "LCARS Full Display",
PageTemplates: []string{"full-display"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "lcars:lcars_header",
Title: "LCARS Header",
Content: map[string]any{"title": "LCARS", "subtitle": "Main Viewer"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Main Content Slot",
Content: map[string]any{"slotName": "main", "placeholder": "Enter display data…"},
Slot: "main",
SortOrder: 0,
},
},
},
}
}

53
master_pages.json Normal file
View File

@ -0,0 +1,53 @@
[
{
"key": "lcars:standard-display",
"title": "LCARS Standard Display",
"page_templates": ["default"],
"blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main", "companyName": "Subspace Signal" }, "slot": "header", "sort_order": 0 },
{ "block_key": "html", "title": "Command Rail", "content": { "html": "<nav class=\"lcars-rail\" aria-label=\"Command rail\"><span class=\"lcars-rail-cap\" aria-hidden=\"true\"></span><a class=\"lcars-rail-btn\" href=\"/\">Home</a><a class=\"lcars-rail-btn\" href=\"/about\">About</a><a class=\"lcars-rail-btn\" href=\"/episodes\">Episodes</a><a class=\"lcars-rail-btn\" href=\"/contact\">Hail</a><div class=\"lcars-rail-readout\"><span class=\"lcars-readout\" style=\"font-size:0.6875rem;\">Sig OK</span></div></nav>" }, "slot": "sidebar", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Subspace Signal", "tagline": "A weekly transmission on science fiction and the future.", "copyright": "Subspace Signal" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "lcars:content-display",
"title": "LCARS Content Display",
"page_templates": ["full-width", "full-display", "article", "blog-index"],
"blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main", "companyName": "Subspace Signal" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Subspace Signal", "tagline": "A weekly transmission on science fiction and the future.", "copyright": "Subspace Signal" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "lcars:landing-display",
"title": "LCARS Landing Display",
"page_templates": ["landing"],
"blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main", "companyName": "Subspace Signal" }, "slot": "header", "sort_order": 0 },
{ "block_key": "hero", "title": "Landing Hero", "content": { "template": "left-aligned", "eyebrow": "Now broadcasting", "headline": "Subspace Signal", "subheadline": "A weekly transmission on science fiction and the future." }, "slot": "hero", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "cta", "title": "Closing CTA", "content": { "background": "band", "heading": "Tune the frequency", "text": "New transmission every week. Subscribe on your channel of choice.", "primaryLabel": "Subscribe", "primaryUrl": "/episodes" }, "slot": "cta", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Subspace Signal", "copyright": "Subspace Signal" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "lcars:contact-display",
"title": "LCARS Contact Display",
"page_templates": ["contact"],
"blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main", "companyName": "Subspace Signal" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Subspace Signal", "copyright": "Subspace Signal" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "lcars:auth-display",
"title": "LCARS Auth Display",
"page_templates": ["auth"],
"blocks": [
{ "block_key": "slot", "title": "Auth Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }
]
}
]

View File

@ -2,11 +2,8 @@
name = "lcars"
display_name = "LCARS"
scope = "@themes"
version = "0.2.8"
version = "0.5.2"
description = "Star Trek-inspired LCARS interface theme for BlockNinja — angular panels, bold accent colors, and the distinctive curved frame layout of the 24th-century starship computer."
kind = "theme"
categories = ["templates"]
tags = ["retro-futurist", "scifi", "lcars", "demo", "showcase", "angular", "amber"]
[compatibility]
block_core = ">=0.11.0 <0.12.0"

View File

@ -1,202 +1,277 @@
[
{
"id": "federation",
"name": "Federation",
"description": "Classic LCARS — gold, periwinkle, and coral on black",
"id": "ops-amber",
"name": "Ops Amber",
"description": "Canonical LCARS. Black ops ground with orange, amber, and lilac panels. Light mode is daylight-ops cream with the colour bars kept vivid.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:lcars:Antonio",
"fontBody": "template:lcars:Antonio",
"fontMono": "template:lcars:Share Tech Mono"
},
"lightColors": {
"background": "0 0% 2%",
"foreground": "40 33% 92%",
"card": "0 0% 5%",
"cardForeground": "40 33% 92%",
"popover": "0 0% 5%",
"popoverForeground": "40 33% 92%",
"primary": "39 90% 65%",
"primaryForeground": "0 0% 0%",
"secondary": "230 55% 70%",
"secondaryForeground": "0 0% 0%",
"muted": "0 0% 10%",
"mutedForeground": "40 20% 55%",
"accent": "10 70% 65%",
"accentForeground": "0 0% 0%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 98%",
"border": "0 0% 15%",
"input": "0 0% 15%",
"ring": "39 90% 65%"
"background": "40 38% 95%",
"foreground": "28 30% 12%",
"card": "40 30% 91%",
"cardForeground": "28 30% 12%",
"popover": "40 30% 91%",
"popoverForeground": "28 30% 12%",
"primary": "28 92% 48%",
"primaryForeground": "40 38% 96%",
"secondary": "38 88% 46%",
"secondaryForeground": "40 38% 96%",
"muted": "40 24% 88%",
"mutedForeground": "28 18% 38%",
"accent": "268 52% 52%",
"accentForeground": "40 38% 96%",
"destructive": "2 74% 46%",
"destructiveForeground": "40 38% 96%",
"border": "40 22% 82%",
"input": "40 22% 82%",
"ring": "28 92% 48%"
},
"darkColors": {
"background": "0 0% 2%",
"foreground": "40 33% 92%",
"card": "0 0% 5%",
"cardForeground": "40 33% 92%",
"popover": "0 0% 5%",
"popoverForeground": "40 33% 92%",
"primary": "39 90% 65%",
"primaryForeground": "0 0% 0%",
"secondary": "230 55% 70%",
"secondaryForeground": "0 0% 0%",
"muted": "0 0% 10%",
"mutedForeground": "40 20% 55%",
"accent": "10 70% 65%",
"accentForeground": "0 0% 0%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 98%",
"border": "0 0% 15%",
"input": "0 0% 15%",
"ring": "39 90% 65%"
},
"mode": "dark"
"background": "240 8% 6%",
"foreground": "40 30% 92%",
"card": "240 8% 11%",
"cardForeground": "40 30% 92%",
"popover": "240 8% 11%",
"popoverForeground": "40 30% 92%",
"primary": "28 100% 58%",
"primaryForeground": "240 8% 6%",
"secondary": "40 96% 62%",
"secondaryForeground": "240 8% 6%",
"muted": "240 6% 16%",
"mutedForeground": "40 14% 66%",
"accent": "266 70% 76%",
"accentForeground": "240 8% 6%",
"destructive": "0 78% 58%",
"destructiveForeground": "240 8% 6%",
"border": "240 6% 20%",
"input": "240 6% 22%",
"ring": "28 100% 58%"
}
}
},
{
"id": "red-alert",
"name": "Red Alert",
"description": "Emergency operations — red and amber on black",
"description": "Condition-one console. Signal red and orange over black, with an amber standby channel. Light mode reads as an emergency daylight console.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:lcars:Antonio",
"fontBody": "template:lcars:Antonio",
"fontMono": "template:lcars:Share Tech Mono"
},
"lightColors": {
"background": "0 0% 2%",
"foreground": "0 0% 92%",
"card": "0 0% 5%",
"cardForeground": "0 0% 92%",
"popover": "0 0% 5%",
"popoverForeground": "0 0% 92%",
"primary": "0 80% 50%",
"primaryForeground": "0 0% 100%",
"secondary": "35 95% 55%",
"secondaryForeground": "0 0% 0%",
"muted": "0 0% 10%",
"mutedForeground": "0 15% 55%",
"accent": "35 95% 55%",
"accentForeground": "0 0% 0%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 98%",
"border": "0 30% 18%",
"input": "0 30% 18%",
"ring": "0 80% 50%"
"background": "20 34% 95%",
"foreground": "8 34% 12%",
"card": "20 28% 91%",
"cardForeground": "8 34% 12%",
"popover": "20 28% 91%",
"popoverForeground": "8 34% 12%",
"primary": "4 78% 46%",
"primaryForeground": "20 34% 96%",
"secondary": "24 90% 48%",
"secondaryForeground": "20 34% 96%",
"muted": "20 22% 88%",
"mutedForeground": "8 18% 38%",
"accent": "40 92% 46%",
"accentForeground": "20 34% 96%",
"destructive": "0 80% 42%",
"destructiveForeground": "20 34% 96%",
"border": "20 20% 82%",
"input": "20 20% 82%",
"ring": "4 78% 46%"
},
"darkColors": {
"background": "0 0% 2%",
"foreground": "0 0% 92%",
"card": "0 0% 5%",
"cardForeground": "0 0% 92%",
"popover": "0 0% 5%",
"popoverForeground": "0 0% 92%",
"primary": "0 80% 50%",
"primaryForeground": "0 0% 100%",
"secondary": "35 95% 55%",
"secondaryForeground": "0 0% 0%",
"muted": "0 0% 10%",
"mutedForeground": "0 15% 55%",
"accent": "35 95% 55%",
"accentForeground": "0 0% 0%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 98%",
"border": "0 30% 18%",
"input": "0 30% 18%",
"ring": "0 80% 50%"
},
"mode": "dark"
"background": "0 12% 6%",
"foreground": "20 30% 92%",
"card": "0 12% 11%",
"cardForeground": "20 30% 92%",
"popover": "0 12% 11%",
"popoverForeground": "20 30% 92%",
"primary": "4 86% 60%",
"primaryForeground": "0 12% 6%",
"secondary": "24 100% 58%",
"secondaryForeground": "0 12% 6%",
"muted": "0 8% 16%",
"mutedForeground": "20 14% 66%",
"accent": "42 98% 60%",
"accentForeground": "0 12% 6%",
"destructive": "0 84% 62%",
"destructiveForeground": "0 12% 6%",
"border": "0 8% 20%",
"input": "0 8% 22%",
"ring": "4 86% 60%"
}
}
},
{
"id": "sickbay",
"name": "Sickbay",
"description": "Medical operations — teal, cyan, and blue on black",
"id": "blue-console",
"name": "Blue Console",
"description": "24th-century sciences layout. Periwinkle blue and cyan panels with an orange action channel over black. Light mode is a bright bridge console.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:lcars:Antonio",
"fontBody": "template:lcars:Antonio",
"fontMono": "template:lcars:Share Tech Mono"
},
"lightColors": {
"background": "0 0% 2%",
"foreground": "180 20% 90%",
"card": "0 0% 5%",
"cardForeground": "180 20% 90%",
"popover": "0 0% 5%",
"popoverForeground": "180 20% 90%",
"primary": "174 72% 50%",
"primaryForeground": "0 0% 0%",
"secondary": "199 80% 55%",
"secondaryForeground": "0 0% 0%",
"muted": "0 0% 10%",
"mutedForeground": "180 15% 50%",
"accent": "199 80% 55%",
"accentForeground": "0 0% 0%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 98%",
"border": "180 15% 15%",
"input": "180 15% 15%",
"ring": "174 72% 50%"
"background": "215 34% 96%",
"foreground": "220 32% 14%",
"card": "215 28% 92%",
"cardForeground": "220 32% 14%",
"popover": "215 28% 92%",
"popoverForeground": "220 32% 14%",
"primary": "222 62% 48%",
"primaryForeground": "215 34% 97%",
"secondary": "190 70% 40%",
"secondaryForeground": "215 34% 97%",
"muted": "215 22% 89%",
"mutedForeground": "220 16% 38%",
"accent": "28 92% 50%",
"accentForeground": "215 34% 97%",
"destructive": "2 74% 46%",
"destructiveForeground": "215 34% 97%",
"border": "215 20% 83%",
"input": "215 20% 83%",
"ring": "222 62% 48%"
},
"darkColors": {
"background": "0 0% 2%",
"foreground": "180 20% 90%",
"card": "0 0% 5%",
"cardForeground": "180 20% 90%",
"popover": "0 0% 5%",
"popoverForeground": "180 20% 90%",
"primary": "174 72% 50%",
"primaryForeground": "0 0% 0%",
"secondary": "199 80% 55%",
"secondaryForeground": "0 0% 0%",
"muted": "0 0% 10%",
"mutedForeground": "180 15% 50%",
"accent": "199 80% 55%",
"accentForeground": "0 0% 0%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 98%",
"border": "180 15% 15%",
"input": "180 15% 15%",
"ring": "174 72% 50%"
},
"mode": "dark"
"background": "222 20% 7%",
"foreground": "210 28% 92%",
"card": "222 20% 12%",
"cardForeground": "210 28% 92%",
"popover": "222 20% 12%",
"popoverForeground": "210 28% 92%",
"primary": "220 78% 66%",
"primaryForeground": "222 20% 7%",
"secondary": "188 78% 52%",
"secondaryForeground": "222 20% 7%",
"muted": "222 16% 17%",
"mutedForeground": "210 16% 66%",
"accent": "30 100% 60%",
"accentForeground": "222 20% 7%",
"destructive": "0 80% 60%",
"destructiveForeground": "222 20% 7%",
"border": "222 14% 22%",
"input": "222 14% 24%",
"ring": "220 78% 66%"
}
}
},
{
"id": "engineering",
"name": "Engineering",
"description": "Engineering deck — orange, amber, and tan on black",
"id": "nebula",
"name": "Nebula",
"description": "Deep-space media suite. Lilac and magenta panels with a peach action channel over indigo-black. Light mode keeps the nebula hues on soft violet paper.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:lcars:Antonio",
"fontBody": "template:lcars:Antonio",
"fontMono": "template:lcars:Share Tech Mono"
},
"lightColors": {
"background": "0 0% 2%",
"foreground": "35 30% 88%",
"card": "0 0% 5%",
"cardForeground": "35 30% 88%",
"popover": "0 0% 5%",
"popoverForeground": "35 30% 88%",
"primary": "25 95% 55%",
"primaryForeground": "0 0% 0%",
"secondary": "40 80% 60%",
"secondaryForeground": "0 0% 0%",
"muted": "0 0% 10%",
"mutedForeground": "30 20% 50%",
"accent": "33 65% 70%",
"accentForeground": "0 0% 0%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 98%",
"border": "25 20% 16%",
"input": "25 20% 16%",
"ring": "25 95% 55%"
"background": "280 30% 96%",
"foreground": "278 30% 14%",
"card": "280 24% 92%",
"cardForeground": "278 30% 14%",
"popover": "280 24% 92%",
"popoverForeground": "278 30% 14%",
"primary": "270 56% 50%",
"primaryForeground": "280 30% 97%",
"secondary": "322 66% 48%",
"secondaryForeground": "280 30% 97%",
"muted": "280 20% 89%",
"mutedForeground": "278 16% 40%",
"accent": "22 88% 54%",
"accentForeground": "280 30% 97%",
"destructive": "2 74% 46%",
"destructiveForeground": "280 30% 97%",
"border": "280 18% 83%",
"input": "280 18% 83%",
"ring": "270 56% 50%"
},
"darkColors": {
"background": "0 0% 2%",
"foreground": "35 30% 88%",
"card": "0 0% 5%",
"cardForeground": "35 30% 88%",
"popover": "0 0% 5%",
"popoverForeground": "35 30% 88%",
"primary": "25 95% 55%",
"primaryForeground": "0 0% 0%",
"secondary": "40 80% 60%",
"secondaryForeground": "0 0% 0%",
"muted": "0 0% 10%",
"mutedForeground": "30 20% 50%",
"accent": "33 65% 70%",
"accentForeground": "0 0% 0%",
"destructive": "0 85% 55%",
"destructiveForeground": "0 0% 98%",
"border": "25 20% 16%",
"input": "25 20% 16%",
"ring": "25 95% 55%"
"background": "268 24% 7%",
"foreground": "285 26% 92%",
"card": "268 24% 12%",
"cardForeground": "285 26% 92%",
"popover": "268 24% 12%",
"popoverForeground": "285 26% 92%",
"primary": "268 74% 74%",
"primaryForeground": "268 24% 7%",
"secondary": "322 82% 64%",
"secondaryForeground": "268 24% 7%",
"muted": "268 18% 17%",
"mutedForeground": "285 16% 66%",
"accent": "24 96% 64%",
"accentForeground": "268 24% 7%",
"destructive": "0 80% 62%",
"destructiveForeground": "268 24% 7%",
"border": "268 16% 22%",
"input": "268 16% 24%",
"ring": "268 74% 74%"
}
}
},
{
"id": "medbay-green",
"name": "Medbay Green",
"description": "Sickbay diagnostic layout. Teal and green panels with an amber alert channel over black. Light mode is a clean daylight medbay readout.",
"theme": {
"mode": "both",
"typography": {
"fontHeading": "template:lcars:Antonio",
"fontBody": "template:lcars:Antonio",
"fontMono": "template:lcars:Share Tech Mono"
},
"mode": "dark"
"lightColors": {
"background": "150 26% 96%",
"foreground": "165 30% 12%",
"card": "150 22% 92%",
"cardForeground": "165 30% 12%",
"popover": "150 22% 92%",
"popoverForeground": "165 30% 12%",
"primary": "168 62% 34%",
"primaryForeground": "150 26% 97%",
"secondary": "142 58% 40%",
"secondaryForeground": "150 26% 97%",
"muted": "150 18% 89%",
"mutedForeground": "165 16% 38%",
"accent": "40 92% 48%",
"accentForeground": "150 26% 97%",
"destructive": "2 74% 46%",
"destructiveForeground": "150 26% 97%",
"border": "150 16% 83%",
"input": "150 16% 83%",
"ring": "168 62% 34%"
},
"darkColors": {
"background": "168 18% 6%",
"foreground": "150 24% 92%",
"card": "168 18% 11%",
"cardForeground": "150 24% 92%",
"popover": "168 18% 11%",
"popoverForeground": "150 24% 92%",
"primary": "168 72% 52%",
"primaryForeground": "168 18% 6%",
"secondary": "142 66% 56%",
"secondaryForeground": "168 18% 6%",
"muted": "168 14% 16%",
"mutedForeground": "150 14% 66%",
"accent": "42 98% 60%",
"accentForeground": "168 18% 6%",
"destructive": "0 80% 60%",
"destructiveForeground": "168 18% 6%",
"border": "168 12% 21%",
"input": "168 12% 23%",
"ring": "168 72% 52%"
}
}
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 984 KiB

View File

@ -1,50 +0,0 @@
package main
import (
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/templates"
"git.dev.alexdunmow.com/block/core/templates/pongo"
)
var engine = pongo.NewEngine(TemplatesFS(), "/templates/lcars/style.css")
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "lcars",
Title: "LCARS",
Description: "Star Trek LCARS computer interface theme",
})
if err := tr.RegisterPageTemplate("lcars", templates.PageTemplateMeta{
Key: "default",
Title: "Standard Display",
Description: "Classic LCARS layout with sidebar and header frame",
Slots: []string{"header", "sidebar", "main", "footer"},
}, engine.MustPageTemplate("default.html")); err != nil {
return err
}
if err := tr.RegisterPageTemplate("lcars", templates.PageTemplateMeta{
Key: "full-display",
Title: "Full Display",
Description: "Full-width LCARS display without sidebar",
Slots: []string{"header", "main", "footer"},
}, engine.MustPageTemplate("full_display.html")); err != nil {
return err
}
if err := br.LoadSchemasFromFS(SchemasFS()); err != nil {
return err
}
br.Register(LCARSHeaderMeta, engine.MustBlockTemplateWithDefaults("blocks/header.html", headerDefaults))
br.Register(LCARSSidebarMeta, engine.MustBlockTemplateWithDefaults("blocks/sidebar.html", sidebarDefaults))
br.Register(LCARSPanelMeta, engine.MustBlockTemplate("blocks/panel.html"))
br.RegisterTemplateOverride("lcars", "heading", engine.MustTemplateOverride("blocks/heading_override.html"))
br.RegisterTemplateOverride("lcars", "text", engine.MustTemplateOverride("blocks/text_override.html"))
tr.RegisterEmailWrapper("lcars", engine.MustEmailWrapper("email_wrapper.html"))
return nil
}

View File

@ -1,24 +0,0 @@
package main
import (
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
var Registration = plugin.PluginRegistration{
Name: "lcars",
Version: plugin.ParseModVersion(pluginModBytes),
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
return Register(tr, br)
},
Assets: func() http.Handler { return AssetsHandler() },
Schemas: func() fs.FS { return SchemasFS() },
ThemePresets: func() []byte { return ThemePresets() },
BundledFonts: func() []byte { return BundledFonts() },
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
}

View File

@ -1,41 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LCARS Header",
"description": "Top bar with elbow bracket, status indicators, and stardate",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"description": "Main display title",
"x-editor": "text"
},
"subtitle": {
"type": "string",
"title": "Subtitle",
"description": "Secondary text below the title",
"x-editor": "text"
},
"stardate": {
"type": "string",
"title": "Stardate",
"description": "Stardate display (leave empty for auto)",
"x-editor": "text"
},
"status": {
"type": "string",
"title": "Status",
"description": "System status indicator",
"enum": ["online", "standby", "alert", "offline"],
"default": "online",
"x-editor": "select"
},
"menu_name": {
"type": "string",
"title": "Menu Name",
"description": "Navigation menu to display",
"x-editor": "text"
}
},
"required": ["title"]
}

View File

@ -1,29 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LCARS Panel",
"description": "Framed content area with LCARS border treatment",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Panel Title",
"description": "Title shown in the panel header bar",
"x-editor": "text"
},
"content": {
"type": "string",
"title": "Content",
"description": "Panel body content (HTML)",
"x-editor": "richtext"
},
"border_color": {
"type": "string",
"title": "Border Color",
"description": "Which theme color for the panel border",
"enum": ["primary", "secondary", "accent"],
"default": "primary",
"x-editor": "select"
}
},
"required": ["title"]
}

View File

@ -1,33 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LCARS Sidebar",
"description": "Left panel with rounded-rectangle navigation buttons",
"type": "object",
"properties": {
"items": {
"type": "array",
"title": "Navigation Items",
"description": "List of sidebar navigation buttons",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"title": "Label"
},
"url": {
"type": "string",
"title": "URL"
},
"color": {
"type": "string",
"title": "Color",
"enum": ["primary", "secondary", "accent"],
"default": "primary"
}
},
"required": ["label", "url"]
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

BIN
seed/media/console.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
seed/media/deep-space.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
seed/media/milky-way.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

BIN
seed/media/nebula.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

BIN
seed/media/starfield.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

129
seed/seed.json Normal file
View File

@ -0,0 +1,129 @@
{
"demo": true,
"settings": {
"merge": {
"site_name": "Subspace Signal",
"tagline": "A weekly transmission on science fiction and the future."
}
},
"media": [
{ "id": "435207dd-8607-4fe6-b8fa-fba0c7486e9f", "file": "media/nebula.jpg", "alt": "The Lagoon Nebula glowing violet and gold in deep space" },
{ "id": "74eb0f23-fb2b-419a-ab3e-06410aa71d64", "file": "media/milky-way.jpg", "alt": "The Milky Way arcing over a dark horizon" },
{ "id": "5d6beec6-a06c-4af7-b1fe-48869f09cef3", "file": "media/starfield.jpg", "alt": "A dense field of stars across the night sky" },
{ "id": "78c7e990-2682-4b39-acb4-cb7868e36e41", "file": "media/deep-space.jpg", "alt": "A distant world against the depth of the cosmos" },
{ "id": "b1a9259a-919f-4ac7-855f-315f295a6536", "file": "media/console.jpg", "alt": "A futuristic spaceship control console lit in amber" },
{ "id": "68341d09-4984-4a9c-ba91-f4005bcb52fd", "file": "media/control-buttons.jpg", "alt": "Illuminated control-panel buttons on a bridge console" }
],
"data_tables": [
{
"key": "hail_signals",
"name": "Hail signals",
"description": "Messages sent from the Subspace Signal contact console.",
"primary_key": "id",
"columns": [
{ "key": "name", "label": "Name", "type": "text", "required": true },
{ "key": "email", "label": "Email", "type": "email", "required": true },
{ "key": "subject", "label": "Subject", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"pages": [
{
"slug": "/",
"title": "Subspace Signal",
"template_key": "full-width",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 1, "content": { "template": "split", "mediaPosition": "right", "backgroundImage": "435207dd-8607-4fe6-b8fa-fba0c7486e9f", "eyebrow": "Now broadcasting", "headline": "Subspace Signal", "subheadline": "A weekly transmission on science fiction, deep space, and the people building the future. Two hosts. One open channel.", "ctaPrimary": { "text": "Listen now", "url": "/episodes" }, "ctaSecondary": { "text": "Meet the crew", "url": "/about" } } },
{ "block_key": "lcars:episode_console", "title": "Featured episode", "slot": "main", "sort_order": 2, "content": { "eyebrow": "Latest transmission", "title": "First contact was a podcast", "cover": "74eb0f23-fb2b-419a-ab3e-06410aa71d64", "episodeNumber": "048", "duration": "58:12", "stardate": "78412.3", "description": "We open the season with a simple question. If a signal reached us tomorrow, who would answer, and what would we say back.", "listenUrl": "/episodes/first-contact", "listenLabel": "Listen now", "platforms": [ { "label": "Apple", "url": "/episodes" }, { "label": "Spotify", "url": "/episodes" }, { "label": "RSS", "url": "/episodes" } ] } },
{ "block_key": "stats", "title": "Signal log", "slot": "main", "sort_order": 3, "content": { "heading": "Signal log", "columns": 3, "countUp": false, "items": [ { "value": "48", "label": "Episodes aired" }, { "value": "4", "label": "Seasons on air" }, { "value": "26", "label": "Star systems covered" } ] } },
{ "block_key": "lcars:crew_roster", "title": "Crew", "slot": "main", "sort_order": 4, "content": { "eyebrow": "On the bridge", "heading": "Your crew", "members": [ { "name": "Vega Okonkwo", "role": "Host", "callsign": "Bridge-1", "bio": "Reads too much hard science fiction and asks the questions engineers dread." }, { "name": "Rell Tavras", "role": "Co-host", "callsign": "Science-2", "bio": "Keeps the physics honest and the metaphors from getting away from us." } ] } },
{ "block_key": "cta", "title": "Closing CTA", "slot": "main", "sort_order": 5, "content": { "background": "band", "heading": "Tune the frequency", "text": "New transmission every week. Subscribe on your channel of choice.", "primaryLabel": "Subscribe", "primaryUrl": "/episodes" } }
]
},
{
"slug": "/about",
"title": "About",
"template_key": "default",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "About the signal", "level": 1 } },
{ "block_key": "text", "title": "Intro", "slot": "main", "sort_order": 2, "content": { "text": "<p>Subspace Signal is a weekly show about science fiction and the real science it borrows from. We read the books, watch the shows, and talk to the people who build the future for a living. Every episode is one open channel between two hosts and you.</p>" } },
{ "block_key": "image", "title": "Bridge console", "slot": "main", "sort_order": 3, "content": { "src": "68341d09-4984-4a9c-ba91-f4005bcb52fd", "alt": "Illuminated control-panel buttons on a bridge console", "caption": "The Subspace Signal recording console, Deck 12.", "align": "center" } },
{ "block_key": "lcars:transmission_status", "title": "Systems", "slot": "main", "sort_order": 4, "content": { "eyebrow": "Console", "heading": "Channel status", "systems": [ { "label": "Weekly uplink", "status": "online", "detail": "Every Tuesday" }, { "label": "Back catalogue", "status": "online", "detail": "48 episodes" }, { "label": "Listener comms", "status": "standby", "detail": "Open for hails" }, { "label": "Live recordings", "status": "standby", "detail": "Next season" } ] } },
{ "block_key": "lcars:stardate_log", "title": "Log", "slot": "main", "sort_order": 5, "content": { "eyebrow": "Ship's log", "heading": "How we got here", "entries": [ { "stardate": "74001.0", "title": "Signal first opened", "text": "Two friends started recording arguments about warp drives in a spare room." }, { "stardate": "76210.5", "title": "First guest aboard", "text": "A working propulsion engineer joined to explain why the arguments were wrong." }, { "stardate": "78412.3", "title": "Season four begins", "text": "New gear, same open channel, more star systems to cover." } ] } }
]
},
{
"slug": "/episodes",
"title": "Episodes",
"template_key": "blog-index",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "card", "title": "Episode 48", "slot": "main", "sort_order": 1, "content": { "title": "First contact was a podcast", "media": "78c7e990-2682-4b39-acb4-cb7868e36e41", "text": "<p>If a signal reached us tomorrow, who would answer, and what would we say back.</p>", "link": "/episodes/first-contact", "linkLabel": "Listen" } },
{ "block_key": "card", "title": "Episode 47", "slot": "main", "sort_order": 2, "content": { "title": "The physics of warp, honestly", "media": "5d6beec6-a06c-4af7-b1fe-48869f09cef3", "text": "<p>A propulsion engineer walks us through what the math actually allows.</p>", "link": "/episodes/warp-honestly", "linkLabel": "Listen" } },
{ "block_key": "card", "title": "Episode 46", "slot": "main", "sort_order": 3, "content": { "title": "Terraforming on a budget", "media": "b1a9259a-919f-4ac7-855f-315f295a6536", "text": "<p>What it would really take to make one small world a little more livable.</p>", "link": "/episodes/terraforming", "linkLabel": "Listen" } }
]
},
{
"slug": "/episodes/first-contact",
"parent_slug": "/episodes",
"title": "First contact was a podcast",
"template_key": "article",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "First contact was a podcast", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Stardate 78412.3. We open the season with a simple question, the kind that sounds small until you try to answer it out loud. If a signal reached us tomorrow, who would answer, and what would we say back? The honest answer is that nobody is really in charge of the reply, and that is where this whole transmission begins.</p><p>Start with the receiving end. There is no single red phone that rings when the universe calls. A confirmed detection would arrive not as a voice but as a pattern buried in noise, checked and rechecked across observatories on three continents before anyone dared say the word out loud. By the time it was real, it would already have passed through a hundred hands, and not one of them would have the authority to speak for the planet.</p><p>So the argument that fills the first act of tonight's episode is not about aliens at all. It is about us. Who gets to draft humanity's opening line? A committee? A head of state? The astronomer who happened to be on shift? We walk through the protocols that actually exist on paper, and then we walk through how quickly those protocols would dissolve the moment a real signal turned the abstract into the urgent.</p><p>Rell makes the case that the first message should be pure mathematics. Prime numbers, the value of pi, the hydrogen line. Nothing that could be misread, nothing that carries the baggage of a culture or a century. A handshake in the only language two strangers across a galaxy could possibly share. It is a clean idea, and for about four minutes it is winning.</p><p>Then Vega asks the question that undoes it. If the whole point of answering is to say something true about who we are, why would we lead with the one part of ourselves that looks identical to every other species that can count? Mathematics proves we are here. It says nothing about what it is like to be here. And maybe, she argues, that second thing is the message.</p><p>We bring in a linguist who has spent a career thinking about first contact between human languages that share no common root. Her warning is sobering. Every act of translation smuggles in assumptions, she says, and the assumptions you cannot see are the ones that hurt you. The first message is not a greeting. It is a lens, and whoever builds it decides what the other side will see of us first.</p><p>That reframes the engineering problem entirely. Bandwidth stops being the constraint. Intent becomes the constraint. You are not trying to transmit the most information. You are trying to transmit the least misleading version of an entire civilization in a package small enough to survive the crossing intact.</p><p>Halfway through, we do the exercise ourselves. Live, unscripted, we each try to write three lines we would put at the front of the reply. It is harder than it sounds, and both attempts are worse than we expected. Every draft either flatters us or apologizes for us. Nobody writes the plain, unhurried truth on the first try, which turns out to be the most human result of all.</p><p>What we keep circling back to is patience. A conversation across light-years is measured in lifetimes. You will not hear back. Your grandchildren might. That changes the character of the message from a statement into an inheritance, something written to be finished by people you will never meet, in a context you cannot predict.</p><p>We close the way we opened, with the question turned around. Not what would we say to them, but what do we wish someone had said to us, early, before we knew enough to be afraid of the answer. That is the message. That is what we would send. And if the channel stays quiet for another thousand years, at least we will have spent the wait learning how to speak plainly to each other.</p><p>Next week we hand the microphone to a working propulsion engineer and ask him to break our hearts about warp drive. Until then, this is Subspace Signal, and the channel stays open.</p>" } }
]
},
{
"slug": "/episodes/warp-honestly",
"parent_slug": "/episodes",
"title": "The physics of warp, honestly",
"template_key": "article",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "The physics of warp, honestly", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Stardate 78295.1. Warp drive makes for a great story and a hard equation. This week we asked a working propulsion engineer to walk us through what the math actually allows, and where the fiction quietly cheats to keep the plot moving.</p><p>The short version is that the energy budget is the whole story. Everything else is set dressing until you solve for that. Bend spacetime enough to matter and the numbers you get back are not large in the everyday sense. They are large in the sense that they compare to the output of stars.</p><p>Our guest is generous about the fiction. He does not want warp drive to be impossible, and he is careful to separate what is forbidden by physics from what is merely absurd by engineering. The first list is short. The second list is long, and it is where the honest heartbreak lives.</p><p>We spend a while on exotic matter, the placeholder every warp metric leans on to make the equations behave. It is not a material you could ever put in a crate. It is a property the math demands and the universe has never once been observed to provide at the scale a ship would need.</p><p>By the end, nobody at the table wants to give up the dream. We just want to hold it honestly. The stars are not close, the shortcuts are not free, and the wanting is the most real part of the whole enterprise. That, at least, the math cannot take away from us.</p>" } }
]
},
{
"slug": "/episodes/terraforming",
"parent_slug": "/episodes",
"title": "Terraforming on a budget",
"template_key": "article",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Terraforming on a budget", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Stardate 78190.7. Making a whole planet livable is a project measured in centuries. So this week we asked a smaller, more honest question. What is the cheapest first step that would make one small world a little more forgiving to the people who came to stay?</p><p>The answer is not domes or rockets. It is patience, pressure, and a very long supply of the right microbes. The glamorous engineering comes much later, if it comes at all. The early work is closer to gardening than to conquest.</p><p>Our guest, a planetary scientist who models atmospheres for a living, keeps pulling us back to the timescales. Nothing about this happens in a human career. You plant a process and you hand it to people who are not born yet, and you trust them to keep the thermostat pointed the right way.</p><p>We talk about the ethics of it too. A world you are slowly changing might already have something living on it, quiet and slow and easy to miss. The budget that matters most, she argues, is not measured in credits. It is measured in how carefully you look before you begin.</p><p>We end where terraforming actually starts, with a single greenhouse and a stubborn strain of algae. Small, unglamorous, and real. The rest is just that same patience, repeated for a thousand years, until one morning the air outside is one degree kinder than it was.</p>" } }
]
},
{
"slug": "/contact",
"title": "Hail us",
"template_key": "contact",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "contact-form", "title": "Hail console", "slot": "main", "sort_order": 1, "content": { "heading": "Open a channel", "description": "Send a hail. We read every signal and reply within a few days.", "submitLabel": "Transmit", "successMessage": "Signal received. We will reply on this channel shortly.", "formConfig": { "targetTable": "hail_signals", "enabled": true, "captchaEnabled": false }, "fields": [ { "name": "name", "label": "Name", "type": "text", "required": true, "width": "half" }, { "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" }, { "name": "subject", "label": "Subject", "type": "text", "required": false, "width": "full" }, { "name": "message", "label": "Message", "type": "textarea", "required": true, "width": "full" } ] } },
{ "block_key": "contact-card", "title": "Direct channels", "slot": "aside", "sort_order": 1, "content": { "heading": "Direct channels", "columns": 1, "channels": [ { "icon": "lucide:mail", "label": "Email", "value": "hail@subspace.example", "link": "mailto:hail@subspace.example" }, { "icon": "lucide:radio", "label": "Studio line", "value": "+61 2 5550 0148", "link": "tel:+61255500148" } ] } },
{ "block_key": "hours-location", "title": "Broadcast hours", "slot": "aside", "sort_order": 2, "content": { "heading": "Broadcast hours", "address": "Deck 12, Signal Studios, Sydney NSW 2000", "hours": [ { "day": "New episode", "hours": "Tuesdays 06:00" }, { "day": "Listener comms", "hours": "Monday to Friday 09:00 to 17:00" }, { "day": "Weekend", "closed": true } ] } }
]
},
{
"slug": "/login",
"title": "Access terminal",
"template_key": "auth",
"reconcile_blocks": true,
"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": "Episodes", "page_slug": "/episodes", "sort_order": 3 },
{ "menu": "main", "label": "Hail us", "page_slug": "/contact", "sort_order": 4 }
]
}

19
seed/workflows.json Normal file
View File

@ -0,0 +1,19 @@
{
"workflows": [
{
"key": "hail_notify_admins",
"name": "Notify admins on hail signal",
"description": "Emails site admins whenever the Subspace Signal contact console receives a hail.",
"enabled": true,
"trigger": { "type": "row_inserted", "table": "hail_signals" },
"steps": [
{
"type": "email",
"name": "Email admins",
"template": "Form Submission Notification",
"recipients": { "mode": "admins", "scope": "all" }
}
]
}
]
}

View File

@ -1,23 +0,0 @@
<header class="lcars-header">
<div class="lcars-header-bar">
<div class="lcars-header-pill lcars-bg-primary"></div>
<div class="lcars-header-title-area">
<h1 class="lcars-title">{{ title }}</h1>
{% if subtitle %}
<span class="lcars-subtitle">{{ subtitle }}</span>
{% endif %}
</div>
<div class="lcars-header-indicators">
{% if stardate %}
<span class="lcars-stardate">SD {{ stardate }}</span>
{% endif %}
<span class="lcars-status lcars-status-{{ status|default:"online" }}">
{{ status|default:"online"|upper }}
</span>
</div>
<div class="lcars-header-pill lcars-bg-secondary"></div>
</div>
{% if ctx.isEditor %}
<div class="lcars-editor-badge">EDIT MODE</div>
{% endif %}
</header>

View File

@ -1 +0,0 @@
<{{ tag|default:"h2" }} class="lcars-heading lcars-heading-{{ level|default:2 }}">{{ text }}</{{ tag|default:"h2" }}>

View File

@ -1,14 +0,0 @@
<div class="lcars-panel lcars-panel-{{ border_color|default:"primary" }}">
{% if title %}
<div class="lcars-panel-header">
<div class="lcars-panel-header-pill"></div>
<span class="lcars-panel-title">{{ title }}</span>
<div class="lcars-panel-header-bar"></div>
</div>
{% endif %}
<div class="lcars-panel-body">
{% if content %}
{{ content|safe }}
{% endif %}
</div>
</div>

View File

@ -1,11 +0,0 @@
<nav class="lcars-sidebar">
{% for item in items %}
<a href="{{ item.url }}" class="lcars-sidebar-btn lcars-bg-{{ item.color|default:"primary" }}">
<span class="lcars-sidebar-btn-label">{{ item.label }}</span>
</a>
{% empty %}
<div class="lcars-sidebar-btn lcars-bg-muted">
<span class="lcars-sidebar-btn-label">No items</span>
</div>
{% endfor %}
</nav>

View File

@ -1 +0,0 @@
<div class="lcars-prose">{{ html|safe }}</div>

View File

@ -1,45 +0,0 @@
{% extends "base.html" %}
{% block body_class %}lcars-page lcars-default bg-background text-foreground antialiased min-h-screen{% endblock %}
{% block body %}
<div class="lcars-frame">
<div class="lcars-header-area">
{{ slots.header|safe }}
</div>
<div class="lcars-body">
<div class="lcars-elbow-top">
<div class="lcars-elbow-corner lcars-elbow-tl"></div>
<div class="lcars-bar lcars-bar-top"></div>
</div>
<div class="lcars-content-area">
<aside class="lcars-sidebar-area">
{{ slots.sidebar|safe }}
</aside>
<div class="lcars-divider"></div>
<main class="lcars-main-area">
{% if slots.main %}
{{ slots.main|safe }}
{% else %}
<div class="lcars-empty-state">
<p class="text-muted-foreground uppercase tracking-widest">No data on file</p>
</div>
{% endif %}
</main>
</div>
<div class="lcars-elbow-bottom">
<div class="lcars-elbow-corner lcars-elbow-bl"></div>
<div class="lcars-bar lcars-bar-bottom"></div>
</div>
</div>
<footer class="lcars-footer-area">
{{ slots.footer|safe }}
</footer>
</div>
{% endblock %}

View File

@ -0,0 +1,66 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ site_name }}</title>
<style type="text/css">
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
@media only screen and (max-width: 620px) {
.lcars-email-container { width: 100% !important; max-width: 100% !important; }
.lcars-email-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style="background-color: #0b0b10; margin: 0; padding: 0; font-family: 'Antonio', 'Arial Narrow', Arial, sans-serif;">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="padding: 32px 12px; background-color: #0b0b10;">
<table role="presentation" class="lcars-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; width: 600px; background-color: #14141b; border-collapse: separate; border-radius: 10px; overflow: hidden;">
<!-- Header: dark bar + uppercase title -->
<tr>
<td class="lcars-email-padding" style="padding: 28px 48px 12px 48px; background-color: #14141b;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr>
<td style="width: 56px;"><div style="width: 48px; height: 20px; background-color: #ff8c1a; border-radius: 10px; line-height: 20px; font-size: 0;">&nbsp;</div></td>
<td>{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 34px; width: auto; display: block;">{% elif site_name %}<span style="font-size: 22px; font-weight: 700; text-transform: uppercase; letter-spacing: 3px; color: #ff8c1a; font-family: 'Antonio', 'Arial Narrow', Arial, sans-serif;">{{ site_name }}</span>{% endif %}</td>
</tr></table>
</td>
</tr>
<!-- LCARS tri-colour bar as solid table cells -->
<tr>
<td style="padding: 8px 48px 0 48px;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr>
<td style="width: 34%; height: 6px; background-color: #ff8c1a; line-height: 6px; font-size: 0;">&nbsp;</td>
<td style="width: 4px; font-size: 0; line-height: 6px;">&nbsp;</td>
<td style="width: 33%; height: 6px; background-color: #f9b23c; line-height: 6px; font-size: 0;">&nbsp;</td>
<td style="width: 4px; font-size: 0; line-height: 6px;">&nbsp;</td>
<td style="width: 33%; height: 6px; background-color: #b89ae6; line-height: 6px; font-size: 0;">&nbsp;</td>
</tr></table>
</td>
</tr>
<!-- Body -->
<tr>
<td class="lcars-email-padding" style="padding: 28px 48px; color: #e6ddc9; font-size: 16px; line-height: 1.65; font-family: 'Antonio', 'Arial Narrow', Arial, sans-serif;">{{ body|safe }}</td>
</tr>
<!-- Footer -->
<tr>
<td class="lcars-email-padding" style="padding: 20px 48px 30px 48px; border-top: 2px solid #2a2a34;">
{% if site_name %}<p style="margin: 0 0 6px 0; font-size: 12px; color: #ff8c1a; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; font-family: 'Antonio', 'Arial Narrow', Arial, sans-serif;">{{ site_name }}</p>{% endif %}
<p style="margin: 0 0 12px 0; font-size: 12px; color: #8c8a94; font-family: 'Courier New', monospace; letter-spacing: 1px;">You are receiving this transmission because you subscribed to updates.</p>
{% if site_url %}<p style="margin: 0 0 8px 0; font-size: 12px;"><a href="{{ site_url }}" style="color: #f9b23c; text-decoration: none;">{{ site_url }}</a></p>{% endif %}
{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: #8c8a94; text-decoration: underline;">Unsubscribe</a></p>{% endif %}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
{% if preview_text %}
<span style="display:none;max-height:0;overflow:hidden;">{{ preview_text }}</span>
{% endif %}
<style>
body { margin: 0; padding: 0; background-color: #050505; color: #e0d4b0; font-family: Arial, Helvetica, sans-serif; }
.lcars-email { max-width: 600px; margin: 0 auto; padding: 20px; }
.lcars-email-header { padding: 16px 20px; }
.lcars-email-header table { width: 100%; }
.lcars-email-pill { width: 80px; height: 32px; border-radius: 16px; background-color: {{ colors.primary }}; }
.lcars-email-title { font-size: 20px; font-weight: bold; text-transform: uppercase; letter-spacing: 3px; color: {{ colors.primary }}; padding-left: 12px; }
.lcars-email-bar { height: 4px; border-radius: 2px; margin-bottom: 24px; }
.lcars-email-body { padding: 0 20px; line-height: 1.6; font-size: 16px; }
.lcars-email-body a { color: {{ colors.secondary }}; text-decoration: underline; }
.lcars-email-footer { margin-top: 32px; padding-top: 16px; border-top: 2px solid {{ colors.border }}; font-size: 12px; text-transform: uppercase; letter-spacing: 2px; opacity: 0.6; text-align: center; }
.lcars-email-footer a { color: {{ colors.secondary }}; text-decoration: underline; }
</style>
</head>
<body>
<div class="lcars-email">
<div class="lcars-email-header">
<table><tr>
<td style="width:80px;"><div class="lcars-email-pill"></div></td>
<td class="lcars-email-title">{{ site_name }}</td>
</tr></table>
</div>
<div class="lcars-email-bar" style="background: linear-gradient(to right, {{ colors.primary }} 0%, {{ colors.primary }} 30%, {{ colors.secondary }} 30%, {{ colors.secondary }} 60%, {{ colors.muted }} 60%, {{ colors.muted }} 100%);"></div>
<div class="lcars-email-body">
{{ body|safe }}
</div>
<div class="lcars-email-footer">
{{ site_name }}{% if unsubscribe_url %} · <a href="{{ unsubscribe_url }}">Unsubscribe</a>{% endif %}
</div>
</div>
</body>
</html>

View File

@ -1,31 +0,0 @@
{% extends "base.html" %}
{% block body_class %}lcars-page lcars-full-display bg-background text-foreground antialiased min-h-screen{% endblock %}
{% block body %}
<div class="lcars-frame lcars-frame-full">
<div class="lcars-header-area">
{{ slots.header|safe }}
</div>
<div class="lcars-body lcars-body-full">
<div class="lcars-bar-accent"></div>
<main class="lcars-main-area lcars-main-full">
{% if slots.main %}
{{ slots.main|safe }}
{% else %}
<div class="lcars-empty-state">
<p class="text-muted-foreground uppercase tracking-widest">No data on file</p>
</div>
{% endif %}
</main>
<div class="lcars-bar-accent"></div>
</div>
<footer class="lcars-footer-area">
{{ slots.footer|safe }}
</footer>
</div>
{% endblock %}

View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<div class="lcars-frame" style="flex:1 1 auto;">
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
<main class="lcars-well" style="flex:1 1 auto;padding-top:1.5rem;padding-bottom:2.5rem;" data-content-well>
<article class="lcars-measure lcars-panel" style="margin:0 auto;">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout">No transcript on file.</p>{% endif %}</article>
{% if slots.aside %}<aside class="lcars-measure" style="margin:2rem auto 0;border-top:0.1875rem solid hsl(var(--border));padding-top:1.5rem;">{{ slots.aside|safe }}</aside>{% endif %}
</main>
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
</div>
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<main style="flex:1 1 auto;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:3rem 1.5rem;">
<div style="width:100%;max-width:28rem;">
<div class="lcars-elbow-top" style="height:1.5rem;margin-bottom:0.375rem;"><div class="lcars-elbow-corner lcars-elbow-tl" style="width:7rem;min-width:7rem;"></div><div class="lcars-bar lcars-bar-top"></div></div>
<a href="/" aria-label="{{ site_settings.Title|default:"Home" }}" class="lcars-brand" style="margin-bottom:1.25rem;">{{ site_settings.Title|default:"LCARS" }}</a>
<div class="lcars-panel lcars-reveal" style="padding:1.75rem;">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout">No access console assigned.</p>{% endif %}
</div>
{% if slots.footer %}<div class="lcars-form-note" style="text-align:center;margin-top:1.25rem;">{{ slots.footer|safe }}</div>{% endif %}
</div>
</main>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<div class="lcars-frame" style="flex:1 1 auto;">
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
<main class="lcars-well" style="flex:1 1 auto;padding-top:2rem;padding-bottom:2.5rem;" data-content-well>
{% if title %}<div style="border-bottom:0.1875rem solid hsl(var(--primary));padding-bottom:0.75rem;margin-bottom:2rem;"><p class="lcars-eyebrow">Broadcast Log</p><h1 class="lcars-heading lcars-heading-1" style="margin:0;">{{ title }}</h1>{% if page_meta.MetaDescription %}<p class="lcars-lede" style="max-width:60ch;margin:0.75rem 0 0;">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
{% if slots.featured %}<section style="margin-bottom:2.5rem;">{{ slots.featured|safe }}</section>{% endif %}
{% if slots.main %}<section>{{ slots.main|safe }}</section>{% else %}<p class="lcars-readout">No episodes logged yet.</p>{% endif %}
</main>
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
</div>
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<div class="lcars-frame" style="flex:1 1 auto;">
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
<main class="lcars-well" style="flex:1 1 auto;padding-top:2rem;padding-bottom:2.5rem;" data-content-well>
{% if slots.aside %}
<div class="lcars-contact-grid" style="display:grid;gap:2rem;grid-template-columns:1fr;">
<div style="min-width:0;">{{ slots.main|safe }}</div>
<aside class="lcars-contact-aside" style="min-width:0;">{{ slots.aside|safe }}</aside>
</div>
<style>@media (min-width:1024px){.lcars-contact-grid{grid-template-columns:1.4fr 1fr;}}</style>
{% else %}
<div class="lcars-measure">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout">No channel configured.</p>{% endif %}</div>
{% endif %}
</main>
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
</div>
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<div class="lcars-frame" style="flex:1 1 auto;">
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
{% if slots.sidebar %}
<div class="lcars-well lcars-content-row" style="align-items:start;padding-top:0.75rem;padding-bottom:0.75rem;">
<aside class="lcars-rail">{{ slots.sidebar|safe }}</aside>
<main class="lcars-main-panel" style="min-width:0;">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout" style="padding:2rem 0;">No data on file.</p>{% endif %}</main>
</div>
{% else %}
<main class="lcars-well" style="padding-top:0.75rem;padding-bottom:0.75rem;">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout" style="padding:2rem 0;">No data on file.</p>{% endif %}</main>
{% endif %}
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
</div>
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<div class="lcars-frame" style="flex:1 1 auto;">
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
<main style="flex:1 1 auto;width:100%;" data-content-well>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="lcars-well" style="padding-top:3rem;padding-bottom:3rem;"><p class="lcars-readout">No data on file.</p></div>{% endif %}</main>
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
</div>
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<div class="lcars-frame" style="flex:1 1 auto;">
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
<main style="flex:1 1 auto;width:100%;" data-content-well>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="lcars-well" style="padding-top:3rem;padding-bottom:3rem;"><p class="lcars-readout">No data on file.</p></div>{% endif %}</main>
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
</div>
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<div class="lcars-frame" style="flex:1 1 auto;">
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
<section style="width:100%;">{{ slots.hero|safe }}</section>
<main style="flex:1 1 auto;width:100%;" data-content-well>{% if slots.main %}<div class="lcars-well" style="padding-top:2.5rem;padding-bottom:2.5rem;">{{ slots.main|safe }}</div>{% endif %}</main>
<section style="width:100%;">{{ slots.cta|safe }}</section>
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
</div>
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,8 @@
{# announcement-bar override — centered notice; carries data-bn-announcement / data-bn-annkey and a data-bn-ann-dismiss close button so the host dismissal JS binds. #}
{% if variant == "promo" %}{% set annvar = "lcars-announce--promo" %}{% set annsty = "background:hsl(var(--primary));color:hsl(var(--primary-foreground));" %}{% elif variant == "alert" %}{% set annvar = "lcars-announce--alert" %}{% set annsty = "background:hsl(var(--destructive));color:hsl(var(--destructive-foreground));" %}{% else %}{% set annvar = "lcars-announce--info" %}{% set annsty = "background:hsl(var(--accent));color:hsl(var(--accent-foreground));" %}{% endif %}
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="lcars-announce {{ annvar }} {{ class }}" style="{{ annsty }}">
<div class="lcars-content-well lcars-announce-row" style="position:relative;display:flex;align-items:center;justify-content:center;gap:0.75rem;padding-top:0.5rem;padding-bottom:0.5rem;font-size:0.875rem;text-align:center;">
<p style="margin:0;font-weight:500;">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">{{ linkText }}</a>{% endif %}</p>
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="lcars-announce-close" style="position:absolute;right:0.75rem;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;padding:0.25rem;background:none;border:0;color:inherit;cursor:pointer;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#x"/></svg></button>{% endif %}
</div>
</div>

View File

@ -0,0 +1,14 @@
{# audio-embed override — hairline card row; carries data-bn-audio / data-audio-url / data-audio-media + data-bn-audio-slot so the host facade JS binds. #}
<figure class="lcars-card {{ class }}" data-bn-audio data-audio-url="{{ url|default:'' }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}" style="margin:2rem 0;padding:0;overflow:hidden;">
<div style="display:flex;align-items:center;gap:1rem;padding:1rem;">
{% if artwork %}<div class="lcars-media-frame" style="width:4rem;height:4rem;flex:0 0 auto;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;">{% img artwork alt=title|default:"" class="lcars-audio-art" %}</div>{% endif %}
<div style="min-width:0;flex:1;">
{% if title %}<p class="lcars-display" style="margin:0;font-size:0.9375rem;color:hsl(var(--foreground));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ title }}</p>{% endif %}
{% if artist %}<p style="margin:0;font-size:0.8125rem;color:hsl(var(--muted-foreground));">{{ artist }}</p>{% endif %}
<div data-bn-audio-slot style="display:flex;align-items:center;gap:0.75rem;margin-top:0.5rem;">
<button type="button" data-bn-audio-play class="lcars-btn-primary lcars-btn-sm" aria-label="Play{% if title %}: {{ title }}{% endif %}" style="display:inline-flex;align-items:center;gap:0.375rem;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#play"/></svg> Play</button>
{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="lcars-btn-ghost" style="display:inline-flex;align-items:center;gap:0.25rem;font-size:0.8125rem;color:hsl(var(--muted-foreground));text-decoration:none;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#download"/></svg> Download</a>{% endif %}
</div>
</div>
</div>
</figure><style>.lcars-audio-art{display:block;width:100%;height:100%;object-fit:cover;}</style>

View File

@ -0,0 +1,22 @@
<div class="lcars-stack {{ class }}" data-block="lcars:auth-form">
<input type="radio" name="lcars-auth-tab" id="lcars-auth-login" class="lcars-auth-radio" style="display:none;"{% if default_tab != "register" %} checked{% endif %}>
<input type="radio" name="lcars-auth-tab" id="lcars-auth-register" class="lcars-auth-radio" style="display:none;"{% if default_tab == "register" %} checked{% endif %}>
<div class="lcars-auth-tabs" style="display:flex; gap:1.5rem; border-bottom:1px solid hsl(var(--border));">
<label for="lcars-auth-login" class="lcars-auth-tab lcars-label" style="cursor:pointer; padding:0.75rem 0;">Sign in</label>
<label for="lcars-auth-register" class="lcars-auth-tab lcars-label" style="cursor:pointer; padding:0.75rem 0;">Create account</label>
</div>
<div id="auth-message" role="status" aria-live="polite"></div>
<form class="lcars-auth-panel lcars-auth-panel-login lcars-form" hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-login-email">Email</label><input class="lcars-input" type="email" name="email" id="lcars-login-email" required></div>
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-login-password">Password</label><input class="lcars-input" type="password" name="password" id="lcars-login-password" required></div>
<button type="submit" class="lcars-btn-primary" hx-disabled-elt="this" style="width:100%; justify-content:center;">Sign in</button>
<p class="lcars-form-note" style="text-align:center;"><a href="/forgot-password" style="color: hsl(var(--accent)); text-decoration: underline; text-underline-offset: 2px;">Forgot password?</a></p>
</form>
<form class="lcars-auth-panel lcars-auth-panel-register lcars-form" hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-reg-username">Username</label><input class="lcars-input" type="text" name="username" id="lcars-reg-username" required pattern="[a-zA-Z0-9_-]{3,30}"></div>
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-reg-email">Email</label><input class="lcars-input" type="email" name="email" id="lcars-reg-email" required></div>
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-reg-password">Password</label><input class="lcars-input" type="password" name="password" id="lcars-reg-password" required minlength="8"></div>
<button type="submit" class="lcars-btn-primary" hx-disabled-elt="this" style="width:100%; justify-content:center;">Create account</button>
</form>
<style>.lcars-auth-panel{display:none;} #lcars-auth-login:checked ~ .lcars-auth-panel-login{display:grid;} #lcars-auth-register:checked ~ .lcars-auth-panel-register{display:grid;}</style>
</div>

View File

@ -0,0 +1 @@
{% if context.isPublicLoggedIn %}<div class="lcars-btn-row {{ class }}" data-block="lcars:auth-status" style="align-items: center;"><span class="lcars-mono" style="color: hsl(var(--muted-foreground));">{{ context.publicDisplayName|default:context.publicUsername }}</span><a class="lcars-btn-outline lcars-btn-sm" href="/account">Account</a><a class="lcars-btn-ghost lcars-btn-sm" href="/api/auth/logout">Sign out</a></div>{% else %}<div class="lcars-btn-row {{ class }}" data-block="lcars:auth-status" style="align-items: center;"><a class="lcars-btn-outline lcars-btn-sm" href="/login">Sign in</a><a class="lcars-btn-primary lcars-btn-sm" href="/login?tab=register">Create account</a></div>{% endif %}

View File

@ -0,0 +1 @@
{% if context.currentAuthor %}<section class="lcars-section {{ class }}" data-block="lcars:author-bio-hero"><div class="lcars-content-well"><div class="lcars-measure lcars-stack" style="text-align: center; margin-left: auto; margin-right: auto;">{% if showAvatar and context.currentAuthor.avatarUrl %}<img class="lcars-avatar" src="{{ context.currentAuthor.avatarUrl }}" alt="{{ context.currentAuthor.displayName }}" style="width: 6rem; height: 6rem; border-radius: 9999px; object-fit: cover; margin-left: auto; margin-right: auto;">{% else %}<span class="lcars-mark lcars-mark-ring" aria-hidden="true" style="margin-left: auto; margin-right: auto;"></span>{% endif %}<h1 class="lcars-display" style="margin: 0;">{{ context.currentAuthor.displayName }}</h1>{% if context.currentAuthor.bio %}<p class="lcars-lede" style="margin: 0;">{{ context.currentAuthor.bio }}</p>{% endif %}{% if showSocial %}<div class="lcars-btn-row" style="justify-content: center;">{% if context.currentAuthor.websiteUrl %}<a class="lcars-btn-ghost lcars-btn-sm" href="{{ context.currentAuthor.websiteUrl }}" target="_blank" rel="noopener noreferrer" title="Website"><svg class="lcars-icon" width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#globe"/></svg></a>{% endif %}{% if context.currentAuthor.twitterHandle %}<a class="lcars-btn-ghost lcars-btn-sm" href="https://twitter.com/{{ context.currentAuthor.twitterHandle|cut:"@" }}" target="_blank" rel="noopener noreferrer" title="X"><svg class="lcars-icon" width="16" height="16" aria-hidden="true"><use href="/icons/simple-icons.svg#x"/></svg></a>{% endif %}{% if context.currentAuthor.linkedinUrl %}<a class="lcars-btn-ghost lcars-btn-sm" href="{{ context.currentAuthor.linkedinUrl }}" target="_blank" rel="noopener noreferrer" title="LinkedIn"><svg class="lcars-icon" width="16" height="16" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"/></svg></a>{% endif %}</div>{% endif %}</div></div></section>{% endif %}

View File

@ -0,0 +1 @@
{% if context.currentPost.authorName %}<aside class="lcars-card lcars-card-accent {{ class }}" data-block="lcars:author-bio">{% if heading %}<p class="lcars-kicker" style="margin-top: 0;">{{ heading }}</p>{% endif %}<div style="display: flex; align-items: flex-start; gap: 1rem;">{% if showAvatar and context.currentPost.authorAvatarUrl %}<img class="lcars-avatar" src="{{ context.currentPost.authorAvatarUrl }}" alt="{{ context.currentPost.authorName }}" style="width: 3.5rem; height: 3.5rem; border-radius: 9999px; object-fit: cover; flex-shrink: 0;">{% endif %}<div style="min-width: 0;"><h3 class="lcars-heading" style="font-size: 1.125rem; margin: 0;">{% if context.currentPost.authorSlug %}<a href="/author/{{ context.currentPost.authorSlug }}">{{ context.currentPost.authorName }}</a>{% else %}{{ context.currentPost.authorName }}{% endif %}</h3>{% if context.currentAuthor.bio %}<p class="lcars-text" style="margin: 0.5rem 0 0 0; font-size: 0.9375rem; color: hsl(var(--muted-foreground));">{{ context.currentAuthor.bio }}</p>{% endif %}</div></div></aside>{% endif %}

View File

@ -0,0 +1 @@
<section class="lcars-section {{ class }}" data-block="lcars:blog-hero"><div class="lcars-content-well"><div class="lcars-stack">{% if title %}<h1 class="lcars-heading" style="font-size: clamp(2rem, 4vw, 3rem); margin: 0;"><span class="lcars-heading-h2-accent">{{ title }}</span></h1>{% endif %}{% if subtitle %}<p class="lcars-lede" style="max-width: 60ch; margin: 0.75rem 0 0 0;">{{ subtitle }}</p>{% endif %}</div>{% if posts %}<div class="lcars-post-grid lcars-post-grid-3" style="margin-top: 2.5rem;">{% for post in posts %}<article class="lcars-post-card{% if forloop.First %} lcars-card-accent{% endif %}">{% if post.featured_image_url %}<a href="{{ post.url }}" class="lcars-post-thumb lcars-media-frame" style="display: block; aspect-ratio: 16 / 9;"><img src="{{ post.featured_image_url }}" alt="{{ post.title }}" style="width: 100%; height: 100%; object-fit: cover;"></a>{% endif %}<h2 class="lcars-post-title"{% if forloop.First %} style="font-size: 1.5rem;"{% endif %}><a href="{{ post.url }}">{{ post.title }}</a></h2>{% if showExcerpt and post.excerpt %}<p class="lcars-post-excerpt">{{ post.excerpt }}</p>{% endif %}<div class="lcars-post-meta">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="lcars-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div></article>{% endfor %}</div>{% endif %}</div></section>

View File

@ -0,0 +1 @@
<nav aria-label="Breadcrumb" class="lcars-crumbs {{ class }}" data-block="lcars:breadcrumbs"><ol style="display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; list-style: none; margin: 0; padding: 0;">{% if showHome %}<li style="display: flex; align-items: center;"><a class="lcars-sidebar-link" href="/" style="padding: 0;">{{ homeLabel|default:"Home" }}</a></li>{% endif %}{% for item in breadcrumbs %}<li style="display: flex; align-items: center;">{% if showHome or not forloop.First %}<span class="lcars-crumb-sep" aria-hidden="true">{{ separator|default:"/" }}</span>{% endif %}{% if item.is_current %}{% if showCurrent %}<span aria-current="page" style="color: hsl(var(--foreground)); font-weight: 500;">{{ item.label }}</span>{% endif %}{% else %}<a class="lcars-sidebar-link" href="{{ item.url }}" style="padding: 0;">{{ item.label }}</a>{% endif %}</li>{% endfor %}</ol></nav>

View File

@ -0,0 +1,6 @@
{% set btnStyle = style|default:"primary" %}
{% set btnClass = "lcars-btn-primary" %}
{% if btnStyle == "outline" or btnStyle == "secondary" %}{% set btnClass = "lcars-btn-outline" %}{% elif btnStyle == "ghost" or btnStyle == "link" %}{% set btnClass = "lcars-btn-ghost" %}{% endif %}
{% set sizeClass = "" %}
{% if size == "sm" %}{% set sizeClass = "lcars-btn-sm" %}{% elif size == "lg" %}{% set sizeClass = "lcars-btn-lg" %}{% endif %}
<div class="lcars-btn-row {{ class }}" style="{% if align == "center" %}justify-content: center;{% elif align == "right" %}justify-content: flex-end;{% endif %}">{% if newTab %}<a class="{{ btnClass }} {{ sizeClass }}" href="{{ link|default:"#" }}" target="_blank" rel="noopener noreferrer">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"/></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"/></svg>{% endif %}</a>{% else %}<a class="{{ btnClass }} {{ sizeClass }}" href="{{ link|default:"#" }}">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"/></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"/></svg>{% endif %}</a>{% endif %}</div>

View File

@ -0,0 +1 @@
<article class="lcars-card lcars-card-accent {{ class }}">{% if media %}<div class="lcars-media-frame" style="aspect-ratio: 16 / 9; margin: -1.5rem -1.5rem 1.25rem -1.5rem; border: 0; border-bottom: 1px solid hsl(var(--border));">{% img media alt=title|default:"" class="lcars-card-media" %}</div>{% endif %}{% if title %}<h3 class="lcars-feature-title" style="margin-top: 0; font-size: 1.25rem;">{{ title }}</h3>{% endif %}{% if text %}<div class="lcars-text" style="font-size: 0.9375rem; line-height: 1.6; color: hsl(var(--muted-foreground));">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<a class="lcars-btn-ghost" href="{{ link }}" style="margin-top: 1rem;">{{ linkLabel }}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"/></svg></a>{% endif %}</article>

View File

@ -0,0 +1 @@
<section class="lcars-section-tight {{ class }}" data-block="lcars:category-list">{% if heading %}<h2 class="lcars-kicker">{{ heading }}</h2>{% endif %}{% if style == "pills" %}<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">{% for cat in categories %}<a href="{{ cat.url }}" class="lcars-tag">{{ cat.name }}{% if showCount %} <span class="lcars-mono numeric-tabular">{{ cat.post_count }}</span>{% endif %}</a>{% endfor %}</div>{% else %}<ul class="lcars-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for cat in categories %}<li><a href="{{ cat.url }}" class="lcars-sidebar-link" style="display: flex; align-items: center; justify-content: space-between;"><span>{{ cat.name }}</span>{% if showCount %}<span class="lcars-mono numeric-tabular">{{ cat.post_count }}</span>{% endif %}</a></li>{% endfor %}</ul>{% endif %}</section>

View File

@ -0,0 +1,14 @@
{# contact-card override — grouped contact channels; each channel a hairline mark + label + value. #}
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if description %}<p class="lcars-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
<div class="lcars-grid {% if columns == 3 %}lcars-grid-3{% elif columns == 1 %}{% else %}lcars-grid-2{% endif %}" style="display:grid;gap:1rem;{% if columns == 3 %}grid-template-columns:repeat(auto-fill, minmax(240px, 1fr));{% elif columns == 1 %}grid-template-columns:1fr;{% else %}grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));{% endif %}">
{% for channel in channels %}<div class="lcars-channel lcars-card" style="display:flex;align-items:flex-start;gap:1rem;">
{% if channel.icon %}<span class="lcars-channel-mark" style="display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;flex:0 0 auto;border:1px solid hsl(var(--border));border-radius:4px;color:hsl(var(--accent));"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"/></svg></span>{% endif %}
<div style="min-width:0;">
{% if channel.label %}<div class="lcars-stat-label" style="margin:0;">{{ channel.label }}</div>{% endif %}
{% if channel.value %}<div style="margin-top:0.25rem;font-weight:500;word-break:break-word;color:hsl(var(--foreground));">{% if channel.link %}<a href="{{ channel.link }}" style="color:inherit;text-decoration:none;">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
</div>
</div>{% endfor %}
</div>
</div></section>

View File

@ -0,0 +1,34 @@
{# contact-form override — server-driven; block-id resolution, {% form %} wiring, honeypot, and field loop copied verbatim from the builtin, reskinned with lcars-form primitives. #}
{% with bid=_block_id|default:context.blockId %}
<section id="contact-form-{{ bid }}" class="lcars-section {{ class }}"><div class="lcars-content-well" style="max-width:640px;">
{% if form.submitted %}
<div class="lcars-card lcars-card-accent lcars-alert-ok" style="text-align:center;padding:2rem;">
<span class="lcars-mark lcars-mark-circle" aria-hidden="true" style="display:inline-block;margin-bottom:1rem;"></span>
<p class="lcars-display" style="margin:0;font-size:1.125rem;">{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}</p>
</div>
{% else %}
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if description %}<p class="lcars-lede" style="margin:0 0 2rem 0;color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
{% if form.errors._form %}<div class="lcars-alert-err" style="margin-bottom:1.5rem;border:1px solid hsl(var(--destructive));border-radius:4px;padding:0.75rem 1rem;font-size:0.875rem;color:hsl(var(--destructive));background:hsl(var(--destructive) / 0.08);">{{ 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="lcars-form-grid" %}
{% for field in fields %}
<div class="lcars-field {% if field.width == "half" %}{% else %}lcars-field--full{% endif %}">
{% if field.label %}<label class="lcars-label" for="cf-{{ bid }}-{{ field.name }}">{{ field.label }}{% if field.required %} <span style="color:hsl(var(--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="lcars-input"></textarea>
{% elif field.type == "select" %}
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="lcars-input">{% 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="lcars-input">
{% endif %}
</div>
{% endfor %}
<div class="lcars-field--full" aria-hidden="true" style="display:none;"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
<div class="lcars-field--full"><button type="submit" class="lcars-btn-primary">{{ submitLabel|default:"Send message" }}</button></div>
{% endform %}
{% endwith %}
{% endif %}
</div></section>
<style>#contact-form-{{ bid }} .lcars-form-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.25rem;margin-top:2rem;}#contact-form-{{ bid }} .lcars-field--full{grid-column:1 / -1;}#contact-form-{{ bid }} .lcars-label{display:block;margin-bottom:0.375rem;font-size:0.8125rem;font-weight:500;color:hsl(var(--foreground));font-family:var(--font-heading, "Antonio", "Antonio", system-ui, sans-serif);}#contact-form-{{ bid }} .lcars-input{width:100%;padding:0.5rem 0.75rem;font-size:0.9375rem;font-family:var(--font-body, "Antonio", system-ui, sans-serif);color:hsl(var(--foreground));background:hsl(var(--background));border:1px solid hsl(var(--input));border-radius:4px;box-sizing:border-box;}#contact-form-{{ bid }} .lcars-input:focus{outline:2px solid hsl(var(--ring));outline-offset:1px;}</style>
{% endwith %}

View File

@ -0,0 +1,44 @@
{# countdown override — oversized mono unit boxes; host countdown JS binds via data-* attrs. #}
<section class="lcars-section {{ class }}"><div class="lcars-content-well" style="text-align: center;">
{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="lcars-lede" style="max-width: 60ch; margin: 0 auto; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
<div data-countdown style="margin-top: 2.5rem;">
<div data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining" style="display: flex; align-items: flex-start; justify-content: center; gap: 1rem; flex-wrap: wrap;">
<div class="lcars-count-unit {% if variant == "boxes" %}lcars-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="lcars-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="days">--</span><span class="lcars-stat-label" style="margin-top: 0.5rem;">{{ labelDays|default:"Days" }}</span></div>
<div class="lcars-count-unit {% if variant == "boxes" %}lcars-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="lcars-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="hours">--</span><span class="lcars-stat-label" style="margin-top: 0.5rem;">{{ labelHours|default:"Hours" }}</span></div>
<div class="lcars-count-unit {% if variant == "boxes" %}lcars-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="lcars-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="minutes">--</span><span class="lcars-stat-label" style="margin-top: 0.5rem;">{{ labelMinutes|default:"Minutes" }}</span></div>
<div class="lcars-count-unit {% if variant == "boxes" %}lcars-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="lcars-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="seconds">--</span><span class="lcars-stat-label" style="margin-top: 0.5rem;">{{ labelSeconds|default:"Seconds" }}</span></div>
</div>
<div data-countdown-expired style="display: none; margin-top: 2rem;"><p class="lcars-heading" style="font-size: 1.25rem;">{{ expiredMessage|default:"This has started." }}</p>{% if expiredUrl and expiredLabel %}<a class="lcars-btn-primary" href="{{ expiredUrl }}" style="margin-top: 1rem;">{{ expiredLabel }}</a>{% endif %}</div>
</div>
<noscript><p class="lcars-text" style="margin-top: 1.5rem; color: hsl(var(--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.style.display = 'block';
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,8 @@
{# cta override — full-width strip; band / card / split variants. #}
<section class="lcars-cta-strip {% if background == "band" %}lcars-cta-strip--band{% elif background == "card" %}lcars-cta-strip--card{% endif %} {{ class }}" style="padding-top: 3rem; padding-bottom: 3rem;"><div class="lcars-content-well" style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1.5rem 2.5rem;">
<div style="{% if background != "split" %}max-width: 60ch;{% endif %}">
{% if heading %}<h2 class="lcars-cta-headline lcars-heading" style="font-size: clamp(1.75rem, 4vw, 2.75rem); margin: 0; color: inherit;">{{ heading }}</h2>{% endif %}
{% if text %}<p class="lcars-lede" style="margin: 0.75rem 0 0 0; max-width: 55ch; color: {% if background == "band" %}inherit{% else %}hsl(var(--muted-foreground)){% endif %};">{{ text }}</p>{% endif %}
</div>
{% if primaryLabel or secondaryLabel %}<div class="lcars-btn-row" style="display: flex; flex-wrap: wrap; gap: 0.75rem; flex: none;">{% if primaryUrl and primaryLabel %}<a class="lcars-btn-primary lcars-btn-lg" href="{{ primaryUrl }}">{{ primaryLabel }}</a>{% endif %}{% if secondaryUrl and secondaryLabel %}<a class="lcars-btn-outline lcars-btn-lg" href="{{ secondaryUrl }}">{{ secondaryLabel }}</a>{% endif %}</div>{% endif %}
</div></section>

View File

@ -0,0 +1,2 @@
{# divider override — plain hairline / thick spacer rule / centered ornament between two hairlines. #}
{% if variant == "ornament" %}<div class="lcars-divider-ornament {{ class }}" role="separator" style="display:flex;align-items:center;gap:1rem;margin:2.5rem auto;{% if width == "narrow" %}max-width:200px;{% elif width == "wide" %}max-width:768px;{% endif %}"><span class="lcars-hairline-top" style="flex:1;height:0;"></span><span style="display:inline-flex;color:hsl(var(--muted-foreground));"><svg width="20" height="20" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|default:"lucide:asterisk"|split:":"|first }}.svg#{{ icon|default:"lucide:asterisk"|split:":"|last }}"/></svg></span><span class="lcars-hairline-top" style="flex:1;height:0;"></span></div>{% elif variant == "spacer-rule" %}<hr class="lcars-divider-line lcars-divider-thick {{ class }}" role="separator" style="border:0;border-top:{% if weight == "thick" %}4px{% elif weight == "medium" %}2px{% else %}1px{% endif %} solid hsl(var(--border));margin:3rem auto;{% if width == "narrow" %}max-width:200px;{% elif width == "wide" %}max-width:768px;{% endif %}">{% else %}<hr class="lcars-divider-line {{ class }}" role="separator" style="border:0;border-top:{% if weight == "thick" %}4px{% elif weight == "medium" %}2px{% else %}1px{% endif %} solid hsl(var(--border));margin:2rem auto;{% if width == "narrow" %}max-width:200px;{% elif width == "wide" %}max-width:768px;{% endif %}">{% endif %}

View File

@ -0,0 +1,31 @@
{# event-list override — dates shown VERBATIM in each event's own timezone (never converted); list rows or card grid. #}
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="lcars-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if layout == "cards" %}
<div class="lcars-grid lcars-grid-3" style="margin-top:2.5rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(260px, 1fr));gap:1.5rem;">
{% for event in events %}{% if not (hidePast and event.past) %}<article class="lcars-card" style="display:flex;flex-direction:column;padding:0;overflow:hidden;{% if event.past %}opacity:0.6;{% endif %}">
{% if event.media %}<div class="lcars-media-frame" style="aspect-ratio:16 / 9;overflow:hidden;border-bottom:1px solid hsl(var(--border));">{% img event.media alt=event.title|default:"" class="lcars-event-media" %}</div>{% endif %}
<div style="display:flex;flex-direction:column;gap:0.5rem;padding:1.25rem;flex:1;">
<div class="lcars-mono numeric-tabular" style="font-size:0.8125rem;color:hsl(var(--accent));"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} &ndash; {{ event.end }}{% endif %}</time>{% if event.timezone %} <span style="color:hsl(var(--muted-foreground));">{{ event.timezone }}</span>{% endif %}</div>
<h3 class="lcars-feature-title" style="font-size:1.125rem;margin:0;">{{ event.title }}</h3>
{% if event.venue %}<p style="margin:0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p style="margin:0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.description }}</p>{% endif %}
{% if event.link %}<a class="lcars-btn-ghost" href="{{ event.link }}" style="margin-top:auto;display:inline-flex;align-items:center;gap:0.375rem;">{{ event.linkLabel|default:"Details" }}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"/></svg></a>{% endif %}
</div>
</article>{% endif %}{% endfor %}
</div><style>.lcars-event-media{display:block;width:100%;height:100%;object-fit:cover;}</style>
{% else %}
<ul class="lcars-hairline-top" style="list-style:none;margin:2.5rem 0 0 0;padding:0;">
{% for event in events %}{% if not (hidePast and event.past) %}<li class="lcars-hairline-bottom" style="display:flex;flex-wrap:wrap;align-items:baseline;gap:0.75rem 1.5rem;padding:1.25rem 0;{% if event.past %}opacity:0.6;{% endif %}">
<div class="lcars-mono numeric-tabular" style="flex:0 0 12rem;font-size:0.875rem;"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time>{% if event.end %}<div style="font-size:0.75rem;color:hsl(var(--muted-foreground));">to {{ event.end }}</div>{% endif %}{% if event.timezone %}<div style="font-size:0.75rem;color:hsl(var(--muted-foreground));">{{ event.timezone }}</div>{% endif %}</div>
<div style="min-width:0;flex:1;">
<h3 class="lcars-feature-title" style="font-size:1rem;margin:0;">{{ event.title }}</h3>
{% if event.venue %}<p style="margin:0.125rem 0 0 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p style="margin:0.25rem 0 0 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.description }}</p>{% endif %}
</div>
{% if event.link %}<a class="lcars-btn-outline lcars-btn-sm" href="{{ event.link }}" style="flex:0 0 auto;">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
</li>{% endif %}{% endfor %}
</ul>
{% endif %}
</div></section>

View File

@ -0,0 +1,6 @@
{# faq override — native hairline-divided disclosure list. #}
<section class="lcars-section {{ class }}"><div class="lcars-content-well lcars-measure">
{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="lcars-lede" style="max-width: 60ch; margin: 0 0 2rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if items %}<div class="lcars-hairline-top" style="margin-top: 1.5rem;">{% for item in items %}<details class="lcars-faq-item lcars-hairline-bottom"{% if variant == "open-list" %} open{% endif %}><summary class="lcars-heading" style="display: flex; justify-content: space-between; align-items: center; gap: 1rem; cursor: pointer; padding: 1rem 0; font-size: 1.125rem; list-style: none;">{{ item.question }}<span class="lcars-faq-chevron" aria-hidden="true" style="color: hsl(var(--muted-foreground)); flex: none;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"/></svg></span></summary><div class="lcars-faq-answer lcars-text" style="padding-bottom: 1rem; color: hsl(var(--muted-foreground));">{{ item.answer|safe }}</div></details>{% endfor %}</div>{% endif %}
</div></section>

View File

@ -0,0 +1,7 @@
{# feature-grid override — Swiss cells, each led by a Bauhaus square mark. #}
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
{% if eyebrow %}<p class="lcars-eyebrow">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="lcars-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if items %}<div class="lcars-grid {% if columns == 2 %}lcars-grid-2{% elif columns == 4 %}lcars-grid-4{% else %}lcars-grid-3{% endif %}">{% for item in items %}<div class="lcars-feature">{% if item.icon %}<span class="lcars-mark lcars-mark-square" aria-hidden="true"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"/></svg></span>{% else %}<span class="lcars-mark lcars-mark-square" aria-hidden="true"></span>{% endif %}{% if item.title %}<h3 class="lcars-feature-title">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" style="color: inherit; text-decoration: none;">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>{% endif %}{% if item.text %}<p class="lcars-text" style="font-size: 0.9375rem; line-height: 1.6; color: hsl(var(--muted-foreground));">{{ item.text }}</p>{% endif %}{% if item.linkUrl and item.linkLabel %}<a class="lcars-btn-ghost" href="{{ item.linkUrl }}" style="margin-top: 0.75rem;">{{ item.linkLabel }}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"/></svg></a>{% endif %}</div>{% endfor %}</div>{% endif %}
</div></section>

View File

@ -0,0 +1 @@
<section class="lcars-section {{ class }}" data-block="lcars:featured-posts"><div class="lcars-content-well">{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 2rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}<div class="lcars-post-grid {% if columns == 2 %}lcars-post-grid-2{% else %}lcars-post-grid-3{% endif %}">{% for post in posts %}<article class="lcars-post-card">{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="lcars-post-thumb lcars-media-frame" style="display: block; aspect-ratio: 16 / 9;"><img src="{{ post.featured_image_url }}" alt="{{ post.title }}" style="width: 100%; height: 100%; object-fit: cover;"></a>{% endif %}<h3 class="lcars-post-title"><a href="{{ post.url }}">{{ post.title }}</a></h3>{% if showExcerpt and post.excerpt %}<p class="lcars-post-excerpt">{{ post.excerpt }}</p>{% endif %}<div class="lcars-post-meta">{% if showAuthor and post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if showDate and post.published_at %}<span class="lcars-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}{% if post.reading_time %}<span class="lcars-mono numeric-tabular">{{ post.reading_time }} min</span>{% endif %}</div></article>{% endfor %}</div></div></section>

View File

@ -0,0 +1,17 @@
{# footer override — Swiss-grid footer chrome from CONTENT fields (brand, contact, newsletter, legal); menu columns / social / legal rows render from the footer providers (empty today, forward-correct). #}
<footer class="lcars-footer {{ class }}"><div class="lcars-content-well">
<div class="lcars-footer-cols" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:2.5rem;align-items:start;">
<div class="lcars-stack" style="grid-column:span 2;min-width:220px;display:flex;flex-direction:column;gap:1rem;">
<a href="/" style="display:inline-flex;align-items:center;gap:0.5rem;text-decoration:none;color:hsl(var(--foreground));">{% if logo %}{% img logo alt=companyName|default:"Home" class="lcars-footer-logo" %}{% else %}<span class="lcars-mark lcars-mark-square" aria-hidden="true"></span><span class="lcars-display" style="font-size:1.125rem;">{{ companyName|default:context.site.title|default:"Studio" }}</span>{% endif %}</a>
{% if tagline %}<p class="lcars-text" style="font-size:0.9375rem;max-width:34ch;margin:0;color:hsl(var(--muted-foreground));">{{ tagline }}</p>{% endif %}
{% if address or email or phone %}<address class="lcars-mono numeric-tabular" style="font-style:normal;font-size:0.8125rem;line-height:1.8;color:hsl(var(--muted-foreground));">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="lcars-footer-link" style="color:inherit;text-decoration:none;">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="lcars-footer-link" style="color:inherit;text-decoration:none;">{{ phone }}</a></div>{% endif %}</address>{% endif %}
{% if showNewsletter %}<div class="lcars-stack-sm" style="margin-top:0.5rem;display:flex;flex-direction:column;gap:0.5rem;"><p class="lcars-footer-head" style="margin:0;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.08em;color:hsl(var(--foreground));">{{ newsletterTitle|default:"Stay in the loop" }}</p>{% if newsletterDescription %}<p class="lcars-text" style="margin:0;font-size:0.8125rem;color:hsl(var(--muted-foreground));">{{ newsletterDescription }}</p>{% endif %}{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div>{% endif %}
</div>
{% for col in columns %}<div class="lcars-stack-sm"><p class="lcars-footer-head" style="margin:0 0 0.75rem;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.08em;color:hsl(var(--foreground));">{{ col.header }}</p><ul style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0.5rem;">{% for item in col.items %}<li><a class="lcars-footer-link" href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} style="font-size:0.875rem;color:hsl(var(--muted-foreground));text-decoration:none;">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
</div>
<div class="lcars-footer-legal" style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:1rem;">
<p style="margin:0;">{% if copyright %}&copy; {{ copyright }}{% else %}&copy; {{ companyName }}{% endif %}</p>
{% if legal_items %}<nav style="display:flex;flex-wrap:wrap;gap:1.25rem;">{% for l in legal_items %}<a href="{{ l.url|default:'#' }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="lcars-footer-link" style="font-size:0.8125rem;color:hsl(var(--muted-foreground));text-decoration:none;">{{ l.label }}</a>{% endfor %}</nav>{% endif %}
{% if showSocial and social_items %}<div class="lcars-social-row" style="display:flex;align-items:center;gap:0.75rem;">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="lcars-social-link" style="display:inline-flex;color:hsl(var(--muted-foreground));">{% if s.icon %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ s.icon|split:":"|first }}.svg#{{ s.icon|split:":"|last }}"/></svg>{% endif %}</a>{% endfor %}</div>{% endif %}
</div>
</div><style>.lcars-footer-logo{max-height:2rem;width:auto;display:block;}.lcars-footer-link:hover{color:hsl(var(--foreground));}</style></footer>

View File

@ -0,0 +1,6 @@
{# gallery override — Swiss grid of hairline-framed images; carries data-bn-gallery / data-lightbox / data-bn-gallery-item so the host lightbox JS binds. #}
{% if columns == 4 %}{% set gcols = "lcars-gallery-4" %}{% set gn = 4 %}{% elif columns == 2 %}{% set gcols = "" %}{% set gn = 2 %}{% else %}{% set gcols = "lcars-gallery-3" %}{% set gn = 3 %}{% endif %}
{% if gap == "sm" %}{% set ggap = "0.5rem" %}{% elif gap == "lg" %}{% set ggap = "1.5rem" %}{% else %}{% set ggap = "1rem" %}{% endif %}
<div class="lcars-gallery-grid {{ gcols }} {{ class }}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}" style="margin:1.5rem 0;display:grid;grid-template-columns:repeat({{ gn }}, minmax(0, 1fr));gap:{{ ggap }};">
{% for image in images %}<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:'' }}" class="lcars-media-frame" style="display:block;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--muted));">{% img image.src alt=image.alt|default:"" class="lcars-gallery-img" %}</a>{% endfor %}
</div><style>.lcars-gallery-grid .lcars-gallery-img{display:block;width:100%;height:100%;aspect-ratio:1;object-fit:cover;}</style>

View File

@ -0,0 +1 @@
{% if level|integer == 1 %}<h1 class="lcars-heading lcars-heading-h1 {{ textClass }}" style="font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1;">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="lcars-heading lcars-heading-h3 {{ textClass }}" style="font-size: 1.5rem; line-height: 1.3;">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="lcars-heading lcars-heading-h4 {{ textClass }}" style="font-size: 1.25rem; line-height: 1.3;">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="lcars-heading lcars-heading-h5 {{ textClass }}" style="font-size: 1.125rem; line-height: 1.4;">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="lcars-heading lcars-heading-h6 {{ textClass }}" style="font-size: 1rem; line-height: 1.4;">{{ text }}</h6>{% else %}<h2 class="lcars-heading lcars-heading-h2 {{ textClass }}" style="font-size: clamp(1.5rem, 3vw, 2.25rem); line-height: 1.2;"><span class="lcars-heading-h2-accent">{{ text }}</span></h2>{% endif %}

View File

@ -0,0 +1,57 @@
{# hero override — LCARS command-deck hero. GO BIG motion tier: one dependency-free canvas panel-sweep behind the content, lazy-inited, prefers-reduced-motion honored, with the fully-styled hero as the static no-JS fallback. All content fields preserved verbatim from the builtin. Styled-text fields accept {text,color} OR a bare string. #}
{% set eyebrowText = eyebrow|get:"text"|default:eyebrow %}
{% set headlineText = headline|get:"text"|default:headline %}
{% set subText = subheadline|get:"text"|default:subheadline %}
{% set descText = description|get:"text"|default:description %}
{% set pText = ctaPrimary.text %}
{% set pUrl = ctaPrimary.url %}
{% set sText = ctaSecondary.text %}
{% set sUrl = ctaSecondary.url %}
{% set variant = template|default:"centered" %}
{% macro herobody() %}
{% if eyebrowText %}<p class="lcars-eyebrow">{{ eyebrowText }}</p>{% endif %}
{% if headlineText %}<h1 class="lcars-hero-headline lcars-display" style="margin: 0;">{{ headlineText }}</h1>{% endif %}
{% if subText %}<p class="lcars-hero-sub lcars-lede" style="margin: 1.25rem 0 0 0; max-width: 60ch;">{{ subText }}</p>{% endif %}
{% if descText %}<p class="lcars-text" style="margin: 1rem 0 0 0; color: hsl(var(--muted-foreground)); max-width: 60ch;">{{ descText }}</p>{% endif %}
{% if pText or sText %}<div class="lcars-btn-row" style="margin-top: 2rem;">{% if pText and pUrl %}<a class="lcars-btn-primary lcars-btn-lg" href="{{ pUrl }}">{{ pText }}</a>{% endif %}{% if sText and sUrl %}<a class="lcars-btn-outline lcars-btn-lg" href="{{ sUrl }}">{{ sText }}</a>{% endif %}</div>{% endif %}
{% if trustLogos %}<div class="lcars-hairline-top" style="margin-top: 3rem; padding-top: 1.5rem;"><p class="lcars-kicker lcars-eyebrow" style="margin-bottom: 1rem; color: hsl(var(--muted-foreground));">Trusted by</p><div class="lcars-logo-row">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}">{% img logo.image alt=logo.alt class="lcars-logo" %}</a>{% else %}{% img logo.image alt=logo.alt class="lcars-logo" %}{% endif %}{% endfor %}</div></div>{% endif %}
{% endmacro %}
{% if variant == "split" %}
<section class="lcars-hero lcars-hero-split lcars-hero-canvas-host {{ class }}"><canvas class="lcars-hero-canvas" data-lcars-hero-canvas aria-hidden="true"></canvas><div class="lcars-content-well">{% if backgroundImage %}<div class="lcars-grid lcars-grid-2" style="align-items: center;"><div>{{ herobody() }}</div><div class="lcars-media-frame" style="aspect-ratio: 4 / 3; max-height: 26rem; {% if mediaPosition == "left" %}order: -1;{% endif %}">{% img backgroundImage alt=headlineText class="lcars-hero-media" %}</div></div>{% else %}<div style="max-width: 60ch;">{{ herobody() }}</div>{% endif %}</div></section>
{% else %}
<section class="lcars-hero lcars-hero-canvas-host {{ class }}" style="{% if variant == "left-aligned" %}text-align: left;{% else %}text-align: center;{% endif %}"><canvas class="lcars-hero-canvas" data-lcars-hero-canvas aria-hidden="true"></canvas><div class="lcars-content-well" style="{% if variant != "left-aligned" %}display: flex; flex-direction: column; align-items: center;{% endif %}"><div style="max-width: 60ch; width: 100%;">{{ herobody() }}</div></div></section>
{% endif %}
<script>
(function(){
if(window.__lcarsHeroInit)return;window.__lcarsHeroInit=1;
function reduced(){return window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;}
function tok(el,name){var v=getComputedStyle(el).getPropertyValue(name);return (v&&v.trim())?('hsl('+v.trim()+')'):'currentColor';}
function start(cv){
if(cv.__on)return;cv.__on=1;
var ctx=cv.getContext&&cv.getContext('2d');if(!ctx)return;
var host=cv.parentElement;
var cols=[tok(host,'--primary'),tok(host,'--secondary'),tok(host,'--accent')];
var bars=[];
function seed(w,h){bars=[];var n=Math.max(4,Math.floor(h/46));for(var i=0;i<n;i++){bars.push({y:(i+0.5)*(h/n),w:40+Math.random()*160,x:Math.random()*w,v:(0.25+Math.random()*0.9)*(Math.random()<0.5?-1:1),c:cols[i%3]});}}
function size(){var r=host.getBoundingClientRect();var dpr=Math.min(window.devicePixelRatio||1,2);cv.width=Math.max(1,r.width*dpr);cv.height=Math.max(1,r.height*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);seed(r.width,r.height);}
size();
var still=reduced();
function frame(){
var r=host.getBoundingClientRect();ctx.clearRect(0,0,r.width,r.height);ctx.globalAlpha=0.16;
for(var i=0;i<bars.length;i++){var b=bars[i];ctx.fillStyle=b.c;var h=14;var rad=h/2;var x=b.x,y=b.y-h/2;
ctx.beginPath();ctx.moveTo(x+rad,y);ctx.arcTo(x+b.w,y,x+b.w,y+h,rad);ctx.arcTo(x+b.w,y+h,x,y+h,rad);ctx.arcTo(x,y+h,x,y,rad);ctx.arcTo(x,y,x+b.w,y,rad);ctx.closePath();ctx.fill();
if(!still){b.x+=b.v;if(b.x>r.width)b.x=-b.w;if(b.x<-b.w)b.x=r.width;}}
if(!still)cv.__raf=requestAnimationFrame(frame);
}
frame();
var rt;window.addEventListener('resize',function(){clearTimeout(rt);rt=setTimeout(size,150);},{passive:true});
}
function init(){
var nodes=document.querySelectorAll('[data-lcars-hero-canvas]');
if(!('IntersectionObserver' in window)){nodes.forEach(start);return;}
var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){start(e.target);io.unobserve(e.target);}});},{rootMargin:'120px'});
nodes.forEach(function(n){io.observe(n);});
}
if(document.readyState!='loading')init();else document.addEventListener('DOMContentLoaded',init);
})();
</script>

View File

@ -0,0 +1,18 @@
{# hours-location override — hairline hours table + address; click-to-load map preserves the builtin data-map-src / onclick wiring. #}
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
<div class="lcars-grid lcars-grid-2" style="display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:2.5rem;margin-top:2.5rem;">
<div>
{% if hours %}<table style="width:100%;border-collapse:collapse;font-size:0.9375rem;"><tbody>{% for row in hours %}<tr class="lcars-hours-row lcars-hairline-bottom"><th scope="row" style="text-align:left;padding:0.5rem 1rem 0.5rem 0;font-weight:500;">{{ row.day }}</th><td class="numeric-tabular" style="text-align:right;padding:0.5rem 0;color:{% if row.closed %}hsl(var(--muted-foreground)){% else %}hsl(var(--foreground)){% endif %};">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td></tr>{% endfor %}</tbody></table>{% endif %}
{% if specialHoursNote %}<p class="lcars-surface-muted" style="margin-top:1rem;border:1px solid hsl(var(--border));background:hsl(var(--muted));padding:0.5rem 0.75rem;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ specialHoursNote }}</p>{% endif %}
{% if address %}<div style="margin-top:1.5rem;"><div class="lcars-stat-label" style="margin:0;">Address</div><address style="margin-top:0.25rem;white-space:pre-line;font-style:normal;color:hsl(var(--foreground));">{{ address }}</address></div>{% endif %}
{% if phone %}<div style="margin-top:1rem;"><div class="lcars-stat-label" style="margin:0;">Phone</div><a href="tel:{{ phone }}" style="display:inline-block;margin-top:0.25rem;color:hsl(var(--foreground));text-decoration:none;">{{ phone }}</a></div>{% endif %}
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener noreferrer" class="lcars-btn-outline" style="margin-top:1.5rem;display:inline-flex;align-items:center;gap:0.5rem;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#navigation"/></svg>Get directions</a>{% endif %}
</div>
{% if mapEmbedUrl %}
<div class="lcars-media-frame" style="width:100%;min-height:16rem;aspect-ratio:4 / 3;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--muted));">
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('style','width:100%;height:100%;border:0;');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" style="display:flex;align-items:center;justify-content:center;gap:0.5rem;width:100%;height:100%;min-height:16rem;border:0;background:none;cursor:pointer;color:hsl(var(--foreground));font-family:var(--font-heading, 'Antonio', 'Antonio', system-ui, sans-serif);font-weight:500;"><svg width="20" height="20" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#map-pin"/></svg>{{ mapButtonLabel|default:"Load map" }}</button>
</div>
{% endif %}
</div>
</div></section>

View File

@ -0,0 +1,5 @@
{# image override — hairline-framed figure with alignment and an optional caption. #}
{% if src %}{% if align == "left" %}{% set ialign = "flex-start" %}{% elif align == "right" %}{% set ialign = "flex-end" %}{% else %}{% set ialign = "center" %}{% endif %}<figure class="{{ class }}" style="margin:1.5rem 0;display:flex;flex-direction:column;align-items:{{ ialign }};">
<div class="lcars-media-frame" style="max-width:100%;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;line-height:0;">{% img src alt=alt|default:"" class="lcars-image-el" %}</div>
{% if caption %}<figcaption class="lcars-stat-label" style="margin-top:0.5rem;text-align:{% if align == "center" %}center{% else %}left{% endif %};">{{ caption }}</figcaption>{% endif %}
</figure><style>.lcars-media-frame > .lcars-image-el,.lcars-media-frame > img{display:block;max-width:100%;height:auto;}</style>{% endif %}

View File

@ -0,0 +1,5 @@
{# logo-strip override — quiet greyscale client row under a kicker. #}
<section class="lcars-section-tight {{ class }}" style="padding-top: 3rem; padding-bottom: 3rem;"><div class="lcars-content-well">
{% if heading %}<p class="lcars-kicker lcars-eyebrow" style="text-align: center; color: hsl(var(--muted-foreground)); margin-bottom: 0;">{{ heading }}</p>{% endif %}
<div class="lcars-logo-row" style="display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 2.5rem; margin-top: 1.5rem;">{% for logo in logos %}{% if logo.url %}<a href="{{ logo.url }}">{% img logo.image alt=logo.alt class="lcars-logo" %}</a>{% else %}{% img logo.image alt=logo.alt class="lcars-logo" %}{% endif %}{% endfor %}</div>
</div></section>

View File

@ -0,0 +1,21 @@
{# menu-list override — sectioned price list; tabular-numeral prices right-aligned, dietary badges as hairline pills. #}
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="lcars-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
<div class="lcars-grid {% if columns == 2 %}lcars-grid-2{% endif %}" style="display:grid;gap:2.5rem 3rem;{% if columns == 2 %}grid-template-columns:repeat(2, minmax(0, 1fr));{% endif %}">
{% for section in sections %}<div class="lcars-menu-section" style="break-inside:avoid;">
{% if section.title %}<h3 class="lcars-display" style="font-size:1.25rem;border-bottom:1px solid hsl(var(--border));padding-bottom:0.5rem;margin:0 0 1rem 0;">{{ section.title }}</h3>{% endif %}
{% if section.note %}<p style="margin:0 0 1rem 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ section.note }}</p>{% endif %}
<ul style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:1.25rem;">
{% for item in section.items %}<li class="lcars-menu-item" style="display:flex;align-items:baseline;gap:1rem;">
<div style="min-width:0;flex:1;">
<div style="display:flex;flex-wrap:wrap;align-items:baseline;gap:0.5rem;"><span class="lcars-display" style="font-size:1rem;color:hsl(var(--foreground));">{{ item.name }}</span>{% for badge in item.badges %}{% if badge.label %}<span style="display:inline-block;border:1px solid hsl(var(--border));border-radius:4px;padding:0.05rem 0.4rem;font-size:0.6875rem;text-transform:uppercase;letter-spacing:0.05em;color:hsl(var(--muted-foreground));">{{ badge.label }}</span>{% endif %}{% endfor %}</div>
{% if item.description %}<p style="margin:0.25rem 0 0 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ item.description }}</p>{% endif %}
</div>
{% if item.price %}<span class="lcars-menu-price lcars-mono numeric-tabular" style="flex:0 0 auto;color:hsl(var(--foreground));">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
</li>{% endfor %}
</ul>
</div>{% endfor %}
</div>
{% if footnote %}<p class="lcars-stat-label" style="margin-top:2.5rem;">{{ footnote }}</p>{% endif %}
</div></section>

View File

@ -0,0 +1 @@
<nav class="lcars-navbar {{ class }}" aria-label="Primary"><a class="lcars-brand" href="/"><span class="lcars-mark lcars-mark-square" aria-hidden="true"></span>{% if logoType == "image" and logo %}{% img logo alt=alt|default:"Home" class="lcars-brand-logo" %}{% else %}<span>{{ logoText|get:"text"|default:logoText|default:context.site.title|default:"Studio" }}</span>{% endif %}</a><div class="lcars-nav-links">{% for item in menu.items %}<a class="lcars-nav-link" href="{{ item.url }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}>{{ item.label|default:item.title }}</a>{% endfor %}{% if cta.text and cta.url %}<a class="lcars-btn-primary lcars-btn-sm" href="{{ cta.url }}">{{ cta.text }}</a>{% endif %}</div><input type="checkbox" id="lcars-nav-toggle-cb" class="lcars-nav-cb" style="display:none;"><label for="lcars-nav-toggle-cb" class="lcars-nav-toggle lcars-btn-outline lcars-btn-sm" aria-label="Toggle menu"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#menu"/></svg></label><div class="lcars-nav-drawer">{% for item in menu.items %}<a href="{{ item.url }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}>{{ item.label|default:item.title }}</a>{% endfor %}{% if cta.text and cta.url %}<a href="{{ cta.url }}" class="lcars-btn-primary lcars-btn-sm" style="margin-top:0.75rem;">{{ cta.text }}</a>{% endif %}</div><style>.lcars-nav-cb:checked ~ .lcars-nav-drawer{display:flex;}</style></nav>

View File

@ -0,0 +1 @@
<section class="lcars-section {{ class }}" data-block="lcars:newsletter-signup"><div class="lcars-content-well"><div class="{% if layout == "inline" %}lcars-panel{% else %}lcars-card lcars-card-accent{% endif %}" style="text-align: center;">{% if title %}<h2 class="lcars-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0;"><span class="lcars-heading-h2-accent">{{ title }}</span></h2>{% endif %}{% if description %}<p class="lcars-lede" style="max-width: 52ch; margin: 0.75rem auto 0 auto;">{{ description }}</p>{% endif %}<div class="lcars-form" style="max-width: 28rem; margin: 1.5rem auto 0 auto;">{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}</div></div></div></section>

View File

@ -0,0 +1 @@
<section class="lcars-section {{ class }}" data-block="lcars:page-suggestions"><div class="lcars-content-well"><div class="lcars-measure lcars-stack"><span class="lcars-mark lcars-mark-triangle" aria-hidden="true"></span><h2 class="lcars-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0;"><span class="lcars-heading-h2-accent">{{ title|default:"Were you looking for..." }}</span></h2>{% if suggestions %}<ul class="lcars-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for s in suggestions %}<li><a class="lcars-sidebar-link" href="/{{ s.slug }}" style="display: block;"><span class="lcars-post-title" style="font-size: 1rem;">{{ s.title }}</span>{% if showDescription and s.excerpt %}<span class="lcars-text" style="display: block; font-size: 0.875rem; color: hsl(var(--muted-foreground));">{{ s.excerpt }}</span>{% endif %}</a></li>{% endfor %}</ul>{% else %}{% if emptyMessage %}<p class="lcars-text" style="margin: 0; color: hsl(var(--muted-foreground));">{{ emptyMessage }}</p>{% endif %}<div class="lcars-btn-row"><a class="lcars-btn-outline lcars-btn-sm" href="/"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#home"/></svg>Home</a><a class="lcars-btn-outline lcars-btn-sm" href="/blog"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#newspaper"/></svg>Blog</a></div>{% endif %}</div></div></section>

View File

@ -0,0 +1,11 @@
<div class="lcars-stack {{ class }}" data-block="lcars:password-reset-form">
<span class="lcars-mark lcars-mark-ring" aria-hidden="true"></span>
<h2 class="lcars-heading" style="font-size: 1.5rem; margin: 0;">Reset password</h2>
<p class="lcars-text" style="margin: 0; font-size: 0.9375rem; color: hsl(var(--muted-foreground));">Enter your email and we will send a link to reset your password.</p>
<div id="reset-message" role="status" aria-live="polite"></div>
<form class="lcars-form" hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML">
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-reset-email">Email</label><input class="lcars-input" type="email" name="email" id="lcars-reset-email" required></div>
<button type="submit" class="lcars-btn-primary" hx-disabled-elt="this" style="width:100%; justify-content:center;">Send reset link</button>
</form>
<a class="lcars-btn-ghost lcars-btn-sm" href="/login" style="align-self: flex-start;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#arrow-left"/></svg>Back to sign in</a>
</div>

View File

@ -0,0 +1 @@
<section class="lcars-section-tight {{ class }}" data-block="lcars:popular-posts">{% if heading %}<h2 class="lcars-kicker">{{ heading }}</h2>{% endif %}<ol class="lcars-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for post in posts %}<li style="display: flex; gap: 0.75rem; align-items: flex-start;">{% if showRank %}<span class="lcars-rank numeric-tabular">{{ forloop.Counter }}</span>{% endif %}{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="lcars-post-thumb lcars-media-frame" style="width: 3rem; height: 3rem; flex-shrink: 0;"><img src="{{ post.featured_image_url }}" alt="{{ post.title }}" style="width: 100%; height: 100%; object-fit: cover;"></a>{% endif %}<div style="min-width: 0;"><h3 class="lcars-post-title" style="font-size: 0.9375rem;"><a href="{{ post.url }}">{{ post.title }}</a></h3>{% if post.published_at %}<p class="lcars-post-meta"><span class="lcars-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span></p>{% endif %}</div></li>{% endfor %}</ol></section>

View File

@ -0,0 +1 @@
<section class="lcars-section {{ class }}" data-block="lcars:post-hero"><div class="lcars-content-well">{% if context.currentPost.featuredImageUrl %}<div class="lcars-media-frame" style="aspect-ratio: 21 / 9; margin-bottom: 2.5rem;"><img src="{{ context.currentPost.featuredImageUrl }}" alt="{{ context.currentPost.featuredImageAlt|default:context.currentPost.title }}" style="width: 100%; height: 100%; object-fit: cover;"></div>{% endif %}<div class="lcars-measure lcars-stack" style="{% if align == "left" %}text-align: left;{% else %}margin-left: auto; margin-right: auto; text-align: center;{% endif %}"><span class="lcars-mark lcars-mark-square" aria-hidden="true"{% if align != "left" %} style="margin-left: auto; margin-right: auto;"{% endif %}></span><h1 class="lcars-display" style="margin: 0;">{{ context.currentPost.title }}</h1>{% if showExcerpt and context.currentPost.excerpt %}<p class="lcars-lede" style="margin: 0;">{{ context.currentPost.excerpt }}</p>{% endif %}{% if showMeta %}<div class="lcars-post-meta"{% if align != "left" %} style="justify-content: center;"{% endif %}>{% if context.currentPost.authorName %}<span>{{ context.currentPost.authorName }}</span>{% endif %}{% if context.currentPost.publishedAt %}<span class="lcars-mono numeric-tabular">{{ context.currentPost.publishedAt }}</span>{% endif %}{% if context.currentPost.readingTimeMinutes %}<span class="lcars-mono numeric-tabular">{{ context.currentPost.readingTimeMinutes }} min read</span>{% endif %}</div>{% endif %}</div></div></section>

View File

@ -0,0 +1 @@
<div class="lcars-post-meta {{ class }}" data-block="lcars:post-metadata">{% if showAuthor and context.currentPost.authorName %}<span style="display: inline-flex; align-items: center; gap: 0.5rem;">{% if showAvatar and context.currentPost.authorAvatarUrl %}<img class="lcars-avatar" src="{{ context.currentPost.authorAvatarUrl }}" alt="{{ context.currentPost.authorName }}" style="width: 1.75rem; height: 1.75rem; border-radius: 9999px; object-fit: cover;">{% endif %}<span style="color: hsl(var(--foreground)); font-weight: 500;">{{ context.currentPost.authorName }}</span></span>{% endif %}{% if showDate and context.currentPost.publishedAt %}<time class="lcars-mono numeric-tabular" datetime="{{ context.currentPost.publishedAtIso }}">{{ context.currentPost.publishedAt }}</time>{% endif %}{% if showReadingTime and context.currentPost.readingTimeMinutes %}<span class="lcars-mono numeric-tabular">{{ context.currentPost.readingTimeMinutes }} min read</span>{% endif %}</div>

View File

@ -0,0 +1,14 @@
{# pricing override — flat hairline tiers, mono price figures, accent-top featured tier. #}
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="lcars-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if tiers %}<div class="lcars-grid lcars-grid-3">{% for tier in tiers %}<div class="lcars-card lcars-price-tier {% if tier.highlighted %}lcars-price-tier--featured lcars-card-accent{% endif %}" style="display: flex; flex-direction: column;">
{% if tier.badge %}<span class="lcars-badge" style="display: inline-block; align-self: flex-start; background: hsl(var(--accent)); color: hsl(var(--accent-foreground)); font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.25rem 0.5rem; border-radius: 4px; margin-bottom: 0.75rem;">{{ tier.badge }}</span>{% endif %}
{% if tier.name %}<h3 class="lcars-feature-title" style="margin: 0;">{{ tier.name }}</h3>{% endif %}
<p style="display: flex; align-items: baseline; gap: 0.25rem; margin: 1rem 0 0 0;"><span class="lcars-price numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 4vw, 2.75rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>{% if tier.period %}<span class="lcars-price-period lcars-stat-label" style="margin-top: 0;">{{ tier.period }}</span>{% endif %}</p>
{% if tier.description %}<p class="lcars-text" style="font-size: 0.9375rem; color: hsl(var(--muted-foreground)); margin-top: 0.75rem;">{{ tier.description }}</p>{% endif %}
{% if tier.features %}<ul class="lcars-stack-sm" style="list-style: none; padding: 0; margin: 1.5rem 0 0 0; display: flex; flex-direction: column; gap: 0.625rem; flex: 1;">{% for feature in tier.features %}<li class="lcars-check" style="display: flex; gap: 0.5rem; align-items: flex-start; font-size: 0.9375rem; color: hsl(var(--foreground));"><span style="color: hsl(var(--accent)); flex: none;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#check"/></svg></span><span>{{ feature }}</span></li>{% endfor %}</ul>{% endif %}
{% if tier.ctaUrl and tier.ctaLabel %}{% if tier.highlighted %}<a class="lcars-btn-primary" href="{{ tier.ctaUrl }}" style="margin-top: 1.5rem; width: 100%; box-sizing: border-box; text-align: center;">{{ tier.ctaLabel }}</a>{% else %}<a class="lcars-btn-outline" href="{{ tier.ctaUrl }}" style="margin-top: 1.5rem; width: 100%; box-sizing: border-box; text-align: center;">{{ tier.ctaLabel }}</a>{% endif %}{% endif %}
</div>{% endfor %}</div>{% endif %}
{% if billingNote %}<p class="lcars-stat-label" style="text-align: center; margin-top: 2rem;">{{ billingNote }}</p>{% endif %}
</div></section>

View File

@ -0,0 +1,6 @@
{# quote override — pull quote (accent top rule) / block quote (accent left rule) / inline card. #}
{% if style == "pull" %}{% set qcls = "lcars-pull-quote" %}{% set qsty = "margin:2rem 0;" %}{% elif style == "inline-card" %}{% set qcls = "lcars-quote-block lcars-card lcars-card-accent" %}{% set qsty = "margin:2rem 0;" %}{% else %}{% set qcls = "lcars-quote-block" %}{% set qsty = "margin:2rem 0;border-left:2px solid hsl(var(--accent));padding-left:1.25rem;" %}{% endif %}
<figure class="{{ qcls }} {{ class }}" style="{{ qsty }}{% if align == "center" %}text-align:center;{% endif %}">
<blockquote{% if cite %} cite="{{ cite }}"{% endif %} style="margin:0;border:0;padding:0;{% if style != "pull" %}font-size:1.25rem;line-height:1.5;color:hsl(var(--foreground));{% endif %}">{{ quote|safe }}</blockquote>
{% if attribution or role or cite %}<figcaption class="lcars-quote-cite" style="margin-top:1rem;font-size:0.875rem;color:hsl(var(--muted-foreground));">{% if attribution %}<span class="lcars-display" style="font-size:0.9375rem;color:hsl(var(--foreground));">{{ attribution }}</span>{% endif %}{% if role %}<span style="display:block;">{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" style="display:inline-block;margin-top:0.25rem;color:inherit;text-decoration:underline;text-underline-offset:2px;">Source</a>{% endif %}</figcaption>{% endif %}
</figure>

Some files were not shown because too many files have changed in this diff Show More