Compare commits

...

4 Commits

Author SHA1 Message Date
Alex Dunmow
6af6950f23 fix(scifi-clean): repair blank hero + whitespace/layout + mobile responsiveness
Layout / whitespace:
- Root-cause blank hero: seed passed hero styled-text (eyebrow/headline/
  subheadline) as bare strings, but the hero template does field.text|default:field
  and the ninjatpl engine throws a hard error on attribute-access-on-string, so the
  whole hero (and the builtin fallback) errored and rendered as an empty ~160px band.
  Seed now passes {text:...} objects (the builtin-sample / admin-editor format).
- tech_spec table: value + unit were both right-aligned, leaving a cavernous mid-gap;
  label column now absorbs slack and value+unit group together on the right. Box
  narrowed (34rem) with tighter padding so the HUD corners frame a deliberate panel.
- Tightened section rhythm (sc-section 4->3.5rem, sc-section-tight 2.5->2.75rem) and
  hero bottom padding to remove oversized inter-section gaps.
- Defined sc-article-measure (was referenced but undefined -> unbounded line length).

Empty-media graceful state (seed ships no images):
- rich-section and hero (split) rendered an empty aspect-ratio muted box when media
  was absent; media frame is now guarded and the grid collapses to one column.

Mobile responsiveness (explicit sc-* classes + @media, not JIT-scanned Tailwind):
- sc-form-grid (global + contact-form scoped) collapses to 1 column <=640px.
- gallery dropped inline grid-template-columns that overrode its responsive classes,
  so 3/4-col galleries now collapse on small screens.
- nav toggle tap target bumped to 42x42px.

Motion robustness:
- Added a safety-net full-reveal timeout to the scroll-reveal script so below-fold
  [data-sc-reveal] content is guaranteed visible on no-scroll screenshot captures and
  IntersectionObserver edge cases. Reduced-motion / no-JS already render fully.

Semantic tokens only; check-safety 32 checks 19 ok 13 skip -> OK; codeless build OK.
No version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:27:27 +08:00
Alex Dunmow
2163eb669b bump to 0.2.1 2026-07-05 16:44:49 +08:00
Alex Dunmow
f7f1932352 docs: publishing README 2026-07-05 13:44:03 +08:00
Alex Dunmow
46c9177265 feat(scifi-clean): Wave A theme — 49 overrides, 7 templates, persona blocks, seed
WO-TF-024. Re-skin from frozen Wave-0 builtins into blueprint/HUD/mono chrome
(AI/robotics product persona), preserving every content field, provider field,
custom tag, and JS block; button/card legacy field reads reconciled per §4.

- 49 mandatory template_overrides + 7 page templates (default, full-width,
  landing, article, blog-index, contact, auth) with MID-tier motion
  (JS scroll-reveal + ticking counters + CSS line-draw; reduced-motion + no-JS
  static fallback).
- 4 persona blocks: tech_spec, benchmark_table, telemetry_grid,
  changelog_console. Deleted 5 builtin-duplicating theme blocks (now overrides).
- 4 dual-mode presets (19 tokens, light + dark tuned).
- Bundled OFL fonts (Space Grotesk / Inter / JetBrains Mono) + LICENSES.
- Email-safe wrapper retuned; master pages + seed demo site (Halcyon Robotics:
  home, about, blog x3, contact with data-table + row_inserted email workflow,
  login).
- required_icon_packs (lucide, simple-icons) + RECOMMENDED docs.

make build exit 0 (codeless .bnp); check-safety exit 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:43:47 +08:00
92 changed files with 1965 additions and 745 deletions

View File

@ -1,182 +1,52 @@
# Sci-Fi Clean — Build Report (v0.1.0, wave-1 pass)
# Sci-Fi Clean — Build Report (Wave A, WO-TF-024)
Codeless `.bnp` theme. Persona: AI / robotics product. Blueprint schematics,
HUD guide lines, mono readouts, spec + benchmark furniture. Light = white-room
lab; dark = mission control. Motion tier MID (JS scroll-reveal + ticking
counters + CSS line-draw, no canvas, prefers-reduced-motion honored, static
no-JS fallback).
## What landed
### Module scaffolding
- `plugin.mod` with `kind = "theme"`, `scope = "@themes"`, all spec §2 fields verbatim
(categories `["templates", "developer"]`, 9 tags), `[compatibility] block_core = ">=0.11.0 <0.12.0"`.
- `go.mod` pinned to `git.dev.alexdunmow.com/block/core v0.11.1`, `go 1.26.4`, no `replace` directives.
- `Makefile` with `all` (default), `clean`, `templ`, `help` targets. The build line is
`CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o scifi-clean.so .`.
- `embed.go` declares all five canonical embed directives plus `ThemeCSSManifest()`.
- `manifest.yaml`: 7 page templates (default, full-width, landing, article,
blog-index, contact, auth), 49 mandatory `template_overrides`, bundled fonts,
presets, master pages, `required_icon_packs` (lucide, simple-icons), and the
`sc-` component CSS system (blueprint/HUD identity layer over a shared base).
- `templates/overrides/scifi-clean/`: all 49 mandatory builtin overrides,
re-skinned from the frozen Wave-0 builtin defaults with every content field,
provider field, custom tag, and JS block preserved. Legacy button/card field
reads reconciled (§4): button reads `label`/`link`/`style`; card reads
`text`/`media`.
- `templates/scifi-clean/`: 7 page-template shells with the MID-tier motion
script and a line-draw schematic on the hero.
- `blocks/`: 4 persona blocks — `tech_spec`, `benchmark_table`,
`telemetry_grid`, `changelog_console`. The 5 builtin-duplicating theme blocks
(footer, hero, stats, status bar, image caption) were deleted and are now
builtin overrides.
- `presets.json`: 4 dual-mode presets (Flightline, Cleanroom, Signal Cyan,
Graphite Lab), all 19 tokens, light AND dark tuned.
- `fonts.json` + `assets/fonts/`: Space Grotesk (heading), Inter (body),
JetBrains Mono (mono), all SIL OFL 1.1 — see `assets/fonts/LICENSES.md`.
- `templates/email/scifi-clean.ninjatpl`: email-safe table wrapper retuned to
the scifi palette.
- `master_pages.json` + `seed/seed.json` + `seed/workflows.json`: a seeded
"Halcyon Robotics" demo site (home, about, blog with 3 posts, contact with a
data-table + row_inserted email-admins workflow, login).
### Registration
- One `RegisterSystemTemplate({Key: "scifi-clean", ...})`.
- Four page templates per spec §6:
- `default` — slots `header, main, footer`
- `landing` — slots `hero, specs, main, cta, footer`
- `article` — slots `header, rail, main, footer`
- `full-width` — slots `header, main, footer`
- `br.LoadSchemasFromFS(Schemas())` runs **before** any `br.Register(...)`. UAT §3.6.
- Six theme blocks registered: `tech_spec`, `diagram_caption`, `mission_stat`,
`status_bar`, `footer`, `schematic_hero`. Each has `Source = "scifi-clean"` and an
unqualified `Key`. Addressed at runtime as `scifi-clean:<key>`.
- Three overrides registered against theme key `"scifi-clean"`:
- `heading → ScifiHeadingBlock` (Space Grotesk via `--font-heading`, optional mono kicker)
- `text → ScifiTextBlock` (Inter via `--font-body`, tabular-nums)
- `button → ScifiButtonBlock` (mono uppercase label, literal `→` U+2192 chevron + `data-icon="chevron-right"` for UAT §13.14)
- `tr.RegisterEmailWrapper("scifi-clean", ScifiEmailWrapper)` wires the 600px white-card
email shell with Space Grotesk display lockup top-left and mono callsign top-right.
## Verification
### Master pages
`DefaultMasterPages()` returns two entries:
- `make build` -> exit 0 (codeless `.bnp`, 4 blocks, no wasm).
- `check-safety` -> exit 0 (32 checks: 19 ok, 13 skip).
- `scifi-clean:default-master`, attached to `default`, `article`, `full-width`.
Blocks: `navbar` (header, 0) + `scifi-clean:status_bar` (header, 10) +
`slot` (main, 0, `{"slotName": "main"}`) + `scifi-clean:footer` (footer, 0).
Matches spec §7 / UAT §9 byte-for-byte.
- `scifi-clean:landing-master`, attached to `landing`.
Pre-populates `hero` with `scifi-clean:mission_stat` and `specs` with
`scifi-clean:tech_spec`, both seeded with example payloads.
## Known platform limits (per OVERRIDE-CONTRACT §11/§12)
### Schemas
Six `schemas/*.schema.json` files, all draft-07, x-editor values restricted to the
allowed set (`text`, `richtext`, `media`, `select`, `number`, `array`, `collection`,
`link`). Property names match the corresponding Go content reads exactly.
- Auth trio (`auth-form`, `auth-status`, `password-reset-form`) and
`page-suggestions` are compiled builtins with no override dispatch — the
themed files are correct-to-model and inert until the platform fix lands.
- Provider-backed overrides (navbar/footer menus, featured/related/popular
posts, category-list, breadcrumbs, blog-hero, sidebar-nav, author-bio-hero)
render themed chrome but empty live lists under the codeless loader until the
loader registers overrides via the source path. Authored faithfully to the
builtin provider var names.
### Presets
`presets.json` is a JSON array of length 3 in the order required by UAT §5.1:
1. `flightline` — mode `light`, only `lightColors`. Declares
`"primary": "215 90% 45%"` and `"accent": "18 95% 55%"` byte-exactly (UAT §13.1).
2. `mission-control` — mode `dark`, only `darkColors`. Declares
`"background": "220 16% 7%"` and `"accent": "18 100% 60%"` byte-exactly (UAT §13.2).
3. `cleanroom` — mode `both`, **both** `lightColors` and `darkColors`.
All three presets carry all 19 tokens in each declared color block. Every value is
an HSL triple string of the shape `^\d+ \d+% \d+%$` (no `hsl()` wrappers).
### CSS manifest
`ThemeCSSManifest()` returns a `*plugin.CSSManifest` whose `InputCSSAppend`
contains the literal substrings UAT §13.4 grep-greps: `'Space Grotesk'`,
`'JetBrains Mono'`, `.hairline`, `.bg-grid`. The `.hairline` declaration is
exactly `border: 1px solid hsl(var(--border));` (UAT §13.5).
The manifest also defines:
- Root-level CSS-variable fallback stacks for `--font-heading`, `--font-body`,
`--font-mono` so the theme looks correct before the admin picks Google Fonts.
- `.bg-grid` blueprint grid utility.
- `.scrim` utility for full-bleed hero text (UAT §6 scrim requirement).
- `.scifi-focus` outlined focus ring derived from `--ring`.
- `.scifi-chevron` Unicode arrow suffix for button labels.
### Fonts policy (wave-1)
- `fonts.json` is the literal `[]`. No woff2 files bundled in this pass.
- `RECOMMENDED_FONTS.md` at the theme root lists Space Grotesk / Inter /
JetBrains Mono as Google Fonts picker recommendations.
- No `LICENSES.md` (nothing is bundled to license).
## Build output
```
$ cd ~/src/blockninja/themes/scifi-clean && make clean && make
rm -f scifi-clean.so
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o scifi-clean.so .
$ ls -la scifi-clean.so
-rw-rw-r-- 1 alex alex 21535264 ... scifi-clean.so (≈ 20.5 MiB)
$ file scifi-clean.so
ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped
```
`make 2>&1 | grep -Ei 'warning|error'` returns empty. `go mod tidy` is a no-op
after the initial run; the resolved `block/core` version is `v0.11.1`.
## Safety check
Run from the standalone check-safety module (at `~/src/blockninja/check-safety/`,
not `backend/cmd/check-safety` — that path does not exist in this tree):
```
$ cd ~/src/blockninja/check-safety && \
go run . ~/src/blockninja/themes/scifi-clean \
--plugin-dir ~/src/blockninja/themes/scifi-clean
...
EXIT: 0
```
All 22 checks PASS or SKIP. Specifically:
- Check 2c (SDK import boundary): OK on v0.11.1.
- Check 6 (no hardcoded colors in .templ/.ninjatpl files): OK.
- Check 11 (no placeholder code): OK.
- Check 17 (no TODO markers): OK.
- Check 21 (presets.json validation against `theme.Theme`): OK.
The only non-OK output is **Check 2e** (any usage), which is a **WARN**, not a FAIL.
The 34 warnings are the expected `map[string]any` content map signature for plugin
block render funcs — that is the SDK's required block-func shape and cannot be
avoided in a standalone plugin. Same as `gotham/` and `lcars/`.
## Open items / deferred
These are intentionally out of scope for the wave-1 implementation pass:
### Fonts
- No woff2 files bundled (`fonts.json = []` per `themes/docs/FONTS.md`).
Wave-2 may bundle commercial display faces (Eurostile when licensed,
otherwise stick with Space Grotesk) under `assets/fonts/web/`.
- Section §11 of the UAT (woff2 file presence, `@font-face` count, network
fetch checks) is superseded by the FONTS.md wave-1 policy, which only
requires `fonts.json` to parse as JSON, `RECOMMENDED_FONTS.md` to exist,
and CSS to consume `var(--font-*)`. All three pass.
### Live instance / `make rebuild`
- `make rebuild` is **not** wired in this `Makefile`. Adding it requires the
`blockninja-go-builder` podman image and the live CMS at `~/src/blockninja`.
Copy gotham's `rebuild`, `backend`, `build-frontend`, `copy-plugin-source`,
`build-so`, `sync-migrations`, `build-css`, `deploy-css`, `logs`, `status`
targets when the next implementation pass needs container deployment.
### Marketplace assets (UAT §12)
- No screenshots captured (`docs/uat-evidence/screenshots/`). Requires a
running instance.
- No `Project Aurora` demo content seeded. Requires admin tooling and a
populated database.
- No `docs/launch-copy.txt`. Copy is in spec §13 verbatim; persist when the
marketplace listing is created.
### Versioning / git
- The theme is not under git in this scope; UAT §1.11 (`git describe --tags`)
cannot be evaluated until the directory is initialised as a git repo and
tagged `v0.1.0`.
### Email wrapper hex fallbacks
- `email_wrapper.templ` carries a small set of hardcoded hex values
(`#F6F7F8`, `#FFFFFF`, `#D6D9DD`, `#16202E`, `#5C6776`) that act as the
ultimate fallback when `EmailContext.Colors` is empty. Email clients
(Outlook in particular) cannot resolve `hsl(var(--token))`, so emails
must inline hex; the runtime path always passes preset-resolved hex
via `EmailContext`. Check-safety §6 only scans `.templ` files for the
hardcoded-colors rule and currently passes (the hex literals live inside
Go fallback funcs and are explicitly out of the templ-emit path). UAT
§5.5 may need a clarifying note that email fallbacks are intentional.
### `Hidden` blocks
- None of the six theme blocks set `Hidden: true`; the spec did not request
any. (Gotham hides its `stat_item` child block; the scifi-clean equivalent
is the in-`rows` collection on `tech_spec`, which is schema-only and never
registered as a standalone block, so no `Hidden` flag is needed.)
### Block category metadata
- All six theme blocks set `Category` to one of the SDK constants
(`CategoryContent`, `CategoryNavigation`, `CategoryLayout`). The spec's
§8 table lists category names like `media` and `blog` that do not map
to SDK constants; those are stored implicitly via the spec source rather
than the BlockMeta — flag in BUILD_REPORT for future enum extension.
### Slot block placeholder
- Master pages reference the built-in `slot` block with
`{"slotName": "main", "placeholder": "Page payload"}`. The `placeholder`
key is a CMS-side convention copied verbatim from gotham; it is **not**
the same as the check-safety §11 "no placeholder code" rule (which fires
on dev-time comments like `// TODO: placeholder`). Check-safety passes.
Visual quality is UNVERIFIED until Wave B screenshots.

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Sci-Fi Clean
A minimalist futurist theme in off-white with technical blue and a signal-orange accent. Thin blueprint lines, mono numerals, and spec furniture give it the calm of a mission-control readout. It reads precise and technical, and it stays quiet where a product deserves to speak for itself.
## Good for
We built Sci-Fi Clean for hardware and research teams. Treat these as starting points, not limits. If the mood fits your product, use it:
- AI and robotics product sites
- Aerospace and hardware companies
- R&D labs and biotech
- Developer platforms and technical documentation
## What you get
- Four color presets, each with a tuned light mode and a tuned dark mode: Flightline, Cleanroom, Signal Cyan, and Graphite Lab.
- Space Grotesk headings, Inter body, and JetBrains Mono for readouts, bundled and ready. Fonts stay admin-controlled, so you can swap them from the typography settings.
- Four persona blocks: a technical spec sheet, a benchmark comparison table, a live telemetry grid, and a changelog console.
- A demo site that installs when you activate the theme: a home page, an about page, a short blog, 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.

View File

@ -1,33 +1,32 @@
# Recommended fonts for Sci-Fi Clean
This theme ships `fonts.json = []` — no woff2 files bundled in this implementation pass.
Add the fonts below from the site admin's Google Fonts picker.
This theme **bundles** its type. `fonts.json` ships latin-subset woff2 files in
`assets/fonts/` (all SIL OFL 1.1 — see `assets/fonts/LICENSES.md`), so the
intended aesthetic renders out of the box with no admin setup.
Open **Admin → Settings → Typography**, switch to the **Google Fonts** tab, and assign:
| Slot | Bundled family | Weights | Role |
|---------|---------------------|-----------|---------------------------------------------------|
| Heading | `Space Grotesk` | 500, 700 | Geometric display face for headlines and brand. |
| Body | `Inter` | 400, 600 | Workhorse UI sans for prose and controls. |
| Mono | `JetBrains Mono` | 400, 500 | Every numeral, identifier, readout, and spec row. |
| Slot | Pick | Source | Notes |
|----------|---------------------------------------|----------------|---------------------------------------------------------|
| Heading | `Space Grotesk` (weights 400, 500, 700) | `google:Space Grotesk` | Geometric display face. Spec §3 calls it "the safe ship default" over Eurostile. |
| Body | `Inter` (weights 400, 500, 600) | `google:Inter` | Workhorse UI sans. Pairs cleanly with Space Grotesk. |
| Mono | `JetBrains Mono` (weights 400, 500) | `google:JetBrains Mono` | Used for every numeral, identifier, and tabular figure. |
All three are in the curated Google Fonts list; no upload is required.
The bundled families appear as `source=template` rows in **Admin → Settings →
Typography**. Fonts stay admin-controlled: an admin can override any slot from
the Google Fonts tab or an upload, and the theme follows because every
`font-family` goes through `var(--font-heading|body|mono, <fallback>)`.
## CSS fallback stacks
Until the admin assigns fonts, templates fall through to these CSS-variable fallbacks
(declared in the theme's CSS manifest, applied to `:root`):
Until (or unless) the admin reassigns a slot, templates fall through to these
CSS-variable fallbacks (declared on `:root` in the theme CSS manifest):
- `--font-heading` `"Space Grotesk", "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif`
- `--font-body` `"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif`
- `--font-mono` `"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`
- `--font-heading` -> `"Space Grotesk", "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif`
- `--font-body` -> `"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif`
- `--font-mono` -> `"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`
The fallback stacks intentionally already approximate the intended aesthetic, so the
theme looks right out of the box even before the admin picks Google Fonts.
## Good alternates
## Why no bundled woff2s in this pass
Per `themes/docs/FONTS.md` (wave-1 policy): no theme bundles woff2 files in this
implementation pass. Wave-2 may bundle commercial display faces (e.g. Eurostile)
once licensing for `.so` redistribution is sorted; until then, Space Grotesk is
the canonical heading face for Sci-Fi Clean.
If an admin prefers to swap the mono readout face, `Space Mono` (OFL) and
`IBM Plex Mono` (OFL) both hold tabular numerals well. For headings, `Sora`
(OFL) is a close geometric cousin of Space Grotesk. All are in the curated
Google Fonts list; no upload required.

26
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,26 @@
# Recommended icons for Sci-Fi Clean
Icons render only via the sprite system:
`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`. Block content stores refs
as `"pack:name"` strings (e.g. `lucide:mail`).
## Required packs
Declared in `plugin.mod` `required_icon_packs` and `manifest.yaml`
`required_icon_packs`. Until the loader auto-installs, install these manually
before activation:
| Pack | Slug | Why |
|----------------|----------------|------------------------------------------------------------|
| Lucide | `lucide` | Default UI icon set: nav toggle, arrows, mail, phone, home. |
| Simple Icons | `simple-icons` | Brand marks in social links and the author bio hero (X, LinkedIn, GitHub). |
Both are among the six packs that ship bundled with the CMS, so no download is
usually needed. If a pack is missing on a fresh instance, open **Admin ->
Icons**, install Lucide and Simple Icons, and enable them.
## Defaults are safe
Every block that renders an icon defaults to a `lucide:` glyph, so the theme
renders correctly even if Simple Icons is not yet installed (brand links simply
fall back to text/label until the pack is present).

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

@ -0,0 +1,15 @@
# Bundled font licenses
All bundled woff2 files in this directory are latin-subset builds redistributed
under the SIL Open Font License 1.1 (OFL). No modifications were made beyond
subsetting to the latin range. Font-family stays admin-controlled; these appear
as `source=template` rows in the font picker and are consumed only via
`var(--font-heading|body|mono, <fallback>)`.
| Family | Files | License | Source |
|--------|-------|---------|--------|
| Space Grotesk | space-grotesk-500.woff2, space-grotesk-700.woff2 | SIL OFL 1.1 | Florian Karsten / Google Fonts |
| Inter | inter-400.woff2, inter-600.woff2 | SIL OFL 1.1 | Rasmus Andersson / Google Fonts |
| JetBrains Mono | jetbrains-mono-400.woff2 (variable; theme uses 400 and 500) | SIL OFL 1.1 | JetBrains / Google Fonts |
Full OFL text: https://openfontlicense.org/.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<section data-block="scifi-clean:benchmark_table" class="sc-section-tight {{ class }}"><div class="sc-content-well"><div style="max-width: 44rem;">{% if heading %}<p class="sc-eyebrow">{{ eyebrow|default:"Benchmarks" }}</p><h2 class="sc-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0 0 1.5rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}{% if rows %}{% for row in rows %}<div class="sc-bench-row" data-sc-reveal><span class="sc-bench-name">{{ row.label }}</span><span class="sc-bench-value">{{ row.value }}{% if row.unit %} <span class="sc-telemetry-unit">{{ row.unit }}</span>{% endif %}</span><span class="sc-bench-track"><span class="sc-bench-fill{% if row.highlight %} sc-bench-fill--accent{% endif %}" style="width: {{ row.bar|default:0 }}%;"></span></span></div>{% endfor %}{% else %}<p class="sc-text" style="color: hsl(var(--muted-foreground));">No benchmark rows yet.</p>{% endif %}{% if note %}<p class="sc-form-note" style="margin-top: 1rem;">{{ note }}</p>{% endif %}</div></div></section>

View File

@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Benchmark Table",
"description": "Labelled benchmark bars with mono values and a technical-blue fill track.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small mono label above the heading.",
"x-editor": "text"
},
"heading": {
"type": "string",
"title": "Heading",
"description": "Section heading.",
"x-editor": "text"
},
"rows": {
"type": "array",
"title": "Rows",
"description": "Each row is a labelled benchmark bar.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": { "type": "string", "title": "Label", "description": "Benchmark name.", "x-editor": "text" },
"value": { "type": "string", "title": "Value", "description": "Displayed value (e.g. 98.4).", "x-editor": "text" },
"unit": { "type": "string", "title": "Unit", "description": "Trailing unit (e.g. ms, %, tok/s).", "x-editor": "text" },
"bar": { "type": "number", "title": "Bar %", "description": "Fill width 0-100.", "default": 0, "minimum": 0, "maximum": 100, "x-editor": "number" },
"highlight": { "type": "boolean", "title": "Highlight", "description": "Fill this bar with the signal accent instead of technical blue.", "default": false, "x-editor": "select" }
},
"required": ["label", "value"]
}
},
"note": {
"type": "string",
"title": "Note",
"description": "Optional footnote below the bars.",
"x-editor": "text"
}
}
}

View File

@ -1,42 +1,32 @@
# Sci-Fi Clean theme block definitions (codeless .bnp, WO-WZ-021).
# Keys are fully qualified (scifi-clean:<key>) because the codeless loader
# registers definitions verbatim — there is no PluginBlockRegistry prefixing
# in the codeless path (unlike the wasm/templ path). The qualified keys match
# the hardcoded data-block attributes and the master_pages.json references.
# Sci-Fi Clean theme — persona block definitions (codeless .bnp).
# Keys are fully qualified (scifi-clean:<key>): the codeless loader registers
# definitions verbatim (no PluginBlockRegistry prefixing in the codeless path),
# and the qualified keys match the data-block attributes + master_pages / seed
# references. These four are TRUE persona furniture that no builtin covers;
# every duplicating theme block (footer, hero, stats, status bar, image
# caption) is now a builtin OVERRIDE under templates/overrides/scifi-clean/.
blocks:
- key: scifi-clean:tech_spec
title: Tech Spec Table
description: Specification table with right-aligned mono numerals and hairline rows.
description: Specification sheet with right-aligned mono numerals, units, and hairline rows.
category: content
schema: tech_spec.schema.json
template: tech_spec.ninjatpl
- key: scifi-clean:diagram_caption
title: Diagram Caption
description: Annotated technical figure with a Fig. NN. heading and optional callout pins.
- key: scifi-clean:benchmark_table
title: Benchmark Table
description: Labelled benchmark bars with mono values and a technical-blue fill track.
category: content
schema: diagram_caption.schema.json
template: diagram_caption.ninjatpl
- key: scifi-clean:mission_stat
title: Mission Stat
description: Headline metric in mono numerals with a signal-orange delta arrow.
schema: benchmark_table.schema.json
template: benchmark_table.ninjatpl
- key: scifi-clean:telemetry_grid
title: Telemetry Grid
description: Grid of HUD readout tiles with mono figures, units, deltas, and optional ticking counters.
category: content
schema: mission_stat.schema.json
template: mission_stat.ninjatpl
- key: scifi-clean:status_bar
title: Status Bar
description: Persistent top rail with optional UTC clock, build hash, and callsign.
category: navigation
schema: status_bar.schema.json
template: status_bar.ninjatpl
- key: scifi-clean:footer
title: Instrument Footer
description: Hairline-rule footer with callsign, optional signup, and link list.
category: layout
schema: footer.schema.json
template: footer.ninjatpl
- key: scifi-clean:schematic_hero
title: Schematic Hero
description: Landing hero with a large display title over a blueprint image and dual CTAs.
category: layout
schema: schematic_hero.schema.json
template: schematic_hero.ninjatpl
schema: telemetry_grid.schema.json
template: telemetry_grid.ninjatpl
- key: scifi-clean:changelog_console
title: Changelog Console
description: Terminal-style release log with version, date, and note per entry.
category: content
schema: changelog_console.schema.json
template: changelog_console.ninjatpl

View File

@ -0,0 +1 @@
<section data-block="scifi-clean:changelog_console" class="sc-section-tight {{ class }}"><div class="sc-content-well"><div style="max-width: 44rem;">{% if heading %}<p class="sc-eyebrow">{{ eyebrow|default:"Release log" }}</p><h2 class="sc-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0 0 1.5rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}<div class="sc-console" data-sc-reveal><div class="sc-console-bar"><span class="sc-console-dot"></span><span class="sc-console-dot"></span><span class="sc-console-dot"></span><span class="sc-console-title">{{ title|default:"changelog" }}</span></div><div class="sc-console-body">{% if entries %}{% for entry in entries %}<div class="sc-console-entry"><span class="sc-console-ver">{{ entry.version }}</span><span class="sc-console-date">{{ entry.date }}</span><span class="sc-console-note">{{ entry.note }}</span></div>{% endfor %}{% else %}<div class="sc-console-entry"><span class="sc-console-note sc-console-prompt">no entries logged</span></div>{% endif %}</div></div></div></div></section>

View File

@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Changelog Console",
"description": "Terminal-style release log with version, date, and note per entry.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small mono label above the heading.",
"x-editor": "text"
},
"heading": {
"type": "string",
"title": "Heading",
"description": "Section heading.",
"x-editor": "text"
},
"title": {
"type": "string",
"title": "Console title",
"description": "Label shown in the console title bar.",
"default": "changelog",
"x-editor": "text"
},
"entries": {
"type": "array",
"title": "Entries",
"description": "Each entry is a version / date / note row.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"version": { "type": "string", "title": "Version", "description": "Release tag (e.g. v2.4.0).", "x-editor": "text" },
"date": { "type": "string", "title": "Date", "description": "Release date.", "x-editor": "text" },
"note": { "type": "string", "title": "Note", "description": "One-line summary of the change.", "x-editor": "text" }
},
"required": ["version", "note"]
}
}
}
}

View File

@ -1,9 +0,0 @@
<figure data-block="scifi-clean:diagram_caption" class="my-8 max-w-3xl mx-auto px-4">
<div class="relative hairline bg-card">
{% if image %}<img src="{{ image }}" alt="{{ title }}" class="block w-full h-auto">{% for pin in calloutPins %}<span class="absolute inline-flex items-center scifi-mono text-xs uppercase tracking-wider px-2 py-1 bg-card text-foreground hairline" style="left: {{ pin.x|floatformat:2 }}%; top: {{ pin.y|floatformat:2 }}%;">{{ pin.label }}</span>{% endfor %}{% else %}<div class="bg-grid aspect-video flex items-center justify-center"><span class="scifi-mono text-sm text-muted-foreground uppercase tracking-widest">No diagram yet</span></div>{% endif %}
</div>
<figcaption class="mt-3 hairline-t pt-3">
<h4 class="scifi-h4 scifi-mono text-sm uppercase tracking-widest text-foreground">Fig. {% if figureNumber %}{{ figureNumber }}{% else %}00{% endif %} . {% if title %}<span class="text-muted-foreground ml-2">{{ title }}</span>{% endif %}</h4>
{% if body %}<div class="scifi-body prose prose-sm max-w-none text-muted-foreground mt-2">{{ body|safe }}</div>{% endif %}
</figcaption>
</figure>

View File

@ -1,67 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Diagram Caption",
"description": "Annotated technical figure with a Fig. NN. heading and optional callout pins.",
"type": "object",
"properties": {
"image": {
"type": "string",
"title": "Image",
"description": "Diagram / blueprint / schematic to display.",
"x-editor": "media"
},
"figureNumber": {
"type": "string",
"title": "Figure Number",
"description": "Number printed after the Fig. prefix (e.g. 03).",
"x-editor": "text"
},
"title": {
"type": "string",
"title": "Title",
"description": "Short heading next to the figure number.",
"x-editor": "text"
},
"body": {
"type": "string",
"title": "Body",
"description": "Rich description / annotation text rendered below the figure.",
"x-editor": "richtext"
},
"calloutPins": {
"type": "array",
"title": "Callout Pins",
"description": "Percent-positioned overlay pins; each pin is x, y, label.",
"default": [],
"x-editor": "array",
"items": {
"type": "object",
"properties": {
"x": {
"type": "number",
"title": "X (%)",
"description": "Horizontal position as a percentage of the image width (0-100).",
"minimum": 0,
"maximum": 100,
"x-editor": "number"
},
"y": {
"type": "number",
"title": "Y (%)",
"description": "Vertical position as a percentage of the image height (0-100).",
"minimum": 0,
"maximum": 100,
"x-editor": "number"
},
"label": {
"type": "string",
"title": "Label",
"description": "Short callout text.",
"x-editor": "text"
}
},
"required": ["label"]
}
}
}
}

View File

@ -1,24 +0,0 @@
<div data-block="scifi-clean:footer" class="w-full">
<div class="hairline-t pt-8 pb-10">
<div class="max-w-6xl mx-auto px-4">
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-8">
<div class="space-y-2">
<div data-scifi-callsign class="scifi-mono uppercase tracking-widest text-xs text-foreground" style="font-family: var(--font-mono);">{{ callsign|default:"SCF-CLN" }}</div>
<p class="scifi-body text-sm text-muted-foreground max-w-md">Telemetry, mission briefs, and engineering notes from this surface.</p>
</div>
{% if links %}<ul class="space-y-2 scifi-body text-sm">{% for link in links %}<li><a href="{% if link.url %}{{ link.url }}{% else %}#{% endif %}" class="text-muted-foreground hover:text-foreground transition-colors">{{ link.text }}</a></li>{% endfor %}</ul>{% endif %}
{% if showSignup == true or showSignup == "true" %}<form class="space-y-2 max-w-xs" method="post" action="/subscribe">
<label class="scifi-mono uppercase text-xs tracking-widest text-muted-foreground block">Mission updates</label>
<div class="flex hairline">
<input type="email" name="email" required placeholder="ops@station.local" class="scifi-body flex-1 px-3 py-2 bg-card text-foreground text-sm focus:outline-none scifi-focus">
<button type="submit" class="scifi-mono uppercase text-xs tracking-widest px-4 py-2 bg-primary text-primary-foreground scifi-chevron scifi-focus">Subscribe</button>
</div>
</form>{% endif %}
</div>
<div class="mt-8 hairline-t pt-4 flex items-center justify-between scifi-mono text-xs uppercase tracking-widest text-muted-foreground tabular-nums">
<span>Project Aurora</span>
<span>v0.1.0</span>
</div>
</div>
</div>
</div>

View File

@ -1,46 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Instrument Footer",
"description": "Hairline-rule footer with callsign, optional signup, and link list.",
"type": "object",
"properties": {
"showSignup": {
"type": "boolean",
"title": "Show Signup",
"description": "Render the inline email signup form.",
"default": false,
"x-editor": "select"
},
"callsign": {
"type": "string",
"title": "Callsign",
"description": "Short mono identifier displayed at the corner of the footer rail.",
"x-editor": "text"
},
"links": {
"type": "array",
"title": "Links",
"description": "Footer link list (each item is text + url).",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "Link Text",
"description": "Visible link text.",
"x-editor": "text"
},
"url": {
"type": "string",
"title": "URL",
"description": "Destination URL.",
"x-editor": "text"
}
},
"required": ["text"]
}
}
}
}

View File

@ -1,8 +0,0 @@
<div data-block="scifi-clean:mission_stat" class="hairline p-6 bg-card text-card-foreground">
{% if metric %}<div class="scifi-mono uppercase text-xs tracking-widest text-muted-foreground mb-3">{{ metric }}</div>{% endif %}
<div class="flex items-baseline gap-2">
<span class="scifi-mono text-5xl font-medium tabular-nums text-foreground" style="font-family: var(--font-mono); font-variant-numeric: tabular-nums;">{% if value %}{{ value }}{% else %}—{% endif %}</span>
{% if unit %}<span class="scifi-mono text-base text-muted-foreground tabular-nums" style="font-family: var(--font-mono); font-variant-numeric: tabular-nums;">{{ unit }}</span>{% endif %}
</div>
{% if delta %}<div class="mt-3 scifi-mono text-sm tabular-nums" style="font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: hsl(var(--accent));">{% if trend == "up" %}↑{% elif trend == "down" %}↓{% else %}→{% endif %} {{ delta }}</div>{% endif %}
</div>

View File

@ -1,40 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Mission Stat",
"description": "Headline metric in mono numerals with a signal-orange delta arrow.",
"type": "object",
"properties": {
"metric": {
"type": "string",
"title": "Metric",
"description": "Name of the measured quantity (e.g. Apogee).",
"x-editor": "text"
},
"value": {
"type": "string",
"title": "Value",
"description": "Headline numeric value (e.g. 412).",
"x-editor": "text"
},
"unit": {
"type": "string",
"title": "Unit",
"description": "Trailing unit (e.g. km, m/s).",
"x-editor": "text"
},
"delta": {
"type": "string",
"title": "Delta",
"description": "Short delta string (e.g. +12, -0.3%).",
"x-editor": "text"
},
"trend": {
"type": "string",
"title": "Trend",
"description": "Direction of the delta arrow.",
"default": "flat",
"enum": ["up", "down", "flat"],
"x-editor": "select"
}
}
}

View File

@ -1,13 +0,0 @@
<section data-block="scifi-clean:schematic_hero" class="relative overflow-hidden bg-grid">
{% if image %}<div class="absolute inset-0 z-0"><img src="{{ image }}" alt="" class="w-full h-full object-cover opacity-25"></div>{% endif %}
<div class="scrim relative z-10">
<div class="max-w-6xl mx-auto px-4 py-24 md:py-32">
{% if kicker %}<div class="scifi-mono uppercase tracking-widest text-xs text-accent mb-4">{{ kicker }}</div>{% endif %}
<h1 class="scifi-h1 font-medium tracking-tight text-foreground" style="font-family: var(--font-heading); font-size: clamp(3rem, 6vw, 5rem); line-height: 1.05;">{% if title %}{{ title }}{% else %}Untitled mission{% endif %}</h1>
<div class="mt-8 flex flex-wrap gap-3">
{% if primaryCta.text and primaryCta.href %}<a href="{{ primaryCta.href }}" class="scifi-mono uppercase tracking-widest text-xs px-5 py-3 bg-primary text-primary-foreground scifi-chevron scifi-focus">{{ primaryCta.text }}</a>{% endif %}
{% if secondaryCta.text and secondaryCta.href %}<a href="{{ secondaryCta.href }}" class="scifi-mono uppercase tracking-widest text-xs px-5 py-3 hairline text-foreground scifi-chevron scifi-focus">{{ secondaryCta.text }}</a>{% endif %}
</div>
</div>
</div>
</section>

View File

@ -1,62 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Schematic Hero",
"description": "Landing hero with a large display title over a blueprint image and dual CTAs.",
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"description": "Main display headline (rendered in Space Grotesk at >= 64px).",
"x-editor": "text"
},
"kicker": {
"type": "string",
"title": "Kicker",
"description": "Short mono uppercase eyebrow text printed above the title.",
"x-editor": "text"
},
"image": {
"type": "string",
"title": "Background Image",
"description": "Blueprint / schematic image rendered behind the title.",
"x-editor": "media"
},
"primaryCta": {
"type": "object",
"title": "Primary CTA",
"description": "Primary call-to-action link.",
"x-editor": "link",
"properties": {
"text": {
"type": "string",
"title": "Text",
"x-editor": "text"
},
"href": {
"type": "string",
"title": "URL",
"x-editor": "text"
}
}
},
"secondaryCta": {
"type": "object",
"title": "Secondary CTA",
"description": "Optional secondary call-to-action link.",
"x-editor": "link",
"properties": {
"text": {
"type": "string",
"title": "Text",
"x-editor": "text"
},
"href": {
"type": "string",
"title": "URL",
"x-editor": "text"
}
}
}
}
}

View File

@ -1,27 +0,0 @@
<div data-block="scifi-clean:status_bar" class="w-full hairline-b bg-card text-card-foreground scifi-mono text-xs uppercase tracking-widest" style="position: sticky; top: 0; z-index: 30;">
<div class="max-w-6xl mx-auto px-4 py-2 flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="text-muted-foreground">{{ callsign|default:"SCF-CLN" }}</span>
<span class="text-muted-foreground">|</span>
<span class="text-foreground">PUBLIC</span>
</div>
<div class="flex items-center gap-3 tabular-nums">
{% if showClock != false and showClock != "false" %}<span data-status-bar-clock="utc" class="text-muted-foreground tabular-nums">00:00:00 UTC</span>{% endif %}
{% if showBuild != false and showBuild != "false" %}<span class="text-muted-foreground">|</span>
<span class="text-foreground tabular-nums">BLD a1b2c3</span>{% endif %}
</div>
</div>
<script>
(function(){
var el = document.querySelector('[data-status-bar-clock="utc"]');
if (!el) return;
function fmt(n){ return n < 10 ? '0' + n : String(n); }
function tick(){
var d = new Date();
el.textContent = fmt(d.getUTCHours()) + ':' + fmt(d.getUTCMinutes()) + ':' + fmt(d.getUTCSeconds()) + ' UTC';
}
tick();
setInterval(tick, 1000);
})();
</script>
</div>

View File

@ -1,28 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Status Bar",
"description": "Persistent top rail with optional UTC clock, build hash, and callsign.",
"type": "object",
"properties": {
"showClock": {
"type": "boolean",
"title": "Show Clock",
"description": "Show a live UTC clock element.",
"default": true,
"x-editor": "select"
},
"showBuild": {
"type": "boolean",
"title": "Show Build Hash",
"description": "Show the short build hash on the right rail.",
"default": true,
"x-editor": "select"
},
"callsign": {
"type": "string",
"title": "Callsign",
"description": "Short mono identifier for the running surface (e.g. SCF-CLN).",
"x-editor": "text"
}
}
}

View File

@ -1,10 +1 @@
<section data-block="scifi-clean:tech_spec" class="py-8">
<div class="max-w-3xl mx-auto px-4">
{% if caption %}<p class="scifi-mono text-xs uppercase tracking-widest text-muted-foreground mb-3">{{ caption }}</p>{% endif %}
<table class="w-full text-sm hairline">
<tbody>
{% if rows %}{% for row in rows %}<tr class="{% if not forloop.Last %}hairline-b{% endif %}"><td class="scifi-body py-3 px-4 text-muted-foreground uppercase text-xs tracking-wider">{{ row.label }}</td><td class="scifi-mono py-3 px-4 text-right tabular-nums text-foreground font-medium" style="font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right;">{{ row.value }}</td><td class="scifi-mono py-3 pr-4 text-right text-muted-foreground text-xs" style="font-family: var(--font-mono); text-align: right;">{{ row.unit }}</td></tr>{% endfor %}{% else %}<tr class="hairline-b"><td class="scifi-body py-3 px-4 text-foreground">No spec rows yet.</td><td class="scifi-mono py-3 px-4 text-right tabular-nums text-muted-foreground">--</td><td class="scifi-mono py-3 pr-4 text-right text-muted-foreground"></td></tr>{% endif %}
</tbody>
</table>
</div>
</section>
<section data-block="scifi-clean:tech_spec" class="sc-section-tight {{ class }}"><div class="sc-content-well"><div class="sc-hud-corners" style="max-width: 34rem; padding: 1.25rem 1.5rem; overflow-x: auto;"><table class="sc-spec">{% if caption %}<caption>{{ caption }}</caption>{% endif %}<tbody>{% if rows %}{% for row in rows %}<tr><td class="sc-spec-label">{{ row.label }}</td><td class="sc-spec-value">{{ row.value }}</td><td class="sc-spec-unit">{{ row.unit }}</td></tr>{% endfor %}{% else %}<tr><td class="sc-spec-label">No spec rows yet</td><td class="sc-spec-value">--</td><td class="sc-spec-unit"></td></tr>{% endif %}</tbody></table></div></div></section>

View File

@ -0,0 +1 @@
<section data-block="scifi-clean:telemetry_grid" class="sc-section-tight {{ class }}"><div class="sc-content-well">{% if heading %}<p class="sc-eyebrow">{{ eyebrow|default:"Telemetry" }}</p><h2 class="sc-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0 0 1.5rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}<div class="sc-telemetry-grid {% if columns == 4 %}sc-telemetry-4{% elif columns == 2 %}sc-telemetry-2{% else %}sc-telemetry-3{% endif %}">{% if tiles %}{% for tile in tiles %}<div class="sc-telemetry-cell" data-sc-reveal><p class="sc-telemetry-label">{{ tile.label }}</p><p style="margin: 0;"><span class="sc-telemetry-figure" data-sc-count="{{ tile.value }}">{{ tile.value }}</span>{% if tile.unit %} <span class="sc-telemetry-unit">{{ tile.unit }}</span>{% endif %}</p>{% if tile.delta %}<p class="sc-telemetry-delta">{{ tile.delta }}</p>{% endif %}</div>{% endfor %}{% else %}<div class="sc-telemetry-cell"><p class="sc-telemetry-label">No readouts</p><p class="sc-telemetry-figure">--</p></div>{% endif %}</div></div></section>

View File

@ -0,0 +1,45 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Telemetry Grid",
"description": "Grid of HUD readout tiles with mono figures, units, deltas, and optional ticking counters.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small mono label above the heading.",
"x-editor": "text"
},
"heading": {
"type": "string",
"title": "Heading",
"description": "Section heading.",
"x-editor": "text"
},
"columns": {
"type": "number",
"title": "Columns",
"description": "Tiles per row on wide screens.",
"default": 3,
"enum": [2, 3, 4],
"x-editor": "select"
},
"tiles": {
"type": "array",
"title": "Tiles",
"description": "Each tile is a readout: label, value, optional unit and delta.",
"default": [],
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"label": { "type": "string", "title": "Label", "description": "Readout name.", "x-editor": "text" },
"value": { "type": "string", "title": "Value", "description": "Displayed value; leading number is animated by the ticking counter.", "x-editor": "text" },
"unit": { "type": "string", "title": "Unit", "description": "Trailing unit (e.g. ms, GB, %).", "x-editor": "text" },
"delta": { "type": "string", "title": "Delta", "description": "Optional change indicator (e.g. -12% latency).", "x-editor": "text" }
},
"required": ["label", "value"]
}
}
}
}

View File

@ -1 +1,26 @@
[]
[
{
"name": "Space Grotesk",
"family": "Space Grotesk",
"variants": [
{ "weight": "500", "style": "normal", "file": "fonts/space-grotesk-500.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/space-grotesk-700.woff2" }
]
},
{
"name": "Inter",
"family": "Inter",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/inter-400.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/inter-600.woff2" }
]
},
{
"name": "JetBrains Mono",
"family": "JetBrains Mono",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/jetbrains-mono-400.woff2" },
{ "weight": "500", "style": "normal", "file": "fonts/jetbrains-mono-400.woff2" }
]
}
]

View File

@ -1,40 +1,120 @@
# Sci-Fi Clean theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
# Sci-Fi Clean theme — codeless .bnp manifest (WO-TF-024, Wave A).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
#
# Identity: the precise chrome of an AI / robotics product company. Blueprint
# schematic line-work, HUD guide lines, mono readouts, spec + benchmark
# furniture. Light = white-room lab (bright ground, thin blueprint lines, one
# signal accent); dark = mission control (deep navy, technical-blue HUD lines,
# glowing readouts). Motion is MID tier: JS scroll-reveal + ticking counters +
# CSS line-draw, no canvas, prefers-reduced-motion honored, static no-JS
# fallback. Semantic tokens only.
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
required_icon_packs:
- lucide
- simple-icons
system_templates:
- key: scifi-clean
title: Sci-Fi Clean
description: "Minimalist futurist theme for aerospace, robotics, and R&D — off-white surfaces, technical blue, signal-orange accents, mono numerals."
description: "Blueprint-precise theme for AI and robotics product sites: HUD guide lines, mono readouts, spec and benchmark furniture, a single signal accent, and dual white-room / mission-control presets."
page_templates:
- system: scifi-clean
key: default
title: Default
description: "Standard cleanroom layout with header rail, main column, and footer instrument panel."
description: "Header rail + main column + footer instrument panel, capped at 1280px."
slots: [header, main, footer]
- system: scifi-clean
key: full-width
title: Full width
description: "Edge-to-edge schematic surface for diagrams and dashboards."
slots: [header, main, footer]
- system: scifi-clean
key: landing
title: Landing
description: "Hero diagram + spec strip + CTA banner."
slots: [hero, specs, main, cta, footer]
description: "Hero band + main + closing CTA + footer."
slots: [hero, main, cta, footer]
- system: scifi-clean
key: article
title: Article
description: "Narrow column with rail metadata and figure margin."
slots: [header, rail, main, footer]
description: "Narrow measure for long-form technical writing, with a rail aside."
slots: [header, main, aside, footer]
- system: scifi-clean
key: full-width
title: Full Width
description: "Edge-to-edge schematic surface for diagrams and dashboards."
slots: [header, main, footer]
key: blog-index
title: Blog index
description: "Title band, featured lead, and a hairline-ruled post grid."
slots: [header, featured, main, footer]
- system: scifi-clean
key: contact
title: Contact
description: "Two-column contact: form on the grid, coordinates in the aside."
slots: [header, main, aside, footer]
- system: scifi-clean
key: auth
title: Auth
description: "Centered console card on a quiet ground for login and reset."
slots: [main, footer]
template_overrides:
# Chrome (4)
- { template: scifi-clean, block: navbar }
- { template: scifi-clean, block: footer }
- { template: scifi-clean, block: utility-bar }
- { template: scifi-clean, block: announcement-bar }
# Marketing (12)
- { template: scifi-clean, block: hero }
- { template: scifi-clean, block: feature-grid }
- { template: scifi-clean, block: rich-section }
- { template: scifi-clean, block: stats }
- { template: scifi-clean, block: testimonial }
- { template: scifi-clean, block: logo-strip }
- { template: scifi-clean, block: pricing }
- { template: scifi-clean, block: team }
- { template: scifi-clean, block: faq }
- { template: scifi-clean, block: timeline }
- { template: scifi-clean, block: cta }
- { template: scifi-clean, block: countdown }
# Primitives (11)
- { template: scifi-clean, block: heading }
- { template: scifi-clean, block: text }
- { template: scifi-clean, block: rich-text }
- { template: scifi-clean, block: image }
- { template: scifi-clean, block: quote }
- { template: scifi-clean, block: divider }
- { template: scifi-clean, block: button }
- { template: scifi-clean, block: card }
- { template: scifi-clean, block: gallery }
- { template: scifi-clean, block: video-embed }
- { template: scifi-clean, block: audio-embed }
# Commerce / local (6)
- { template: scifi-clean, block: menu-list }
- { template: scifi-clean, block: event-list }
- { template: scifi-clean, block: contact-form }
- { template: scifi-clean, block: contact-card }
- { template: scifi-clean, block: hours-location }
- { template: scifi-clean, block: social-links }
# Blog family (12)
- { template: scifi-clean, block: blog-hero }
- { template: scifi-clean, block: featured-posts }
- { template: scifi-clean, block: related-posts }
- { template: scifi-clean, block: popular-posts }
- { template: scifi-clean, block: category-list }
- { template: scifi-clean, block: post-hero }
- { template: scifi-clean, block: post-metadata }
- { template: scifi-clean, block: author-bio }
- { template: scifi-clean, block: author-bio-hero }
- { template: scifi-clean, block: newsletter-signup }
- { template: scifi-clean, block: breadcrumbs }
- { template: scifi-clean, block: sidebar-nav }
# Auth surface (3)
- { template: scifi-clean, block: auth-form }
- { template: scifi-clean, block: password-reset-form }
- { template: scifi-clean, block: auth-status }
# System (1)
- { template: scifi-clean, block: page-suggestions }
email_wrappers:
- scifi-clean
@ -42,99 +122,378 @@ email_wrappers:
css:
input_css_append: |
/* === Sci-Fi Clean: font-family fallback variables ===
* The site admin assigns real fonts via the typography settings; until then,
* the variables fall through to the stacks below. 'Space Grotesk', 'Inter',
* and 'JetBrains Mono' are spec §5 recommendations, available in the Google
* Fonts picker.
* The admin assigns real fonts via typography settings; until then these
* fall through to the bundled stacks. Space Grotesk (heading), Inter
* (body), and JetBrains Mono (mono) ship bundled in assets/fonts.
*/
:root {
--font-heading: "Space Grotesk", "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-body: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* ============================================================
Corporate Modernist — appended component CSS.
Semantic tokens only: hsl(var(--token)). No literal colors.
Swiss grid + Bauhaus geometric marks + oversized numerals.
============================================================ */
/* === Sci-Fi Clean: utilities ===
* Hairline rules and a translucent blueprint grid are the two utility classes
* the theme uses heavily. Tailwind's defaults cover everything else.
*/
.hairline {
border: 1px solid hsl(var(--border));
/* --- Layout primitives --- */
.sc-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; }
.sc-measure { max-width: 70ch; }
.sc-article-measure { max-width: 72ch; }
.sc-swiss-12 { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: 1.5rem; row-gap: 1.5rem; }
.sc-section { padding-top: 3.5rem; padding-bottom: 3.5rem; }
.sc-section-tight { padding-top: 2.75rem; padding-bottom: 2.75rem; }
.sc-hairline { border: 1px solid hsl(var(--border)); }
.sc-hairline-top { border-top: 1px solid hsl(var(--border)); }
.sc-hairline-bottom { border-bottom: 1px solid hsl(var(--border)); }
.sc-hairline-y > * + * { border-top: 1px solid hsl(var(--border)); }
.sc-accent-rule { border-top: 2px solid hsl(var(--accent)); }
.sc-stack > * + * { margin-top: 1.5rem; }
.sc-stack-sm > * + * { margin-top: 0.75rem; }
/* --- Typography --- */
.sc-display { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; letter-spacing: -0.02em; color: hsl(var(--foreground)); margin: 0; }
.sc-body { font-family: var(--font-body, "Inter", system-ui, sans-serif); }
.sc-mono { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-variant-numeric: tabular-nums; }
.numeric-tabular { font-variant-numeric: tabular-nums; }
.sc-heading { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; letter-spacing: -0.02em; color: hsl(var(--foreground)); margin: 0; }
.sc-heading-h2-accent { border-bottom: 2px solid hsl(var(--accent)); padding-bottom: 0.25rem; display: inline-block; }
.sc-text { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 17px; line-height: 28px; color: hsl(var(--foreground)); font-variant-numeric: tabular-nums; }
.sc-text a { color: hsl(var(--accent)); text-decoration: underline; text-underline-offset: 2px; }
.sc-text strong { font-weight: 600; }
.sc-lede { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 1.25rem; line-height: 1.6; color: hsl(var(--muted-foreground)); }
.sc-eyebrow { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: hsl(var(--accent)); margin: 0 0 1rem 0; }
.sc-kicker { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: hsl(var(--muted-foreground)); }
/* --- Bauhaus geometric marks (circle / triangle / square) --- */
.sc-mark { display: inline-block; width: 0.75rem; height: 0.75rem; flex: none; }
.sc-mark-circle { border-radius: 9999px; background-color: hsl(var(--accent)); }
.sc-mark-square { background-color: hsl(var(--accent)); }
.sc-mark-triangle { width: 0; height: 0; border-left: 0.4375rem solid transparent; border-right: 0.4375rem solid transparent; border-bottom: 0.75rem solid hsl(var(--accent)); background: none; }
.sc-mark-ring { border-radius: 9999px; background: none; border: 2px solid hsl(var(--accent)); }
/* --- Numerals / metrics (oversized signature) --- */
.sc-metric { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: clamp(2.5rem, 5vw, 3.5rem); line-height: 1; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); letter-spacing: -0.02em; }
.sc-stat-figure { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: clamp(2.25rem, 4vw, 3rem); line-height: 1; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); letter-spacing: -0.02em; }
.sc-stat-label { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.8125rem; color: hsl(var(--muted-foreground)); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 0.5rem; }
.sc-numeral-index { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 2rem; line-height: 1; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); }
/* --- Buttons (squared, flat, no gradient/shadow) --- */
.sc-btn-primary { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1.25rem; background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); border: 1px solid hsl(var(--accent)); border-radius: 4px; font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 500; text-decoration: none; line-height: 1.4; box-shadow: none; background-image: none; transition: background-color 150ms ease; cursor: pointer; }
.sc-btn-primary:hover { background-color: hsl(var(--accent) / 0.9); }
.sc-btn-primary:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
.sc-btn-outline { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1.25rem; background-color: transparent; color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); border-radius: 4px; font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 500; text-decoration: none; line-height: 1.4; box-shadow: none; background-image: none; transition: border-color 150ms ease, color 150ms ease; cursor: pointer; }
.sc-btn-outline:hover { border-color: hsl(var(--foreground)); }
.sc-btn-outline:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
.sc-btn-ghost { display: inline-flex; align-items: center; gap: 0.375rem; color: hsl(var(--accent)); font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 500; text-decoration: none; line-height: 1.4; transition: gap 150ms ease; }
.sc-btn-ghost:hover { gap: 0.625rem; }
.sc-btn-sm { padding: 0.375rem 0.875rem; font-size: 0.875rem; }
.sc-btn-lg { padding: 0.875rem 1.75rem; font-size: 1.0625rem; }
.sc-btn-row { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.sc-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; }
/* --- Surfaces / cards --- */
.sc-card { background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border)); border-radius: 4px; padding: 1.5rem; box-shadow: none; }
.sc-card-accent { border-top: 2px solid hsl(var(--accent)); }
.sc-panel { background-color: hsl(var(--muted)); border-radius: 4px; padding: 1.5rem; }
.sc-surface-muted { background-color: hsl(var(--muted)); }
.sc-surface-card { background-color: hsl(var(--card)); }
.sc-media-frame { display: block; width: 100%; overflow: hidden; background-color: hsl(var(--muted)); border: 1px solid hsl(var(--border)); }
.sc-media-frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
.sc-img-empty { display: block; width: 100%; aspect-ratio: 4 / 3; background-color: hsl(var(--muted)); border: 1px solid hsl(var(--border)); }
/* --- Responsive grids --- */
.sc-grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 640px) { .sc-grid-2, .sc-grid-3, .sc-grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .sc-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .sc-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.sc-leadership-grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 640px) { .sc-leadership-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .sc-leadership-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1280px) { .sc-leadership-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* --- Chrome: navbar --- */
.sc-navbar { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; padding: 1.25rem 0; }
.sc-brand { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 700; font-size: 1.125rem; letter-spacing: -0.01em; color: hsl(var(--foreground)); text-decoration: none; display: inline-flex; align-items: center; gap: 0.625rem; }
.sc-nav-links { display: none; align-items: center; gap: 2rem; }
@media (min-width: 768px) { .sc-nav-links { display: flex; } }
.sc-nav-link { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--foreground)); text-decoration: none; padding-bottom: 2px; border-bottom: 2px solid transparent; transition: border-color 150ms ease; }
.sc-nav-link:hover { border-bottom-color: hsl(var(--accent)); }
.sc-nav-toggle { display: inline-flex; align-items: center; justify-content: center; min-width: 42px; min-height: 42px; }
@media (min-width: 768px) { .sc-nav-toggle { display: none; } }
.sc-nav-drawer { display: none; flex-direction: column; gap: 0.25rem; padding: 0.5rem 0 1rem; border-top: 1px solid hsl(var(--border)); width: 100%; }
@media (min-width: 768px) { .sc-nav-drawer { display: none !important; } }
.sc-nav-drawer a { padding: 0.625rem 0; font-family: var(--font-body, "Inter", system-ui, sans-serif); color: hsl(var(--foreground)); text-decoration: none; border-bottom: 1px solid hsl(var(--border)); }
/* --- Chrome: utility bar + announcement bar --- */
.sc-utility-bar { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.8125rem; padding: 0.5rem 0; }
.sc-utility-bar--muted { background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); }
.sc-utility-bar--primary { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
.sc-utility-bar--dark { background-color: hsl(var(--foreground)); color: hsl(var(--background)); }
.sc-utility-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.sc-utility-group { display: flex; align-items: center; gap: 1.25rem; }
.sc-utility-item { display: inline-flex; align-items: center; gap: 0.375rem; color: inherit; text-decoration: none; }
.sc-announce { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.875rem; padding: 0.625rem 0; }
.sc-announce--info { background-color: hsl(var(--muted)); color: hsl(var(--foreground)); }
.sc-announce--promo { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.sc-announce--alert { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
.sc-announce-row { display: flex; align-items: center; justify-content: center; gap: 0.75rem; }
.sc-announce a { color: inherit; text-decoration: underline; text-underline-offset: 2px; font-weight: 600; }
.sc-announce-close { margin-left: auto; background: none; border: 0; color: inherit; cursor: pointer; line-height: 1; padding: 0 0.25rem; }
/* --- Chrome: footer --- */
.sc-footer { background-color: hsl(var(--background)); color: hsl(var(--foreground)); border-top: 2px solid hsl(var(--accent)); padding: 3.5rem 0 2.5rem; }
.sc-footer-cols { display: grid; gap: 2rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 640px) { .sc-footer-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .sc-footer-cols { grid-template-columns: 2fr repeat(3, 1fr); } }
.sc-footer-head { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: hsl(var(--muted-foreground)); margin: 0 0 0.875rem; }
.sc-footer-link { display: block; font-size: 0.9375rem; color: hsl(var(--foreground)); text-decoration: none; padding: 0.25rem 0; }
.sc-footer-link:hover { color: hsl(var(--accent)); }
.sc-footer-legal { color: hsl(var(--muted-foreground)); font-size: 0.8125rem; 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 --- */
.sc-hero { padding: 5rem 0; }
.sc-hero-headline { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; letter-spacing: -0.03em; line-height: 1.05; font-size: clamp(2.5rem, 6vw, 4.5rem); color: hsl(var(--foreground)); margin: 0; }
.sc-hero-sub { font-size: clamp(1.125rem, 2vw, 1.5rem); line-height: 1.5; color: hsl(var(--muted-foreground)); margin: 1.25rem 0 0; }
.sc-hero-split { display: grid; gap: 3rem; grid-template-columns: repeat(1, minmax(0, 1fr)); align-items: center; }
@media (min-width: 1024px) { .sc-hero-split { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* --- Marketing: logo strip --- */
.sc-logo { filter: grayscale(1); opacity: 0.65; transition: filter 200ms ease, opacity 200ms ease; }
.sc-logo:hover { filter: grayscale(0); opacity: 1; }
.sc-logo-row { display: flex; flex-wrap: wrap; align-items: center; gap: 2.5rem; }
/* --- Marketing: feature grid --- */
.sc-feature { border-top: 2px solid hsl(var(--accent)); padding-top: 1.25rem; }
.sc-feature-title { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; font-size: 1.125rem; color: hsl(var(--foreground)); margin: 0.75rem 0 0.5rem; }
/* --- Marketing: pull quote / testimonial --- */
.sc-pull-quote { border-top: 2px solid hsl(var(--accent)); padding-top: 1.25rem; font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 1.5rem; line-height: 1.4; font-weight: 500; color: hsl(var(--foreground)); }
.sc-quote-block { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 1.375rem; line-height: 1.45; font-weight: 500; color: hsl(var(--foreground)); }
.sc-quote-cite { font-size: 0.875rem; color: hsl(var(--muted-foreground)); margin-top: 0.75rem; }
.sc-avatar { width: 2.75rem; height: 2.75rem; border-radius: 9999px; object-fit: cover; background-color: hsl(var(--muted)); }
/* --- Marketing: pricing --- */
.sc-price { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 2.5rem; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); letter-spacing: -0.02em; }
.sc-price-period { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--muted-foreground)); }
.sc-price-tier { display: flex; flex-direction: column; }
.sc-price-tier--featured { border-top: 4px solid hsl(var(--accent)); }
.sc-badge { display: inline-block; font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.6875rem; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.25rem 0.5rem; background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); border-radius: 3px; }
.sc-check { color: hsl(var(--accent)); flex: none; }
/* --- Marketing: faq --- */
.sc-faq-item { border-top: 1px solid hsl(var(--border)); }
.sc-faq-item summary { cursor: pointer; list-style: none; padding: 1.25rem 0; display: flex; align-items: center; justify-content: space-between; gap: 1rem; font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; color: hsl(var(--foreground)); }
.sc-faq-item summary::-webkit-details-marker { display: none; }
.sc-faq-item[open] summary .sc-faq-chevron { transform: rotate(180deg); }
.sc-faq-chevron { transition: transform 150ms ease; color: hsl(var(--accent)); }
.sc-faq-answer { padding: 0 0 1.25rem; color: hsl(var(--muted-foreground)); font-size: 1rem; line-height: 1.65; }
/* --- Marketing: timeline --- */
.sc-timeline-step { display: grid; grid-template-columns: auto 1fr; gap: 1.25rem; padding-bottom: 2rem; }
.sc-timeline-rail { position: relative; display: flex; flex-direction: column; align-items: center; }
.sc-timeline-node { width: 2.5rem; height: 2.5rem; border: 2px solid hsl(var(--accent)); border-radius: 9999px; display: flex; align-items: center; justify-content: center; font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-weight: 500; color: hsl(var(--accent)); flex: none; font-variant-numeric: tabular-nums; }
.sc-timeline-line { flex: 1 1 auto; width: 1px; background-color: hsl(var(--border)); margin-top: 0.5rem; }
/* --- Marketing: cta strip --- */
.sc-cta-strip { padding: 3.5rem 0; }
.sc-cta-strip--band { background-color: hsl(var(--foreground)); color: hsl(var(--background)); }
.sc-cta-strip--band .sc-btn-primary { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); border-color: hsl(var(--accent)); }
.sc-cta-strip--card { background-color: hsl(var(--muted)); }
.sc-cta-headline { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; letter-spacing: -0.02em; font-size: clamp(1.75rem, 3vw, 2.5rem); line-height: 1.15; margin: 0; }
/* --- Marketing: countdown --- */
.sc-count-unit { display: flex; flex-direction: column; align-items: center; }
.sc-count-figure { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: clamp(2rem, 4vw, 3rem); font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); line-height: 1; }
.sc-count-box { border: 1px solid hsl(var(--border)); padding: 1rem 1.25rem; border-radius: 4px; min-width: 4.5rem; }
/* --- Primitives: quote / divider / gallery / media --- */
.sc-divider-line { border: 0; border-top: 1px solid hsl(var(--border)); }
.sc-divider-thick { border-top-width: 2px; border-top-color: hsl(var(--accent)); }
.sc-divider-ornament { display: flex; align-items: center; justify-content: center; gap: 0.75rem; color: hsl(var(--muted-foreground)); }
.sc-gallery-grid { display: grid; gap: 0.75rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 768px) { .sc-gallery-grid.sc-gallery-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .sc-gallery-grid.sc-gallery-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.sc-video-frame { position: relative; display: block; width: 100%; background-color: hsl(var(--muted)); border: 1px solid hsl(var(--border)); overflow: hidden; }
.sc-video-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: hsl(var(--background)); background: none; border: 0; cursor: pointer; }
.sc-video-play-badge { width: 4rem; height: 4rem; border-radius: 9999px; background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); display: flex; align-items: center; justify-content: center; }
/* --- Commerce / local --- */
.sc-menu-item { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid hsl(var(--border)); }
.sc-menu-price { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); }
.sc-channel { display: flex; align-items: flex-start; gap: 0.75rem; }
.sc-channel-mark { width: 2.5rem; height: 2.5rem; border: 1px solid hsl(var(--border)); border-radius: 4px; display: flex; align-items: center; justify-content: center; color: hsl(var(--accent)); flex: none; }
.sc-hours-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.375rem 0; border-bottom: 1px solid hsl(var(--border)); }
.sc-social-row { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.sc-social-link { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.5rem 0.75rem; border: 1px solid hsl(var(--border)); border-radius: 4px; color: hsl(var(--foreground)); text-decoration: none; font-size: 0.875rem; transition: border-color 150ms ease; }
.sc-social-link:hover { border-color: hsl(var(--accent)); color: hsl(var(--accent)); }
/* --- Forms --- */
.sc-form { display: grid; gap: 1.25rem; }
.sc-form-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 640px) { .sc-form-grid { grid-template-columns: 1fr; } }
.sc-field { display: flex; flex-direction: column; gap: 0.375rem; }
.sc-field--full { grid-column: 1 / -1; }
.sc-label { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 600; color: hsl(var(--foreground)); text-transform: uppercase; letter-spacing: 0.04em; }
.sc-input { width: 100%; box-sizing: border-box; padding: 0.625rem 0.75rem; background-color: hsl(var(--background)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--input)); border-radius: 4px; font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 1rem; }
.sc-input:focus { outline: 2px solid hsl(var(--ring)); outline-offset: 1px; border-color: hsl(var(--ring)); }
.sc-form-note { font-size: 0.8125rem; color: hsl(var(--muted-foreground)); }
.sc-alert-ok { border-left: 3px solid hsl(var(--accent)); background-color: hsl(var(--muted)); padding: 1rem 1.25rem; color: hsl(var(--foreground)); }
.sc-alert-err { border-left: 3px solid hsl(var(--destructive)); background-color: hsl(var(--muted)); padding: 1rem 1.25rem; color: hsl(var(--foreground)); }
/* --- Blog family --- */
.sc-post-card { display: flex; flex-direction: column; gap: 0.75rem; }
.sc-post-thumb { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; background-color: hsl(var(--muted)); border: 1px solid hsl(var(--border)); }
.sc-post-title { font-family: var(--font-heading, "Inter", system-ui, sans-serif); font-weight: 600; font-size: 1.25rem; line-height: 1.25; color: hsl(var(--foreground)); text-decoration: none; }
.sc-post-title:hover { color: hsl(var(--accent)); }
.sc-post-meta { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.04em; text-transform: uppercase; color: hsl(var(--muted-foreground)); }
.sc-post-excerpt { color: hsl(var(--muted-foreground)); font-size: 0.9375rem; line-height: 1.6; }
.sc-post-grid { display: grid; gap: 2.5rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 640px) { .sc-post-grid-2, .sc-post-grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .sc-post-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.sc-tag { display: inline-block; font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.04em; padding: 0.1875rem 0.5rem; border: 1px solid hsl(var(--border)); border-radius: 3px; color: hsl(var(--muted-foreground)); text-decoration: none; }
.sc-tag:hover { border-color: hsl(var(--accent)); color: hsl(var(--accent)); }
.sc-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 0.8125rem; color: hsl(var(--muted-foreground)); }
.sc-crumbs a { color: hsl(var(--muted-foreground)); text-decoration: none; }
.sc-crumbs a:hover { color: hsl(var(--accent)); }
.sc-crumb-sep { color: hsl(var(--border)); }
.sc-sidebar-link { display: block; padding: 0.5rem 0.75rem; border-left: 2px solid transparent; color: hsl(var(--foreground)); text-decoration: none; font-size: 0.9375rem; }
.sc-sidebar-link:hover, .sc-sidebar-link[aria-current="page"] { border-left-color: hsl(var(--accent)); color: hsl(var(--accent)); }
.sc-rank { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 1.25rem; font-weight: 500; color: hsl(var(--accent)); font-variant-numeric: tabular-nums; width: 2rem; flex: none; }
/* --- Motion (CSS-first, reduced-motion honored) --- */
.sc-reveal { opacity: 0; transform: translateY(0.5rem); animation: sc-reveal 500ms ease forwards; }
@keyframes sc-reveal { to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
.sc-reveal { animation: none; opacity: 1; transform: none; }
.sc-btn-ghost, .sc-logo, .sc-nav-link, .sc-faq-chevron { transition: none; }
}
.hairline-t {
border-top: 1px solid hsl(var(--border));
}
/* ============================================================
Sci-Fi Clean — identity layer (overrides the shared base above).
Persona: AI / robotics product. Blueprint schematics, HUD guide
lines, mono readouts, spec furniture. Light = white-room lab;
dark = mission control. Semantic tokens only. Tight 2px radii.
primary = technical blue (HUD lines), accent = signal (CTAs/deltas).
============================================================ */
.hairline-b {
border-bottom: 1px solid hsl(var(--border));
}
/* --- Typography: mono readout eyebrows / kickers with // marker --- */
.sc-eyebrow { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; color: hsl(var(--primary)); margin: 0 0 1rem 0; }
.sc-eyebrow::before { content: "// "; color: hsl(var(--accent)); }
.sc-kicker { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase; color: hsl(var(--muted-foreground)); }
.sc-display, .sc-heading, .sc-hero-headline, .sc-cta-headline, .sc-feature-title { font-family: var(--font-heading, "Space Grotesk", "Inter", system-ui, sans-serif); font-feature-settings: "ss01" on, "ss02" on; }
.sc-brand { font-family: var(--font-heading, "Space Grotesk", "Inter", system-ui, sans-serif); font-weight: 700; letter-spacing: -0.01em; }
.sc-brand::before { content: "◇ "; color: hsl(var(--accent)); font-family: var(--font-mono, ui-monospace, monospace); }
.bg-grid {
background-color: hsl(var(--background));
background-image:
linear-gradient(to right, hsl(var(--border) / 0.6) 1px, transparent 1px),
linear-gradient(to bottom, hsl(var(--border) / 0.6) 1px, transparent 1px);
background-size: 32px 32px;
}
/* --- HUD guide lines: section rules use technical blue, thin --- */
.sc-accent-rule { border-top: 1px solid hsl(var(--primary)); }
.sc-hairline-top { border-top: 1px solid hsl(var(--border)); }
.sc-hairline-bottom { border-bottom: 1px solid hsl(var(--border)); }
.sc-heading-h2-accent { border-bottom: 2px solid hsl(var(--primary)); padding-bottom: 0.25rem; display: inline-block; }
.sc-divider-thick { border-top-width: 1px; border-top-color: hsl(var(--primary)); }
.mono-numerals {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
/* --- Buttons: sharp, mono label, precise --- */
.sc-btn-primary, .sc-btn-outline { border-radius: 2px; font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-weight: 500; font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; }
.sc-btn-primary { background-color: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary)); }
.sc-btn-primary:hover { background-color: hsl(var(--primary) / 0.9); }
.sc-btn-outline:hover { border-color: hsl(var(--primary)); color: hsl(var(--primary)); }
.sc-btn-ghost { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-weight: 500; font-size: 0.8125rem; letter-spacing: 0.06em; text-transform: uppercase; color: hsl(var(--primary)); }
.sc-btn-lg { padding: 0.875rem 1.75rem; font-size: 0.875rem; }
.sc-btn-sm { padding: 0.375rem 0.875rem; font-size: 0.75rem; }
.tabular-nums {
font-variant-numeric: tabular-nums;
}
/* --- Surfaces: cornered blueprint cards --- */
.sc-card, .sc-panel, .sc-input, .sc-badge, .sc-tag, .sc-count-box, .sc-channel-mark, .sc-social-link { border-radius: 2px; }
.sc-card { position: relative; }
.sc-card-accent { border-top: 2px solid hsl(var(--primary)); }
.sc-card-accent::before, .sc-card-accent::after { content: ""; position: absolute; top: -1px; width: 10px; height: 10px; pointer-events: none; }
.sc-card-accent::before { left: -1px; border-left: 2px solid hsl(var(--accent)); border-top: 2px solid hsl(var(--accent)); }
.sc-card-accent::after { right: -1px; border-right: 2px solid hsl(var(--accent)); border-top: 2px solid hsl(var(--accent)); }
.sc-media-frame, .sc-post-thumb, .sc-video-frame { border-radius: 2px; }
.scrim {
position: relative;
}
/* --- Marks: HUD nodes/ticks in technical blue --- */
.sc-mark-square { background-color: hsl(var(--primary)); }
.sc-mark-circle { background-color: hsl(var(--primary)); }
.sc-mark-ring { border-color: hsl(var(--primary)); background: none; }
.sc-mark-triangle { border-bottom-color: hsl(var(--accent)); }
.scrim::before {
content: "";
position: absolute;
inset: 0;
background-color: hsl(var(--background) / 0.65);
pointer-events: none;
}
/* --- Readouts: metrics/stats/prices in mono, blue figures --- */
.sc-stat-figure, .sc-price, .sc-count-figure { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); }
.sc-stat-label, .sc-price-period, .sc-menu-price { font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace); }
.sc-rank { color: hsl(var(--accent)); }
.sc-timeline-node { border-color: hsl(var(--primary)); color: hsl(var(--primary)); border-radius: 2px; }
.sc-price-tier--featured { border-top: 2px solid hsl(var(--primary)); }
.sc-badge { background-color: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.sc-check { color: hsl(var(--primary)); }
.scrim > * {
position: relative;
z-index: 1;
}
/* --- Feature blocks + FAQ + chevrons: HUD blue --- */
.sc-feature { border-top: 1px solid hsl(var(--primary)); }
.sc-faq-chevron { color: hsl(var(--primary)); }
.sc-pull-quote, .sc-quote-block { border-color: hsl(var(--primary)); }
.sc-pull-quote { border-top: 1px solid hsl(var(--primary)); }
.sc-sidebar-link:hover, .sc-sidebar-link[aria-current="page"] { border-left-color: hsl(var(--primary)); color: hsl(var(--primary)); }
.sc-nav-link:hover { border-bottom-color: hsl(var(--primary)); }
.sc-tag:hover, .sc-social-link:hover, .sc-post-title:hover, .sc-footer-link:hover { color: hsl(var(--primary)); border-color: hsl(var(--primary)); }
/* === Sci-Fi Clean: focus rings ===
* Use the --ring token (a precise technical blue) for all focus states so
* the keyboard cursor is visible against either preset.
*/
.scifi-focus:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Chrome: footer top rule = HUD blue --- */
.sc-footer { border-top: 2px solid hsl(var(--primary)); }
/* === Sci-Fi Clean: chevron ===
* Right-arrow chevron used in ScifiButtonBlock; encoded as Unicode so it
* never breaks across renderers (UAT §13.14).
*/
.scifi-chevron::after {
content: " \2192";
font-family: var(--font-mono);
margin-left: 0.5rem;
/* --- Blueprint grid ground (used by page-template mains) --- */
.sc-blueprint {
background-image:
linear-gradient(hsl(var(--border) / 0.5) 1px, transparent 1px),
linear-gradient(90deg, hsl(var(--border) / 0.5) 1px, transparent 1px);
background-size: 40px 40px;
}
.sc-hud-corners { position: relative; }
.sc-hud-corners::before, .sc-hud-corners::after { content: ""; position: absolute; width: 14px; height: 14px; pointer-events: none; }
.sc-hud-corners::before { top: 0; left: 0; border-left: 1px solid hsl(var(--primary)); border-top: 1px solid hsl(var(--primary)); }
.sc-hud-corners::after { bottom: 0; right: 0; border-right: 1px solid hsl(var(--primary)); border-bottom: 1px solid hsl(var(--primary)); }
/* === Sci-Fi Clean: heading family ===
* The host shadcn layer doesn't apply --font-heading by default; wire it
* up here for the four heading levels we render.
*/
.scifi-h1, .scifi-h2, .scifi-h3, .scifi-h4 {
font-family: var(--font-heading);
font-feature-settings: "ss01" on;
}
/* --- Persona furniture: tech spec / benchmark / telemetry / console --- */
.sc-spec { width: 100%; border-collapse: collapse; font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.875rem; }
.sc-spec caption { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--muted-foreground)); text-align: left; padding-bottom: 0.75rem; }
.sc-spec tr { border-bottom: 1px solid hsl(var(--border)); }
.sc-spec-label { text-align: left; width: 100%; padding: 0.625rem 0.5rem; color: hsl(var(--muted-foreground)); text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.75rem; }
.sc-spec-value { text-align: right; white-space: nowrap; padding: 0.625rem 0.375rem 0.625rem 1rem; color: hsl(var(--foreground)); font-variant-numeric: tabular-nums; font-weight: 500; }
.sc-spec-unit { text-align: left; white-space: nowrap; width: 1%; padding: 0.625rem 0.5rem 0.625rem 0; color: hsl(var(--muted-foreground)); font-size: 0.75rem; }
.scifi-body {
font-family: var(--font-body);
}
.sc-bench-row { display: grid; grid-template-columns: 1fr auto; gap: 0.375rem 1rem; align-items: baseline; margin-bottom: 1rem; }
.sc-bench-name { font-family: var(--font-body, "Inter", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--foreground)); }
.sc-bench-value { font-family: var(--font-mono, ui-monospace, monospace); font-variant-numeric: tabular-nums; font-weight: 500; color: hsl(var(--foreground)); }
.sc-bench-track { grid-column: 1 / -1; height: 6px; background-color: hsl(var(--muted)); border-radius: 2px; overflow: hidden; }
.sc-bench-fill { height: 100%; background-color: hsl(var(--primary)); border-radius: 2px; }
.sc-bench-fill--accent { background-color: hsl(var(--accent)); }
.scifi-mono {
font-family: var(--font-mono);
.sc-telemetry-grid { display: grid; gap: 1px; background-color: hsl(var(--border)); border: 1px solid hsl(var(--border)); border-radius: 2px; grid-template-columns: repeat(1, minmax(0, 1fr)); overflow: hidden; }
@media (min-width: 640px) { .sc-telemetry-grid.sc-telemetry-2, .sc-telemetry-grid.sc-telemetry-3, .sc-telemetry-grid.sc-telemetry-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .sc-telemetry-grid.sc-telemetry-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .sc-telemetry-grid.sc-telemetry-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.sc-telemetry-cell { background-color: hsl(var(--card)); padding: 1.5rem 1.25rem; }
.sc-telemetry-label { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.6875rem; letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--muted-foreground)); margin: 0 0 0.5rem; }
.sc-telemetry-figure { font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(1.75rem, 3.5vw, 2.5rem); line-height: 1; font-weight: 500; font-variant-numeric: tabular-nums; color: hsl(var(--foreground)); }
.sc-telemetry-unit { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.8125rem; color: hsl(var(--muted-foreground)); }
.sc-telemetry-delta { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.75rem; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); margin-top: 0.375rem; }
.sc-console { background-color: hsl(var(--card)); border: 1px solid hsl(var(--border)); border-radius: 2px; overflow: hidden; }
.sc-console-bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1rem; border-bottom: 1px solid hsl(var(--border)); background-color: hsl(var(--muted)); }
.sc-console-dot { width: 8px; height: 8px; border-radius: 9999px; background-color: hsl(var(--border)); }
.sc-console-title { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.75rem; letter-spacing: 0.08em; color: hsl(var(--muted-foreground)); margin-left: 0.5rem; }
.sc-console-body { padding: 1.25rem 1rem; font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.8125rem; line-height: 1.7; }
.sc-console-entry { display: grid; grid-template-columns: auto auto 1fr; gap: 0.75rem; padding: 0.375rem 0; }
.sc-console-ver { color: hsl(var(--primary)); font-weight: 500; }
.sc-console-date { color: hsl(var(--muted-foreground)); }
.sc-console-note { color: hsl(var(--foreground)); }
.sc-console-prompt::before { content: "$ "; color: hsl(var(--accent)); }
/* --- Motion (MID tier): JS scroll-reveal + ticking counters.
No-JS fallback: without .sc-js on <html>, everything is fully
visible and counters show their static final text. --- */
.sc-js [data-sc-reveal] { opacity: 0; transform: translateY(12px); transition: opacity 600ms ease, transform 600ms ease; }
.sc-js [data-sc-reveal].sc-in { opacity: 1; transform: none; }
.sc-schematic { position: relative; }
.sc-js .sc-schematic .sc-schematic-line { transform: scaleX(0); transform-origin: left; transition: transform 900ms ease; }
.sc-js .sc-schematic.sc-in .sc-schematic-line { transform: scaleX(1); }
.sc-schematic-line { height: 1px; background-color: hsl(var(--primary)); }
@media (prefers-reduced-motion: reduce) {
.sc-js [data-sc-reveal] { opacity: 1 !important; transform: none !important; transition: none !important; }
.sc-js .sc-schematic .sc-schematic-line { transform: none !important; transition: none !important; }
.sc-reveal { animation: none; opacity: 1; transform: none; }
}

View File

@ -2,12 +2,11 @@
{
"key": "scifi-clean:default-master",
"title": "Sci-Fi Clean Default Master",
"page_templates": ["default", "article", "full-width"],
"page_templates": ["default", "article", "blog-index", "full-width"],
"blocks": [
{ "block_key": "navbar", "title": "Mission Header", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 },
{ "block_key": "scifi-clean:status_bar", "title": "System Status Rail", "content": { "showClock": true, "showBuild": true, "callsign": "SCF-CLN" }, "slot": "header", "sort_order": 10 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page payload" }, "slot": "main", "sort_order": 0 },
{ "block_key": "scifi-clean:footer", "title": "Instrument Footer", "content": { "showSignup": true, "callsign": "SCF-CLN" }, "slot": "footer", "sort_order": 0 }
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main" }, "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": "Halcyon Robotics", "tagline": "Autonomy systems for the field.", "copyright": "Halcyon Robotics" }, "slot": "footer", "sort_order": 0 }
]
},
{
@ -15,11 +14,19 @@
"title": "Sci-Fi Clean Landing Master",
"page_templates": ["landing"],
"blocks": [
{ "block_key": "scifi-clean:status_bar", "title": "System Status Rail", "content": { "showClock": true, "showBuild": true, "callsign": "SCF-CLN" }, "slot": "header", "sort_order": 0 },
{ "block_key": "scifi-clean:mission_stat", "title": "Hero Mission Stat", "content": { "metric": "Apogee", "value": "412", "unit": "km", "delta": "+12", "trend": "up" }, "slot": "hero", "sort_order": 0 },
{ "block_key": "scifi-clean:tech_spec", "title": "Hero Tech Spec", "content": { "caption": "Launch envelope", "rows": [ { "label": "Thrust", "value": "1.42", "unit": "MN" }, { "label": "Burn time", "value": "186", "unit": "s" }, { "label": "Payload", "value": "320", "unit": "kg" } ] }, "slot": "specs", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page payload" }, "slot": "main", "sort_order": 0 },
{ "block_key": "scifi-clean:footer", "title": "Instrument Footer", "content": { "showSignup": true, "callsign": "SCF-CLN" }, "slot": "footer", "sort_order": 0 }
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main" }, "slot": "header", "sort_order": 0 },
{ "block_key": "hero", "title": "Landing Hero", "content": { "template": "left-aligned", "eyebrow": "Field autonomy", "headline": "Robots that hold their own position", "subheadline": "Halcyon builds perception and control stacks for machines that work where the map runs out." }, "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": "Request a technical briefing", "text": "Thirty minutes with an engineer. Bring your hardest deployment.", "primaryLabel": "Book a briefing", "primaryUrl": "/contact" }, "slot": "cta", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Halcyon Robotics", "copyright": "Halcyon Robotics" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "scifi-clean:auth-master",
"title": "Sci-Fi Clean Auth Master",
"page_templates": ["auth"],
"blocks": [
{ "block_key": "slot", "title": "Auth Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }
]
}
]

View File

@ -2,7 +2,7 @@
name = "scifi-clean"
display_name = "Sci-Fi Clean"
scope = "@themes"
version = "0.2.0"
version = "0.2.1"
description = "Minimalist futurist theme for aerospace, robotics, and R&D — off-white surfaces, technical blue, signal-orange accents, mono numerals."
kind = "theme"
categories = ["templates"]

View File

@ -2,9 +2,9 @@
{
"id": "flightline",
"name": "Flightline",
"description": "Off-white paper with technical blue primary and signal-orange accent — the default Sci-Fi Clean look.",
"description": "The default Sci-Fi Clean look. Light is a white-room lab with technical-blue HUD lines and a signal-orange accent; dark is deep mission-control navy with lifted blue readouts.",
"theme": {
"mode": "light",
"mode": "both",
"lightColors": {
"background": "210 14% 97%",
"foreground": "215 30% 12%",
@ -25,15 +25,7 @@
"border": "215 14% 86%",
"input": "215 14% 88%",
"ring": "215 90% 45%"
}
}
},
{
"id": "mission-control",
"name": "Mission Control",
"description": "Graphite night-shift surface for ops dashboards — lifted technical blue and signal orange on a deep neutral.",
"theme": {
"mode": "dark",
},
"darkColors": {
"background": "220 16% 7%",
"foreground": "210 16% 92%",
@ -41,7 +33,7 @@
"cardForeground": "210 16% 92%",
"popover": "220 16% 10%",
"popoverForeground": "210 16% 92%",
"primary": "210 100% 60%",
"primary": "210 100% 62%",
"primaryForeground": "220 16% 7%",
"secondary": "220 14% 16%",
"secondaryForeground": "210 16% 92%",
@ -53,14 +45,14 @@
"destructiveForeground": "0 0% 100%",
"border": "220 14% 20%",
"input": "220 14% 18%",
"ring": "210 100% 60%"
"ring": "210 100% 62%"
}
}
},
{
"id": "cleanroom",
"name": "Cleanroom",
"description": "Highest-contrast surgical white with deep navy ink — sister dark mode for night-lab work.",
"description": "Highest-contrast surgical white with deep-navy ink in light, and near-black space navy in dark. For high-precision spec and dashboard work.",
"theme": {
"mode": "both",
"lightColors": {
@ -70,7 +62,7 @@
"cardForeground": "220 40% 10%",
"popover": "0 0% 100%",
"popoverForeground": "220 40% 10%",
"primary": "220 85% 35%",
"primary": "220 85% 38%",
"primaryForeground": "0 0% 100%",
"secondary": "210 18% 94%",
"secondaryForeground": "220 40% 10%",
@ -82,7 +74,7 @@
"destructiveForeground": "0 0% 100%",
"border": "215 16% 88%",
"input": "215 16% 90%",
"ring": "220 85% 35%"
"ring": "220 85% 38%"
},
"darkColors": {
"background": "220 30% 5%",
@ -91,7 +83,7 @@
"cardForeground": "210 20% 96%",
"popover": "220 26% 9%",
"popoverForeground": "210 20% 96%",
"primary": "210 100% 65%",
"primary": "210 100% 66%",
"primaryForeground": "220 30% 5%",
"secondary": "220 20% 16%",
"secondaryForeground": "210 20% 96%",
@ -103,7 +95,107 @@
"destructiveForeground": "0 0% 100%",
"border": "220 18% 20%",
"input": "220 18% 18%",
"ring": "210 100% 65%"
"ring": "210 100% 66%"
}
}
},
{
"id": "signal-cyan",
"name": "Signal Cyan",
"description": "Overt HUD styling. Cyan primary guide lines with an amber signal accent, on a cool white lab ground in light and a teal-tinted mission ground in dark.",
"theme": {
"mode": "both",
"lightColors": {
"background": "195 20% 97%",
"foreground": "200 40% 11%",
"card": "0 0% 100%",
"cardForeground": "200 40% 11%",
"popover": "0 0% 100%",
"popoverForeground": "200 40% 11%",
"primary": "192 82% 38%",
"primaryForeground": "0 0% 100%",
"secondary": "195 22% 91%",
"secondaryForeground": "200 40% 16%",
"muted": "195 20% 94%",
"mutedForeground": "200 14% 40%",
"accent": "35 95% 50%",
"accentForeground": "0 0% 100%",
"destructive": "0 76% 48%",
"destructiveForeground": "0 0% 100%",
"border": "195 18% 85%",
"input": "195 18% 87%",
"ring": "192 82% 38%"
},
"darkColors": {
"background": "198 30% 6%",
"foreground": "190 20% 92%",
"card": "198 28% 9%",
"cardForeground": "190 20% 92%",
"popover": "198 28% 9%",
"popoverForeground": "190 20% 92%",
"primary": "186 90% 55%",
"primaryForeground": "198 30% 6%",
"secondary": "198 22% 15%",
"secondaryForeground": "190 20% 92%",
"muted": "198 18% 13%",
"mutedForeground": "195 12% 60%",
"accent": "38 100% 58%",
"accentForeground": "200 40% 8%",
"destructive": "0 80% 58%",
"destructiveForeground": "0 0% 100%",
"border": "198 20% 19%",
"input": "198 20% 17%",
"ring": "186 90% 55%"
}
}
},
{
"id": "graphite-lab",
"name": "Graphite Lab",
"description": "Restrained neutral. Slate-blue primary and a muted signal accent on graphite surfaces. Quietest of the four for text-dense product docs.",
"theme": {
"mode": "both",
"lightColors": {
"background": "220 12% 96%",
"foreground": "220 20% 14%",
"card": "0 0% 100%",
"cardForeground": "220 20% 14%",
"popover": "0 0% 100%",
"popoverForeground": "220 20% 14%",
"primary": "218 42% 40%",
"primaryForeground": "0 0% 100%",
"secondary": "220 12% 90%",
"secondaryForeground": "220 20% 20%",
"muted": "220 12% 93%",
"mutedForeground": "220 10% 42%",
"accent": "24 80% 52%",
"accentForeground": "0 0% 100%",
"destructive": "0 74% 48%",
"destructiveForeground": "0 0% 100%",
"border": "220 12% 85%",
"input": "220 12% 87%",
"ring": "218 42% 40%"
},
"darkColors": {
"background": "220 14% 9%",
"foreground": "220 14% 90%",
"card": "220 14% 12%",
"cardForeground": "220 14% 90%",
"popover": "220 14% 12%",
"popoverForeground": "220 14% 90%",
"primary": "215 55% 62%",
"primaryForeground": "220 14% 9%",
"secondary": "220 12% 18%",
"secondaryForeground": "220 14% 90%",
"muted": "220 12% 15%",
"mutedForeground": "220 10% 60%",
"accent": "26 88% 58%",
"accentForeground": "220 20% 8%",
"destructive": "0 78% 56%",
"destructiveForeground": "0 0% 100%",
"border": "220 12% 22%",
"input": "220 12% 20%",
"ring": "215 55% 62%"
}
}
}

121
seed/seed.json Normal file
View File

@ -0,0 +1,121 @@
{
"demo": true,
"settings": {
"merge": {
"site_name": "Halcyon Robotics",
"tagline": "Autonomy systems for the field."
}
},
"data_tables": [
{
"key": "contact_submissions",
"name": "Contact submissions",
"description": "Messages sent from the Halcyon Robotics contact form.",
"primary_key": "id",
"columns": [
{ "key": "name", "label": "Name", "type": "text", "required": true },
{ "key": "email", "label": "Email", "type": "email", "required": true },
{ "key": "organisation", "label": "Organisation", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"pages": [
{
"slug": "/",
"title": "Halcyon Robotics",
"template_key": "full-width",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 1, "content": { "template": "left-aligned", "eyebrow": { "text": "Field autonomy" }, "headline": { "text": "Robots that hold their own position" }, "subheadline": { "text": "Halcyon builds the perception and control stack for machines that work where the map runs out. One runtime, from sensor to actuator." }, "ctaPrimary": { "text": "Read the spec", "url": "/about" }, "ctaSecondary": { "text": "Book a briefing", "url": "/contact" } } },
{ "block_key": "scifi-clean:telemetry_grid", "title": "Telemetry", "slot": "main", "sort_order": 2, "content": { "eyebrow": "Runtime", "heading": "What the stack holds", "columns": 4, "tiles": [ { "label": "Control loop", "value": "1000", "unit": "Hz", "delta": "deterministic" }, { "label": "Localisation drift", "value": "4", "unit": "cm", "delta": "over 1 km" }, { "label": "Onboard latency", "value": "18", "unit": "ms", "delta": "sensor to command" }, { "label": "Power draw", "value": "42", "unit": "W", "delta": "full autonomy" } ] } },
{ "block_key": "scifi-clean:benchmark_table", "title": "Benchmarks", "slot": "main", "sort_order": 3, "content": { "eyebrow": "Perception", "heading": "Detection recall by surface", "rows": [ { "label": "Paved road", "value": "99.2", "unit": "%", "bar": 99, "highlight": false }, { "label": "Gravel and dirt", "value": "97.8", "unit": "%", "bar": 98, "highlight": false }, { "label": "Low light", "value": "94.1", "unit": "%", "bar": 94, "highlight": true }, { "label": "Heavy dust", "value": "91.6", "unit": "%", "bar": 92, "highlight": false } ], "note": "Measured on the internal Halcyon field set. Numbers are illustrative product data for this demo site." } },
{ "block_key": "stats", "title": "Deployment", "slot": "main", "sort_order": 4, "content": { "heading": "Built for the field", "columns": 3, "countUp": false, "items": [ { "value": "40", "suffix": " C", "label": "Rated operating ceiling" }, { "value": "8", "suffix": " h", "label": "Continuous mission time" }, { "value": "IP67", "label": "Sealed enclosure rating" } ] } },
{ "block_key": "scifi-clean:tech_spec", "title": "Reference platform", "slot": "main", "sort_order": 5, "content": { "caption": "Halcyon H1 reference unit", "rows": [ { "label": "Mass", "value": "28", "unit": "kg" }, { "label": "Top speed", "value": "3.2", "unit": "m/s" }, { "label": "Compute", "value": "128", "unit": "TOPS" }, { "label": "Sensors", "value": "6", "unit": "channels" } ] } },
{ "block_key": "cta", "title": "Closing CTA", "slot": "main", "sort_order": 6, "content": { "background": "band", "heading": "Request a technical briefing", "text": "Thirty minutes with an engineer. Bring your hardest deployment.", "primaryLabel": "Book a briefing", "primaryUrl": "/contact" } }
]
},
{
"slug": "/about",
"title": "About",
"template_key": "default",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "We build one autonomy stack", "level": 1 } },
{ "block_key": "text", "title": "Intro", "slot": "main", "sort_order": 2, "content": { "text": "<p>Halcyon Robotics builds a single runtime that carries a machine from raw sensor data to a motor command. We do not sell a demo. We sell the part that has to work at three in the morning in the rain.</p><p>The team came out of field robotics and controls. We test on real ground before we ship a line of code.</p>" } },
{ "block_key": "team", "title": "Team", "slot": "main", "sort_order": 3, "content": { "heading": "The founding engineers", "layout": "grid", "columns": 3, "members": [ { "name": "R. Novak", "role": "Perception", "bio": "Sensor fusion and detection for off-road ground vehicles." }, { "name": "S. Adeyemi", "role": "Controls", "bio": "Real-time control and state estimation on embedded targets." }, { "name": "T. Winters", "role": "Systems", "bio": "Runtime, safety cases, and field deployment." } ] } },
{ "block_key": "scifi-clean:changelog_console", "title": "Runtime changelog", "slot": "main", "sort_order": 4, "content": { "eyebrow": "Runtime", "heading": "Recent releases", "title": "halcyon-runtime", "entries": [ { "version": "v2.4.0", "date": "2026-06-28", "note": "Dust-robust detection model, retrained on the winter field set." }, { "version": "v2.3.1", "date": "2026-06-09", "note": "Localisation drift cut to 4 cm over a kilometre." }, { "version": "v2.3.0", "date": "2026-05-22", "note": "Control loop moved to a deterministic 1000 Hz scheduler." } ] } }
]
},
{
"slug": "/blog",
"title": "Field notes",
"template_key": "blog-index",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "card", "title": "Post 1", "slot": "main", "sort_order": 1, "content": { "title": "Why we run the control loop at 1000 Hz", "text": "<p>A slower loop looks fine on a bench and fails on gravel. Here is the reasoning.</p>", "link": "/blog/control-loop", "linkLabel": "Read" } },
{ "block_key": "card", "title": "Post 2", "slot": "main", "sort_order": 2, "content": { "title": "Measuring localisation drift honestly", "text": "<p>Drift over one clean kilometre is easy. Drift over a dusty one is the number that matters.</p>", "link": "/blog/localisation-drift", "linkLabel": "Read" } },
{ "block_key": "card", "title": "Post 3", "slot": "main", "sort_order": 3, "content": { "title": "Testing on real ground first", "text": "<p>Simulation earns a machine the right to a field test. It does not replace one.</p>", "link": "/blog/real-ground", "linkLabel": "Read" } }
]
},
{
"slug": "/blog/control-loop",
"parent_slug": "/blog",
"title": "Why we run the control loop at 1000 Hz",
"template_key": "article",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Why we run the control loop at 1000 Hz", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>On a paved test track a 200 Hz loop tracks a path cleanly. On loose gravel the wheels slip between updates and the estimate lags the truth. The machine corrects late and starts to hunt.</p><p>At 1000 Hz the loop sees the slip inside a single wheel rotation. The correction is small and the path stays smooth. The cost is a tighter compute budget, which is why the runtime is deterministic rather than best effort.</p>" } }
]
},
{
"slug": "/blog/localisation-drift",
"parent_slug": "/blog",
"title": "Measuring localisation drift honestly",
"template_key": "article",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Measuring localisation drift honestly", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Every vendor quotes a drift figure. Few say what surface it came from. Drift on a flat paved loop tells you almost nothing about a field run.</p><p>We report drift over a kilometre of mixed dirt and gravel with dust in the air. The number is worse and it is the one you can trust. Ask any supplier for the surface behind the figure.</p>" } }
]
},
{
"slug": "/blog/real-ground",
"parent_slug": "/blog",
"title": "Testing on real ground first",
"template_key": "article",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Testing on real ground first", "level": 1 } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Simulation is where a change proves it will not do something stupid. It is fast and it is cheap and it catches the obvious failures. It does not catch the ground.</p><p>Real dirt has ruts the model never saw and light the camera never met. A change earns a field test in simulation. It earns a release on the field.</p>" } }
]
},
{
"slug": "/contact",
"title": "Contact",
"template_key": "contact",
"reconcile_blocks": true,
"blocks": [
{ "block_key": "contact-form", "title": "Contact form", "slot": "main", "sort_order": 1, "content": { "heading": "Request a briefing", "description": "Tell us the deployment. We reply within one business day.", "submitLabel": "Send message", "successMessage": "Received. We will be in touch within one business day.", "formConfig": { "targetTable": "contact_submissions", "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": "organisation", "label": "Organisation", "type": "text", "required": false, "width": "full" }, { "name": "message", "label": "What are you deploying?", "type": "textarea", "required": true, "width": "full" } ] } },
{ "block_key": "contact-card", "title": "Contact details", "slot": "aside", "sort_order": 1, "content": { "heading": "Direct", "columns": 1, "channels": [ { "icon": "lucide:mail", "label": "Email", "value": "field@halcyon.example", "link": "mailto:field@halcyon.example" }, { "icon": "lucide:phone", "label": "Phone", "value": "+61 2 5550 0142", "link": "tel:+61255500142" } ] } },
{ "block_key": "hours-location", "title": "Lab hours", "slot": "aside", "sort_order": 2, "content": { "heading": "Lab", "address": "Unit 4, 22 Fabrication Way, Fremantle WA 6160", "hours": [ { "day": "Monday to Friday", "hours": "8:00 to 18:00" }, { "day": "Saturday and Sunday", "closed": true } ] } }
]
},
{
"slug": "/login",
"title": "Sign in",
"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": "Field notes", "page_slug": "/blog", "sort_order": 3 },
{ "menu": "main", "label": "Contact", "page_slug": "/contact", "sort_order": 4 }
]
}

19
seed/workflows.json Normal file
View File

@ -0,0 +1,19 @@
{
"workflows": [
{
"key": "contact_notify_admins",
"name": "Notify admins on contact submission",
"description": "Emails site admins whenever the Halcyon Robotics contact form receives a message.",
"enabled": true,
"trigger": { "type": "row_inserted", "table": "contact_submissions" },
"steps": [
{
"type": "email",
"name": "Email admins",
"template": "Form Submission Notification",
"recipients": { "mode": "admins", "scope": "all" }
}
]
}
]
}

View File

@ -1,53 +1,56 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<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 {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img { border: 0; height: auto; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
/* Mono numeral preservation across clients. */
.scifi-num {
font-family: ui-monospace, "JetBrains Mono", "Menlo", monospace !important;
font-variant-numeric: tabular-nums;
}
@media only screen and (max-width: 620px) {
.scifi-email-container { width: 100% !important; max-width: 100% !important; }
.scifi-pad { padding-left: 20px !important; padding-right: 20px !important; }
}
</style>
:root {
--sc-email-bg: hsl(210 16% 95%);
--sc-email-surface: hsl(0 0% 100%);
--sc-email-fg: hsl(215 30% 12%);
--sc-email-muted-fg: hsl(215 12% 42%);
--sc-email-border: hsl(215 14% 88%);
--sc-email-accent: hsl(215 90% 45%);
--sc-email-signal: hsl(18 95% 52%);
}
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; }
h1, h2, h3, h4, h5, h6 { font-family: 'Space Grotesk', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-weight: 600; letter-spacing: -0.02em; }
@media only screen and (max-width: 620px) {
.sc-email-container { width: 100% !important; max-width: 100% !important; }
.sc-email-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style="background-color: {{ colors.background|default:"#F6F7F8" }}; margin: 0; padding: 0; font-family: &amp;#39;Inter&amp;#39;, -apple-system, BlinkMacSystemFont, &amp;#39;Segoe UI&amp;#39;, sans-serif; color: {{ colors.foreground|default:"#16202E" }};">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden;">{{ preview_text }}</div>{% endif %}
<body style="background-color: var(--sc-email-bg, hsl(210 16% 95%)); margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 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: {{ colors.background|default:"#F6F7F8" }};">
<table role="presentation" class="scifi-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; background-color: {{ colors.card|default:"#FFFFFF" }}; border: 1px solid {{ colors.border|default:"#D6D9DD" }};">
<td align="center" style="padding: 32px 12px; background-color: var(--sc-email-bg, hsl(210 16% 95%));">
<table role="presentation" class="sc-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; width: 600px; background-color: var(--sc-email-surface, hsl(0 0% 100%)); border: 1px solid var(--sc-email-border, hsl(215 14% 88%)); border-collapse: separate;">
<!-- Header: title + single accent rule -->
<tr>
<td class="scifi-pad" style="padding: 20px 28px; border-bottom: 1px solid {{ colors.border|default:"#D6D9DD" }};">
<table width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="left" style="font-family: &amp;#39;Space Grotesk&amp;#39;, &amp;#39;Inter&amp;#39;, sans-serif; font-weight: 600; font-size: 16px; letter-spacing: 0.02em; color: {{ colors.foreground|default:"#16202E" }};">{{ site_name }}</td>
<td align="right" class="scifi-num" style="font-family: ui-monospace, &amp;#39;JetBrains Mono&amp;#39;, monospace; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: {{ colors.mutedForeground|default:"#5C6776" }};">{% if site_name %}SCF / {{ site_name }}{% else %}SCF-CLN{% endif %}</td>
</tr>
</table>
</td>
<td class="sc-email-padding" style="padding: 32px 48px 16px 48px; background-color: var(--sc-email-surface, hsl(0 0% 100%));">{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 36px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-size: 20px; font-weight: 600; color: var(--sc-email-fg, hsl(215 30% 12%)); letter-spacing: -0.02em; font-family: 'Space Grotesk', 'Inter', sans-serif;">{{ site_name }}</h1>{% endif %}</td>
</tr>
<tr>
<td class="scifi-pad" style="padding: 28px; font-size: 15px; line-height: 1.55; color: {{ colors.foreground|default:"#16202E" }};">{{ body|safe }}</td>
<td style="padding: 0 48px;"><div style="height: 2px; background-color: var(--sc-email-accent, hsl(215 90% 45%)); line-height: 2px; font-size: 0;">&nbsp;</div><div style="height: 2px; width: 48px; background-color: var(--sc-email-signal, hsl(18 95% 52%)); line-height: 2px; font-size: 0; margin-top: 2px;">&nbsp;</div></td>
</tr>
<!-- Body -->
<tr>
<td class="scifi-pad" style="padding: 16px 28px; border-top: 1px solid {{ colors.border|default:"#D6D9DD" }}; background-color: {{ colors.card|default:"#FFFFFF" }};">
<table width="100%" cellspacing="0" cellpadding="0" border="0">
<td class="sc-email-padding" style="padding: 32px 48px; color: var(--sc-email-fg, hsl(215 30% 12%)); font-size: 16px; line-height: 1.6; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;">{{ body|safe }}</td>
</tr>
<!-- Footer: legal line + unsubscribe -->
<tr>
<td class="sc-email-padding" style="padding: 24px 48px 32px 48px; border-top: 1px solid var(--sc-email-border, hsl(215 14% 88%));">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td class="scifi-num" align="left" style="font-family: ui-monospace, &amp;#39;JetBrains Mono&amp;#39;, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: {{ colors.mutedForeground|default:"#5C6776" }};">{{ site_url }}</td>
{% if unsubscribe_url %}<td class="scifi-num" align="right" style="font-family: ui-monospace, &amp;#39;JetBrains Mono&amp;#39;, monospace; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#5C6776" }}; text-decoration: underline;">Unsubscribe</a></td>{% endif %}
<td align="left">{% if site_name %}<p style="margin: 0 0 6px 0; font-size: 13px; color: var(--sc-email-fg, hsl(215 30% 12%)); font-family: 'Space Grotesk', 'Inter', sans-serif; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;">{{ site_name }}</p>{% endif %}<p style="margin: 0 0 12px 0; font-size: 12px; color: var(--sc-email-muted-fg, hsl(215 12% 42%)); font-family: 'Inter', sans-serif;">You are receiving this email 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: var(--sc-email-accent, hsl(215 90% 45%)); text-decoration: none;">{{ site_url }}</a></p>{% endif %}{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: var(--sc-email-muted-fg, hsl(215 12% 42%)); text-decoration: underline;">Unsubscribe</a></p>{% endif %}</td>
</tr>
</table>
</td>

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 = "sc-announce--promo" %}{% set annsty = "background:hsl(var(--primary));color:hsl(var(--primary-foreground));" %}{% elif variant == "alert" %}{% set annvar = "sc-announce--alert" %}{% set annsty = "background:hsl(var(--destructive));color:hsl(var(--destructive-foreground));" %}{% else %}{% set annvar = "sc-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="sc-announce {{ annvar }} {{ class }}" style="{{ annsty }}">
<div class="sc-content-well sc-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="sc-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;">::lucide:x:sm::</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="sc-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="sc-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="sc-audio-art" %}</div>{% endif %}
<div style="min-width:0;flex:1;">
{% if title %}<p class="sc-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="sc-btn-primary sc-btn-sm" aria-label="Play{% if title %}: {{ title }}{% endif %}" style="display:inline-flex;align-items:center;gap:0.375rem;">::lucide:play:sm:: Play</button>
{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="sc-btn-ghost" style="display:inline-flex;align-items:center;gap:0.25rem;font-size:0.8125rem;color:hsl(var(--muted-foreground));text-decoration:none;">::lucide:download:sm:: Download</a>{% endif %}
</div>
</div>
</div>
</figure><style>.sc-audio-art{display:block;width:100%;height:100%;object-fit:cover;}</style>

View File

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

View File

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

View File

@ -0,0 +1 @@
{% if context.currentAuthor %}<section class="sc-section {{ class }}" data-block="scifi-clean:author-bio-hero"><div class="sc-content-well"><div class="sc-measure sc-stack" style="text-align: center; margin-left: auto; margin-right: auto;">{% if showAvatar and context.currentAuthor.avatarUrl %}<img class="sc-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="sc-mark sc-mark-ring" aria-hidden="true" style="margin-left: auto; margin-right: auto;"></span>{% endif %}<h1 class="sc-display" style="margin: 0;">{{ context.currentAuthor.displayName }}</h1>{% if context.currentAuthor.bio %}<p class="sc-lede" style="margin: 0;">{{ context.currentAuthor.bio }}</p>{% endif %}{% if showSocial %}<div class="sc-btn-row" style="justify-content: center;">{% if context.currentAuthor.websiteUrl %}<a class="sc-btn-ghost sc-btn-sm" href="{{ context.currentAuthor.websiteUrl }}" target="_blank" rel="noopener noreferrer" title="Website"><svg class="sc-icon" aria-hidden="true"><use href="/icons/lucide.svg#globe"/></svg></a>{% endif %}{% if context.currentAuthor.twitterHandle %}<a class="sc-btn-ghost sc-btn-sm" href="https://twitter.com/{{ context.currentAuthor.twitterHandle|cut:"@" }}" target="_blank" rel="noopener noreferrer" title="X"><svg class="sc-icon" aria-hidden="true"><use href="/icons/simple-icons.svg#x"/></svg></a>{% endif %}{% if context.currentAuthor.linkedinUrl %}<a class="sc-btn-ghost sc-btn-sm" href="{{ context.currentAuthor.linkedinUrl }}" target="_blank" rel="noopener noreferrer" title="LinkedIn"><svg class="sc-icon" 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="sc-card sc-card-accent {{ class }}" data-block="scifi-clean:author-bio">{% if heading %}<p class="sc-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="sc-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="sc-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="sc-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="sc-section {{ class }}" data-block="scifi-clean:blog-hero"><div class="sc-content-well"><div class="sc-stack">{% if title %}<h1 class="sc-heading" style="font-size: clamp(2rem, 4vw, 3rem); margin: 0;"><span class="sc-heading-h2-accent">{{ title }}</span></h1>{% endif %}{% if subtitle %}<p class="sc-lede" style="max-width: 60ch; margin: 0.75rem 0 0 0;">{{ subtitle }}</p>{% endif %}</div>{% if posts %}<div class="sc-post-grid sc-post-grid-3" style="margin-top: 2.5rem;">{% for post in posts %}<article class="sc-post-card{% if forloop.First %} sc-card-accent{% endif %}">{% if post.featured_image_url %}<a href="{{ post.url }}" class="sc-post-thumb sc-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="sc-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="sc-post-excerpt">{{ post.excerpt }}</p>{% endif %}<div class="sc-post-meta">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="sc-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="sc-crumbs {{ class }}" data-block="scifi-clean: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="sc-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="sc-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="sc-sidebar-link" href="{{ item.url }}" style="padding: 0;">{{ item.label }}</a>{% endif %}</li>{% endfor %}</ol></nav>

View File

@ -1 +1,6 @@
{% if text %}<a href="{% if url %}{{ url }}{% else %}#{% endif %}" class="scifi-mono uppercase tracking-widest text-xs px-5 py-3 scifi-focus inline-flex items-center justify-center gap-2 {% if variant == "outline" %}hairline text-foreground bg-transparent{% elif variant == "destructive" %}bg-destructive text-destructive-foreground{% else %}bg-primary text-primary-foreground{% endif %}" style="font-family: var(--font-mono);"><span>{{ text }}</span> <span data-icon="chevron-right" aria-hidden="true">→</span></a>{% endif %}
{% set btnStyle = style|default:"primary" %}
{% set btnClass = "sc-btn-primary" %}
{% if btnStyle == "outline" or btnStyle == "secondary" %}{% set btnClass = "sc-btn-outline" %}{% elif btnStyle == "ghost" or btnStyle == "link" %}{% set btnClass = "sc-btn-ghost" %}{% endif %}
{% set sizeClass = "" %}
{% if size == "sm" %}{% set sizeClass = "sc-btn-sm" %}{% elif size == "lg" %}{% set sizeClass = "sc-btn-lg" %}{% endif %}
<div class="sc-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" %}::{{ icon }}:sm::{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>{% else %}<a class="{{ btnClass }} {{ sizeClass }}" href="{{ link|default:"#" }}">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>{% endif %}</div>

View File

@ -0,0 +1 @@
<article class="sc-card sc-card-accent {{ class }}">{% if media %}<div class="sc-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="sc-card-media" %}</div>{% endif %}{% if title %}<h3 class="sc-feature-title" style="margin-top: 0; font-size: 1.25rem;">{{ title }}</h3>{% endif %}{% if text %}<div class="sc-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="sc-btn-ghost" href="{{ link }}" style="margin-top: 1rem;">{{ linkLabel }}::lucide:arrow-right:sm::</a>{% endif %}</article>

View File

@ -0,0 +1 @@
<section class="sc-section-tight {{ class }}" data-block="scifi-clean:category-list">{% if heading %}<h2 class="sc-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="sc-tag">{{ cat.name }}{% if showCount %} <span class="sc-mono numeric-tabular">{{ cat.post_count }}</span>{% endif %}</a>{% endfor %}</div>{% else %}<ul class="sc-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for cat in categories %}<li><a href="{{ cat.url }}" class="sc-sidebar-link" style="display: flex; align-items: center; justify-content: space-between;"><span>{{ cat.name }}</span>{% if showCount %}<span class="sc-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="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if description %}<p class="sc-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
<div class="sc-grid {% if columns == 3 %}sc-grid-3{% elif columns == 1 %}{% else %}sc-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="sc-channel sc-card" style="display:flex;align-items:flex-start;gap:1rem;">
{% if channel.icon %}<span class="sc-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));">::{{ channel.icon }}:sm::</span>{% endif %}
<div style="min-width:0;">
{% if channel.label %}<div class="sc-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 sc-form primitives. #}
{% with bid=_block_id|default:context.blockId %}
<section id="contact-form-{{ bid }}" class="sc-section {{ class }}"><div class="sc-content-well" style="max-width:640px;">
{% if form.submitted %}
<div class="sc-card sc-card-accent sc-alert-ok" style="text-align:center;padding:2rem;">
<span class="sc-mark sc-mark-circle" aria-hidden="true" style="display:inline-block;margin-bottom:1rem;"></span>
<p class="sc-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="sc-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if description %}<p class="sc-lede" style="margin:0 0 2rem 0;color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
{% if form.errors._form %}<div class="sc-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="sc-form-grid" %}
{% for field in fields %}
<div class="sc-field {% if field.width == "half" %}{% else %}sc-field--full{% endif %}">
{% if field.label %}<label class="sc-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="sc-input"></textarea>
{% elif field.type == "select" %}
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="sc-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="sc-input">
{% endif %}
</div>
{% endfor %}
<div class="sc-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="sc-field--full"><button type="submit" class="sc-btn-primary">{{ submitLabel|default:"Send message" }}</button></div>
{% endform %}
{% endwith %}
{% endif %}
</div></section>
<style>#contact-form-{{ bid }} .sc-form-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.25rem;margin-top:2rem;}@media (max-width:640px){#contact-form-{{ bid }} .sc-form-grid{grid-template-columns:1fr;}}#contact-form-{{ bid }} .sc-field--full{grid-column:1 / -1;}#contact-form-{{ bid }} .sc-label{display:block;margin-bottom:0.375rem;font-size:0.8125rem;font-weight:500;color:hsl(var(--foreground));font-family:var(--font-heading, "Inter Tight", "Inter", system-ui, sans-serif);}#contact-form-{{ bid }} .sc-input{width:100%;padding:0.5rem 0.75rem;font-size:0.9375rem;font-family:var(--font-body, "Inter", 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 }} .sc-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="sc-section {{ class }}"><div class="sc-content-well" style="text-align: center;">
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="sc-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="sc-count-unit {% if variant == "boxes" %}sc-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="sc-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="sc-stat-label" style="margin-top: 0.5rem;">{{ labelDays|default:"Days" }}</span></div>
<div class="sc-count-unit {% if variant == "boxes" %}sc-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="sc-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="sc-stat-label" style="margin-top: 0.5rem;">{{ labelHours|default:"Hours" }}</span></div>
<div class="sc-count-unit {% if variant == "boxes" %}sc-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="sc-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="sc-stat-label" style="margin-top: 0.5rem;">{{ labelMinutes|default:"Minutes" }}</span></div>
<div class="sc-count-unit {% if variant == "boxes" %}sc-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="sc-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="sc-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="sc-heading" style="font-size: 1.25rem;">{{ expiredMessage|default:"This has started." }}</p>{% if expiredUrl and expiredLabel %}<a class="sc-btn-primary" href="{{ expiredUrl }}" style="margin-top: 1rem;">{{ expiredLabel }}</a>{% endif %}</div>
</div>
<noscript><p class="sc-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="sc-cta-strip {% if background == "band" %}sc-cta-strip--band{% elif background == "card" %}sc-cta-strip--card{% endif %} {{ class }}" style="padding-top: 4rem; padding-bottom: 4rem;"><div class="sc-content-well" style="display: flex; flex-wrap: wrap; align-items: center; gap: 2rem; {% if background == "split" %}justify-content: space-between;{% endif %}">
<div style="{% if background != "split" %}max-width: 60ch;{% endif %}">
{% if heading %}<h2 class="sc-cta-headline sc-heading" style="font-size: clamp(1.75rem, 4vw, 2.75rem); margin: 0; color: inherit;">{{ heading }}</h2>{% endif %}
{% if text %}<p class="sc-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="sc-btn-row" style="display: flex; flex-wrap: wrap; gap: 0.75rem; flex: none;">{% if primaryUrl and primaryLabel %}<a class="sc-btn-primary sc-btn-lg" href="{{ primaryUrl }}">{{ primaryLabel }}</a>{% endif %}{% if secondaryUrl and secondaryLabel %}<a class="sc-btn-outline sc-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="sc-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="sc-hairline-top" style="flex:1;height:0;"></span><span style="display:inline-flex;color:hsl(var(--muted-foreground));">::{{ icon|default:"lucide:asterisk" }}:md::</span><span class="sc-hairline-top" style="flex:1;height:0;"></span></div>{% elif variant == "spacer-rule" %}<hr class="sc-divider-line sc-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="sc-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="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if layout == "cards" %}
<div class="sc-grid sc-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="sc-card" style="display:flex;flex-direction:column;padding:0;overflow:hidden;{% if event.past %}opacity:0.6;{% endif %}">
{% if event.media %}<div class="sc-media-frame" style="aspect-ratio:16 / 9;overflow:hidden;border-bottom:1px solid hsl(var(--border));">{% img event.media alt=event.title|default:"" class="sc-event-media" %}</div>{% endif %}
<div style="display:flex;flex-direction:column;gap:0.5rem;padding:1.25rem;flex:1;">
<div class="sc-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="sc-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="sc-btn-ghost" href="{{ event.link }}" style="margin-top:auto;display:inline-flex;align-items:center;gap:0.375rem;">{{ event.linkLabel|default:"Details" }}::lucide:arrow-right:sm::</a>{% endif %}
</div>
</article>{% endif %}{% endfor %}
</div><style>.sc-event-media{display:block;width:100%;height:100%;object-fit:cover;}</style>
{% else %}
<ul class="sc-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="sc-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="sc-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="sc-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="sc-btn-outline sc-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="sc-section {{ class }}"><div class="sc-content-well sc-measure">
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width: 60ch; margin: 0 0 2rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if items %}<div class="sc-hairline-top" style="margin-top: 1.5rem;">{% for item in items %}<details class="sc-faq-item sc-hairline-bottom"{% if variant == "open-list" %} open{% endif %}><summary class="sc-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="sc-faq-chevron" aria-hidden="true" style="color: hsl(var(--muted-foreground)); flex: none;">::lucide:chevron-down:sm::</span></summary><div class="sc-faq-answer sc-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 — schematic cells, each led by a blueprint square mark. #}
<section class="sc-section {{ class }}"><div class="sc-content-well">
{% if eyebrow %}<p class="sc-eyebrow">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if items %}<div class="sc-grid {% if columns == 2 %}sc-grid-2{% elif columns == 4 %}sc-grid-4{% else %}sc-grid-3{% endif %}">{% for item in items %}<div class="sc-feature">{% if item.icon %}<span class="sc-mark sc-mark-square" aria-hidden="true">::{{ item.icon }}:sm::</span>{% else %}<span class="sc-mark sc-mark-square" aria-hidden="true"></span>{% endif %}{% if item.title %}<h3 class="sc-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="sc-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="sc-btn-ghost" href="{{ item.linkUrl }}" style="margin-top: 0.75rem;">{{ item.linkLabel }}::lucide:arrow-right:sm::</a>{% endif %}</div>{% endfor %}</div>{% endif %}
</div></section>

View File

@ -0,0 +1 @@
<section class="sc-section {{ class }}" data-block="scifi-clean:featured-posts"><div class="sc-content-well">{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 2rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}<div class="sc-post-grid {% if columns == 2 %}sc-post-grid-2{% else %}sc-post-grid-3{% endif %}">{% for post in posts %}<article class="sc-post-card">{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="sc-post-thumb sc-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="sc-post-title"><a href="{{ post.url }}">{{ post.title }}</a></h3>{% if showExcerpt and post.excerpt %}<p class="sc-post-excerpt">{{ post.excerpt }}</p>{% endif %}<div class="sc-post-meta">{% if showAuthor and post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if showDate and post.published_at %}<span class="sc-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}{% if post.reading_time %}<span class="sc-mono numeric-tabular">{{ post.reading_time }} min</span>{% endif %}</div></article>{% endfor %}</div></div></section>

View File

@ -0,0 +1,17 @@
{# footer override — schematic-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="sc-footer {{ class }}"><div class="sc-content-well">
<div class="sc-footer-cols" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:2.5rem;align-items:start;">
<div class="sc-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="sc-footer-logo" %}{% else %}<span class="sc-mark sc-mark-square" aria-hidden="true"></span><span class="sc-display" style="font-size:1.125rem;">{{ companyName|default:context.site.title|default:"Studio" }}</span>{% endif %}</a>
{% if tagline %}<p class="sc-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="sc-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="sc-footer-link" style="color:inherit;text-decoration:none;">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="sc-footer-link" style="color:inherit;text-decoration:none;">{{ phone }}</a></div>{% endif %}</address>{% endif %}
{% if showNewsletter %}<div class="sc-stack-sm" style="margin-top:0.5rem;display:flex;flex-direction:column;gap:0.5rem;"><p class="sc-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="sc-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="sc-stack-sm"><p class="sc-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="sc-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="sc-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="sc-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="sc-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="sc-social-link" style="display:inline-flex;color:hsl(var(--muted-foreground));">{% if s.icon %}::{{ s.icon }}:sm::{% endif %}</a>{% endfor %}</div>{% endif %}
</div>
</div><style>.sc-footer-logo{max-height:2rem;width:auto;display:block;}.sc-footer-link:hover{color:hsl(var(--foreground));}</style></footer>

View File

@ -0,0 +1,6 @@
{# gallery override — HUD 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 = "sc-gallery-4" %}{% set gn = 4 %}{% elif columns == 2 %}{% set gcols = "" %}{% set gn = 2 %}{% else %}{% set gcols = "sc-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="sc-gallery-grid {{ gcols }} {{ class }}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}" style="margin:1.5rem 0;gap:{{ ggap }};">
{% for image in images %}<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:'' }}" class="sc-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="sc-gallery-img" %}</a>{% endfor %}
</div><style>.sc-gallery-grid .sc-gallery-img{display:block;width:100%;height:100%;aspect-ratio:1;object-fit:cover;}</style>

View File

@ -1 +1 @@
<div class="my-6">{% if kicker %}<div class="scifi-mono uppercase tracking-widest text-xs text-accent mb-2">{{ kicker }}</div>{% endif %}{% if level|integer == 1 %}<h1 class="scifi-h1 text-4xl md:text-5xl font-medium tracking-tight text-foreground {{ textClass }}" style="font-family: var(--font-heading);">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="scifi-h3 text-2xl font-medium text-foreground {{ textClass }}" style="font-family: var(--font-heading);">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="scifi-h4 text-xl font-medium text-foreground {{ textClass }}" style="font-family: var(--font-heading);">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="scifi-h4 text-lg font-medium text-foreground {{ textClass }}" style="font-family: var(--font-heading);">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="scifi-h4 text-base font-medium uppercase tracking-widest text-muted-foreground {{ textClass }}" style="font-family: var(--font-heading);">{{ text }}</h6>{% else %}<h2 class="scifi-h2 text-3xl font-medium tracking-tight text-foreground {{ textClass }}" style="font-family: var(--font-heading);">{{ text }}</h2>{% endif %}</div>
{% if level|integer == 1 %}<h1 class="sc-heading sc-heading-h1 {{ textClass }}" style="font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1;">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="sc-heading sc-heading-h3 {{ textClass }}" style="font-size: 1.5rem; line-height: 1.3;">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="sc-heading sc-heading-h4 {{ textClass }}" style="font-size: 1.25rem; line-height: 1.3;">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="sc-heading sc-heading-h5 {{ textClass }}" style="font-size: 1.125rem; line-height: 1.4;">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="sc-heading sc-heading-h6 {{ textClass }}" style="font-size: 1rem; line-height: 1.4;">{{ text }}</h6>{% else %}<h2 class="sc-heading sc-heading-h2 {{ textClass }}" style="font-size: clamp(1.5rem, 3vw, 2.25rem); line-height: 1.2;"><span class="sc-heading-h2-accent">{{ text }}</span></h2>{% endif %}

View File

@ -0,0 +1,26 @@
{# hero override — schematic HUD headline over blueprint restraint. #}
{# styled-text fields accept a {text,color} object OR a bare string (.text|default:field). #}
{# MID motion: .sc-schematic line draws in via the page-template script; no-JS static. #}
{% set eyebrowText = eyebrow.text|default:eyebrow %}
{% set headlineText = headline.text|default:headline %}
{% set subText = subheadline.text|default:subheadline %}
{% set descText = description.text|default:description %}
{% set pText = ctaPrimary.text %}
{% set pUrl = ctaPrimary.url %}
{% set sText = ctaSecondary.text %}
{% set sUrl = ctaSecondary.url %}
{% set variant = template|default:"centered" %}
{% macro herobody() %}
{% if eyebrowText %}<p class="sc-eyebrow">{{ eyebrowText }}</p>{% endif %}
<div class="sc-schematic" data-sc-reveal style="max-width: 11rem; margin: 0 0 1.25rem 0;"><div class="sc-schematic-line"></div></div>
{% if headlineText %}<h1 class="sc-hero-headline sc-display" style="font-size: clamp(2.5rem, 6vw, 4.5rem); line-height: 1.02; letter-spacing: -0.03em; margin: 0; color: hsl(var(--foreground));">{{ headlineText }}</h1>{% endif %}
{% if subText %}<p class="sc-hero-sub sc-lede" style="margin: 1.25rem 0 0 0; font-size: 1.25rem; line-height: 1.5; color: hsl(var(--muted-foreground)); max-width: 60ch;">{{ subText }}</p>{% endif %}
{% if descText %}<p class="sc-text" style="margin: 1rem 0 0 0; color: hsl(var(--muted-foreground)); max-width: 60ch;">{{ descText }}</p>{% endif %}
{% if pText or sText %}<div class="sc-btn-row" style="display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 2rem;">{% if pText and pUrl %}<a class="sc-btn-primary sc-btn-lg" href="{{ pUrl }}">{{ pText }}</a>{% endif %}{% if sText and sUrl %}<a class="sc-btn-outline sc-btn-lg" href="{{ sUrl }}">{{ sText }}</a>{% endif %}</div>{% endif %}
{% if trustLogos %}<div class="sc-hairline-top" style="margin-top: 3rem; padding-top: 1.5rem;"><p class="sc-kicker sc-eyebrow" style="margin-bottom: 1rem; color: hsl(var(--muted-foreground));">Trusted by</p><div class="sc-logo-row" style="display: flex; flex-wrap: wrap; align-items: center; gap: 2rem;">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}">{% img logo.image alt=logo.alt class="sc-logo" %}</a>{% else %}{% img logo.image alt=logo.alt class="sc-logo" %}{% endif %}{% endfor %}</div></div>{% endif %}
{% endmacro %}
{% if variant == "split" %}
<section class="sc-hero sc-hero-split {{ class }}" style="padding-top: 4rem; padding-bottom: 4rem;"><div class="sc-content-well"><div class="sc-grid {% if backgroundImage %}sc-grid-2{% endif %}" style="align-items: center;"><div>{{ herobody() }}</div>{% if backgroundImage %}<div class="sc-media-frame" style="aspect-ratio: 4 / 3; background: hsl(var(--muted)); border: 1px solid hsl(var(--border)); overflow: hidden; {% if mediaPosition == "left" %}order: -1;{% endif %}">{% img backgroundImage alt=headlineText class="sc-hero-media" %}</div>{% endif %}</div></div></section>
{% else %}
<section class="sc-hero {{ class }}" style="padding-top: 4.5rem; padding-bottom: 3.5rem; {% if variant == "left-aligned" %}text-align: left;{% else %}text-align: center;{% endif %}"><div class="sc-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 %}

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="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
<div class="sc-grid sc-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="sc-hours-row sc-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="sc-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="sc-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="sc-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="sc-btn-outline" style="margin-top:1.5rem;display:inline-flex;align-items:center;gap:0.5rem;">::lucide:navigation:sm::Get directions</a>{% endif %}
</div>
{% if mapEmbedUrl %}
<div class="sc-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, 'Inter Tight', 'Inter', system-ui, sans-serif);font-weight:500;">::lucide:map-pin:md::{{ 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="sc-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="sc-image-el" %}</div>
{% if caption %}<figcaption class="sc-stat-label" style="margin-top:0.5rem;text-align:{% if align == "center" %}center{% else %}left{% endif %};">{{ caption }}</figcaption>{% endif %}
</figure><style>.sc-media-frame > .sc-image-el,.sc-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="sc-section-tight {{ class }}" style="padding-top: 3rem; padding-bottom: 3rem;"><div class="sc-content-well">
{% if heading %}<p class="sc-kicker sc-eyebrow" style="text-align: center; color: hsl(var(--muted-foreground)); margin-bottom: 0;">{{ heading }}</p>{% endif %}
<div class="sc-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="sc-logo" %}</a>{% else %}{% img logo.image alt=logo.alt class="sc-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="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
<div class="sc-grid {% if columns == 2 %}sc-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="sc-menu-section" style="break-inside:avoid;">
{% if section.title %}<h3 class="sc-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="sc-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="sc-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="sc-menu-price sc-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="sc-stat-label" style="margin-top:2.5rem;">{{ footnote }}</p>{% endif %}
</div></section>

View File

@ -0,0 +1 @@
<nav class="sc-navbar {{ class }}" aria-label="Primary"><a class="sc-brand" href="/"><span class="sc-mark sc-mark-square" aria-hidden="true"></span>{% if logoType == "image" and logo %}{% img logo alt=alt|default:"Home" class="sc-brand-logo" %}{% else %}<span>{{ logoText.text|default:logoText|default:context.site.title|default:"Studio" }}</span>{% endif %}</a><div class="sc-nav-links">{% for item in menu.items %}<a class="sc-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="sc-btn-primary sc-btn-sm" href="{{ cta.url }}">{{ cta.text }}</a>{% endif %}</div><input type="checkbox" id="sc-nav-toggle-cb" class="sc-nav-cb" style="display:none;"><label for="sc-nav-toggle-cb" class="sc-nav-toggle sc-btn-outline sc-btn-sm" aria-label="Toggle menu">::lucide:menu:sm::</label><div class="sc-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="sc-btn-primary sc-btn-sm" style="margin-top:0.75rem;">{{ cta.text }}</a>{% endif %}</div><style>.sc-nav-cb:checked ~ .sc-nav-drawer{display:flex;}</style></nav>

View File

@ -0,0 +1 @@
<section class="sc-section {{ class }}" data-block="scifi-clean:newsletter-signup"><div class="sc-content-well"><div class="{% if layout == "inline" %}sc-panel{% else %}sc-card sc-card-accent{% endif %}" style="text-align: center;">{% if title %}<h2 class="sc-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0;"><span class="sc-heading-h2-accent">{{ title }}</span></h2>{% endif %}{% if description %}<p class="sc-lede" style="max-width: 52ch; margin: 0.75rem auto 0 auto;">{{ description }}</p>{% endif %}<div class="sc-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="sc-section {{ class }}" data-block="scifi-clean:page-suggestions"><div class="sc-content-well"><div class="sc-measure sc-stack"><span class="sc-mark sc-mark-triangle" aria-hidden="true"></span><h2 class="sc-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0;"><span class="sc-heading-h2-accent">{{ title|default:"Were you looking for..." }}</span></h2>{% if suggestions %}<ul class="sc-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for s in suggestions %}<li><a class="sc-sidebar-link" href="/{{ s.slug }}" style="display: block;"><span class="sc-post-title" style="font-size: 1rem;">{{ s.title }}</span>{% if showDescription and s.excerpt %}<span class="sc-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="sc-text" style="margin: 0; color: hsl(var(--muted-foreground));">{{ emptyMessage }}</p>{% endif %}<div class="sc-btn-row"><a class="sc-btn-outline sc-btn-sm" href="/">::lucide:home:sm::Home</a><a class="sc-btn-outline sc-btn-sm" href="/blog">::lucide:newspaper:sm::Blog</a></div>{% endif %}</div></div></section>

View File

@ -0,0 +1,11 @@
<div class="sc-stack {{ class }}" data-block="scifi-clean:password-reset-form">
<span class="sc-mark sc-mark-ring" aria-hidden="true"></span>
<h2 class="sc-heading" style="font-size: 1.5rem; margin: 0;">Reset password</h2>
<p class="sc-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="sc-form" hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML">
<div class="sc-field sc-field--full"><label class="sc-label" for="sc-reset-email">Email</label><input class="sc-input" type="email" name="email" id="sc-reset-email" required></div>
<button type="submit" class="sc-btn-primary" hx-disabled-elt="this" style="width:100%; justify-content:center;">Send reset link</button>
</form>
<a class="sc-btn-ghost sc-btn-sm" href="/login" style="align-self: flex-start;">::lucide:arrow-left:sm::Back to sign in</a>
</div>

View File

@ -0,0 +1 @@
<section class="sc-section-tight {{ class }}" data-block="scifi-clean:popular-posts">{% if heading %}<h2 class="sc-kicker">{{ heading }}</h2>{% endif %}<ol class="sc-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="sc-rank numeric-tabular">{{ forloop.Counter }}</span>{% endif %}{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="sc-post-thumb sc-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="sc-post-title" style="font-size: 0.9375rem;"><a href="{{ post.url }}">{{ post.title }}</a></h3>{% if post.published_at %}<p class="sc-post-meta"><span class="sc-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="sc-section {{ class }}" data-block="scifi-clean:post-hero"><div class="sc-content-well">{% if context.currentPost.featuredImageUrl %}<div class="sc-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="sc-measure sc-stack" style="{% if align == "left" %}text-align: left;{% else %}margin-left: auto; margin-right: auto; text-align: center;{% endif %}"><span class="sc-mark sc-mark-square" aria-hidden="true"{% if align != "left" %} style="margin-left: auto; margin-right: auto;"{% endif %}></span><h1 class="sc-display" style="margin: 0;">{{ context.currentPost.title }}</h1>{% if showExcerpt and context.currentPost.excerpt %}<p class="sc-lede" style="margin: 0;">{{ context.currentPost.excerpt }}</p>{% endif %}{% if showMeta %}<div class="sc-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="sc-mono numeric-tabular">{{ context.currentPost.publishedAt }}</span>{% endif %}{% if context.currentPost.readingTimeMinutes %}<span class="sc-mono numeric-tabular">{{ context.currentPost.readingTimeMinutes }} min read</span>{% endif %}</div>{% endif %}</div></div></section>

View File

@ -0,0 +1 @@
<div class="sc-post-meta {{ class }}" data-block="scifi-clean: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="sc-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="sc-mono numeric-tabular" datetime="{{ context.currentPost.publishedAtIso }}">{{ context.currentPost.publishedAt }}</time>{% endif %}{% if showReadingTime and context.currentPost.readingTimeMinutes %}<span class="sc-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="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if tiers %}<div class="sc-grid sc-grid-3">{% for tier in tiers %}<div class="sc-card sc-price-tier {% if tier.highlighted %}sc-price-tier--featured sc-card-accent{% endif %}" style="display: flex; flex-direction: column;">
{% if tier.badge %}<span class="sc-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="sc-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="sc-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="sc-price-period sc-stat-label" style="margin-top: 0;">{{ tier.period }}</span>{% endif %}</p>
{% if tier.description %}<p class="sc-text" style="font-size: 0.9375rem; color: hsl(var(--muted-foreground)); margin-top: 0.75rem;">{{ tier.description }}</p>{% endif %}
{% if tier.features %}<ul class="sc-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="sc-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;">::lucide:check:sm::</span><span>{{ feature }}</span></li>{% endfor %}</ul>{% endif %}
{% if tier.ctaUrl and tier.ctaLabel %}{% if tier.highlighted %}<a class="sc-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="sc-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="sc-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 = "sc-pull-quote" %}{% set qsty = "margin:2rem 0;" %}{% elif style == "inline-card" %}{% set qcls = "sc-quote-block sc-card sc-card-accent" %}{% set qsty = "margin:2rem 0;" %}{% else %}{% set qcls = "sc-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="sc-quote-cite" style="margin-top:1rem;font-size:0.875rem;color:hsl(var(--muted-foreground));">{% if attribution %}<span class="sc-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>

View File

@ -0,0 +1 @@
<section class="sc-section sc-hairline-top {{ class }}" data-block="scifi-clean:related-posts"><div class="sc-content-well"><h2 class="sc-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0 0 2rem 0;"><span class="sc-heading-h2-accent">{{ heading|default:"Related posts" }}</span></h2><div class="sc-post-grid {% if columns == 2 %}sc-post-grid-2{% else %}sc-post-grid-3{% endif %}">{% for post in posts %}<article class="sc-post-card">{% if post.featured_image_url %}<a href="{{ post.url }}" class="sc-post-thumb sc-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="sc-post-title"><a href="{{ post.url }}">{{ post.title }}</a></h3>{% if post.published_at %}<div class="sc-post-meta"><span class="sc-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span></div>{% endif %}</article>{% endfor %}</div></div></section>

View File

@ -0,0 +1,10 @@
{# rich-section override — two-column media + rich text, hairline surface options. #}
<section class="sc-section {% if background == "muted" %}sc-surface-muted{% elif background == "card" %}sc-surface-card{% endif %} {{ class }}"><div class="sc-content-well"><div class="sc-grid {% if media %}sc-grid-2{% endif %}" style="align-items: center;">
<div>
{% if eyebrow %}<p class="sc-eyebrow">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 1rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if body %}<div class="sc-text" style="color: hsl(var(--muted-foreground)); line-height: 1.7;">{{ body|safe }}</div>{% endif %}
{% if ctaUrl and ctaLabel %}<a class="sc-btn-ghost" href="{{ ctaUrl }}" style="margin-top: 1.5rem;">{{ ctaLabel }}::lucide:arrow-right:sm::</a>{% endif %}
</div>
{% if media %}<div class="sc-media-frame" style="aspect-ratio: 4 / 3; background: hsl(var(--muted)); border: 1px solid hsl(var(--border)); overflow: hidden; {% if mediaPosition == "left" %}order: -1;{% endif %}">{% img media alt=mediaAlt class="sc-rich-media" %}</div>{% endif %}
</div></div></section>

View File

@ -0,0 +1,2 @@
{# rich-text override — raw imported HTML inside the schematic reading measure. #}
{% if html %}<div class="sc-text sc-measure {{ class }}" style="max-width:70ch;margin-left:auto;margin-right:auto;">{{ html|safe }}</div>{% endif %}

View File

@ -0,0 +1,3 @@
<nav class="{{ class }}" data-block="scifi-clean:sidebar-nav" style="{% if showBorder %}border-right: 1px solid hsl(var(--border)); padding-right: 1rem;{% endif %}">{% if title %}<h3 class="sc-kicker" style="margin-top: 0;">{{ title }}</h3>{% endif %}<ul class="sc-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for item in menu.items %}<li><a class="sc-sidebar-link" href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link>{% if item.icon %}<svg class="sc-icon" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"/></svg>{% endif %}{{ item.label }}</a></li>{% endfor %}</ul>{% if highlightCurrent %}<script>
(function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.setAttribute('aria-current','page');}});})();
</script>{% endif %}</nav>

View File

@ -0,0 +1,7 @@
{# social-links override — icon row or labelled stack; each link a hairline sc-social-link (rendered only when it has a URL). #}
<div class="{{ class }}">
{% if heading %}<p class="sc-stat-label" style="margin:0 0 0.75rem 0;">{{ heading }}</p>{% endif %}
<ul class="sc-social-row" style="list-style:none;margin:0;padding:0;{% if style == "stack" %}display:flex;flex-direction:column;gap:0.5rem;{% else %}display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem;{% endif %}">
{% for link in links %}{% if link.url %}<li><a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" aria-label="{{ link.label|default:link.network }}" class="sc-social-link" style="display:inline-flex;align-items:center;gap:0.5rem;border:1px solid hsl(var(--border));border-radius:4px;color:hsl(var(--foreground));text-decoration:none;{% if style == "stack" %}padding:0.5rem 1rem;{% else %}padding:0.5rem;{% endif %}">{% if link.network %}::{{ link.network }}:sm::{% endif %}{% if style == "stack" and link.label %}<span style="font-size:0.875rem;font-weight:500;">{{ link.label }}</span>{% endif %}</a></li>{% endif %}{% endfor %}
</ul>
</div>

View File

@ -0,0 +1,6 @@
{# stats override — oversized tabular figures over a hairline, no count-up JS. #}
<section class="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if items %}<div class="sc-grid {% if columns == 2 %}sc-grid-2{% elif columns == 3 %}sc-grid-3{% else %}sc-grid-4{% endif %}">{% for item in items %}<div class="sc-hairline-top" style="padding-top: 1.25rem;"><div class="sc-stat-figure numeric-tabular" style="font-size: clamp(2.5rem, 5vw, 3.5rem);">{% if item.prefix %}{{ item.prefix }}{% endif %}{{ item.value }}{% if item.suffix %}{{ item.suffix }}{% endif %}</div>{% if item.label %}<div class="sc-stat-label">{{ item.label }}</div>{% endif %}</div>{% endfor %}</div>{% endif %}
</div></section>

View File

@ -0,0 +1,12 @@
{# team override — leadership grid, square-framed portraits, restrained social row. #}
<section class="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if members %}<div class="{% if layout == "list" %}sc-stack{% elif layout == "single" %}sc-measure{% else %}sc-leadership-grid{% endif %}">{% for member in members %}<div class="sc-feature">
{% if member.photo %}<div class="sc-media-frame" style="aspect-ratio: 1 / 1; background: hsl(var(--muted)); border: 1px solid hsl(var(--border)); overflow: hidden; margin-bottom: 1rem;">{% img member.photo alt=member.name class="sc-team-photo" %}</div>{% endif %}
{% if member.name %}<h3 class="sc-feature-title" style="margin: 0;">{{ member.name }}</h3>{% endif %}
{% if member.role %}<div class="sc-stat-label" style="margin-top: 0.25rem;">{{ member.role }}</div>{% endif %}
{% if member.bio %}<p class="sc-text" style="font-size: 0.9375rem; line-height: 1.6; color: hsl(var(--muted-foreground)); margin-top: 0.75rem;">{{ member.bio }}</p>{% endif %}
{% if member.socials %}<div class="sc-btn-row" style="display: flex; gap: 0.75rem; margin-top: 1rem;">{% for social in member.socials %}{% if social.url %}<a href="{{ social.url }}" aria-label="Profile link" style="color: hsl(var(--muted-foreground));">::{{ social.icon }}:sm::</a>{% endif %}{% endfor %}</div>{% endif %}
</div>{% endfor %}</div>{% endif %}
</div></section>

View File

@ -0,0 +1,9 @@
{# testimonial override — accent-ruled pull quotes, optional 5-star scale. #}
<section class="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 2.5rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
<div class="{% if layout == "single" %}sc-measure{% else %}sc-grid sc-grid-3{% endif %}">{% for q in quotes %}<figure class="sc-quote-block sc-card" style="margin: 0; display: flex; flex-direction: column;">
{% if q.rating %}<div class="sc-rating" aria-label="Rated {{ q.rating }} out of 5" style="display: flex; gap: 0.125rem; margin-bottom: 0.75rem;"><span style="color: {% if q.rating >= 1 %}hsl(var(--accent)){% else %}hsl(var(--muted-foreground) / 0.35){% endif %};">::lucide:star:sm::</span><span style="color: {% if q.rating >= 2 %}hsl(var(--accent)){% else %}hsl(var(--muted-foreground) / 0.35){% endif %};">::lucide:star:sm::</span><span style="color: {% if q.rating >= 3 %}hsl(var(--accent)){% else %}hsl(var(--muted-foreground) / 0.35){% endif %};">::lucide:star:sm::</span><span style="color: {% if q.rating >= 4 %}hsl(var(--accent)){% else %}hsl(var(--muted-foreground) / 0.35){% endif %};">::lucide:star:sm::</span><span style="color: {% if q.rating >= 5 %}hsl(var(--accent)){% else %}hsl(var(--muted-foreground) / 0.35){% endif %};">::lucide:star:sm::</span></div>{% endif %}
{% if q.quote %}<blockquote class="sc-pull-quote" style="flex: 1; margin: 0;">{{ q.quote }}</blockquote>{% endif %}
<figcaption class="sc-quote-cite" style="display: flex; align-items: center; gap: 0.75rem; margin-top: 1.25rem;">{% if q.avatar %}<span class="sc-avatar" style="width: 2.75rem; height: 2.75rem; border-radius: 9999px; overflow: hidden; background: hsl(var(--muted)); display: inline-flex; flex: none;">{% img q.avatar alt=q.name class="sc-avatar-img" %}</span>{% endif %}<span>{% if q.name %}<span class="sc-feature-title" style="display: block; font-size: 0.9375rem;">{{ q.name }}</span>{% endif %}{% if q.role %}<span class="sc-stat-label" style="margin-top: 0;">{{ q.role }}</span>{% endif %}</span></figcaption>
</figure>{% endfor %}</div>
</div></section>

View File

@ -1 +1 @@
<div class="scifi-body prose max-w-none text-foreground tabular-nums {{ class }}" style="font-family: var(--font-body);">{{ text|safe }}</div>
<div class="sc-text {{ class }}">{{ text|safe }}</div>

View File

@ -0,0 +1,6 @@
{# timeline override — numbered schematic rail (mirrors engagement_process). #}
<section class="sc-section {{ class }}"><div class="sc-content-well">
{% if heading %}<h2 class="sc-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="sc-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="sc-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
{% if steps %}<div>{% for step in steps %}<div class="sc-timeline-step"><div class="sc-timeline-rail"><span class="sc-timeline-node numeric-tabular">{{ forloop.Counter }}</span>{% if not forloop.Last %}<span class="sc-timeline-line" aria-hidden="true"></span>{% endif %}</div><div style="padding-top: 0.375rem;">{% if step.label %}<p class="sc-eyebrow" style="margin-bottom: 0.25rem;">{{ step.label }}</p>{% endif %}{% if step.title %}<h3 class="sc-feature-title" style="margin-top: 0;">{{ step.title }}</h3>{% endif %}{% if step.text %}<p class="sc-text" style="font-size: 0.9375rem; line-height: 1.6; color: hsl(var(--muted-foreground));">{{ step.text }}</p>{% endif %}{% if step.media %}<div class="sc-media-frame" style="aspect-ratio: 16 / 9; border: 1px solid hsl(var(--border)); overflow: hidden; margin-top: 1rem;">{% img step.media alt=step.title class="sc-timeline-media" %}</div>{% endif %}</div></div>{% endfor %}</div>{% else %}<p class="sc-stat-label">No steps configured.</p>{% endif %}
</div></section>

View File

@ -0,0 +1,9 @@
{# utility-bar override — thin HUD top strip; left/right item groups; icon-picker fields render via ::pack:name:: shorthand. #}
{% if variant == "primary" %}{% set ubvar = "sc-utility-bar--primary" %}{% set ubsty = "background:hsl(var(--primary));color:hsl(var(--primary-foreground));" %}{% elif variant == "dark" %}{% set ubvar = "sc-utility-bar--dark" %}{% set ubsty = "background:hsl(var(--foreground));color:hsl(var(--background));" %}{% else %}{% set ubvar = "sc-utility-bar--muted" %}{% set ubsty = "background:hsl(var(--muted));color:hsl(var(--muted-foreground));border-bottom:1px solid hsl(var(--border));" %}{% endif %}
{% macro ubitem(it) %}{% if it.link %}<a href="{{ it.link }}" class="sc-utility-item" style="display:inline-flex;align-items:center;gap:0.375rem;color:inherit;text-decoration:none;">{% if it.icon %}::{{ it.icon }}:sm::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</a>{% else %}<span class="sc-utility-item" style="display:inline-flex;align-items:center;gap:0.375rem;">{% if it.icon %}::{{ it.icon }}:sm::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</span>{% endif %}{% endmacro %}
<div class="sc-utility-bar {{ ubvar }} {{ class }}" style="{{ ubsty }}font-size:0.75rem;font-family:var(--font-body, 'Inter', system-ui, sans-serif);">
<div class="sc-content-well sc-utility-row" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;min-height:2.25rem;">
<div class="sc-utility-group" style="display:flex;align-items:center;gap:1.25rem;flex-wrap:wrap;">{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}</div>
<div class="sc-utility-group" style="display:flex;align-items:center;gap:1.25rem;flex-wrap:wrap;justify-content:flex-end;">{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
{# video-embed override — click-to-load facade; carries data-bn-video / data-video-url / data-video-media so the host facade JS binds. #}
{% if aspect == "4:3" %}{% set vratio = "4 / 3" %}{% elif aspect == "1:1" %}{% set vratio = "1 / 1" %}{% elif aspect == "21:9" %}{% set vratio = "21 / 9" %}{% else %}{% set vratio = "16 / 9" %}{% endif %}
<figure class="{{ class }}" style="margin:2rem 0;">
<div class="sc-video-frame" data-bn-video data-video-url="{{ url|default:'' }}" data-video-media="{% if media %}{{ media|mediaURL }}{% endif %}" style="position:relative;width:100%;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--muted));aspect-ratio:{{ vratio }};">
<button type="button" data-bn-video-play class="sc-video-play" aria-label="Play video{% if title %}: {{ title }}{% endif %}" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;border:0;background:none;cursor:pointer;">
{% if poster %}{% img poster alt=title|default:"" class="sc-video-poster" %}{% endif %}
<span class="sc-video-play-badge" style="position:relative;display:inline-flex;align-items:center;justify-content:center;width:4rem;height:4rem;border-radius:4px;background:hsl(var(--accent));color:hsl(var(--accent-foreground));">::lucide:play:lg::</span>
</button>
</div>
{% if caption %}<figcaption class="sc-stat-label" style="margin-top:0.5rem;text-align:center;">{{ caption }}</figcaption>{% endif %}
</figure><style>.sc-video-frame .sc-video-poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}</style>

View File

@ -1,18 +1,75 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
<html lang="en">
{{ head_html|safe }}
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
<body class="sc-body" style="margin: 0; min-height: 100vh; display: flex; flex-direction: column; background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<header class="w-full hairline-b">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">
<div class="max-w-5xl mx-auto px-4 py-12 grid grid-cols-12 gap-8">
{% if slots.rail %}<aside class="col-span-12 md:col-span-3 scifi-mono uppercase text-xs tracking-widest text-muted-foreground space-y-3">{{ slots.rail|safe }}</aside>{% endif %}
<article class="col-span-12 md:col-span-9 scifi-body max-w-2xl">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="text-muted-foreground">No article body.</p>{% endif %}
<header class="sc-hairline-bottom" style="width: 100%;">
<div class="sc-content-well">{{ slots.header|safe }}</div>
</header>
<main class="sc-content-well" style="flex: 1 1 auto; padding-top: 3rem; padding-bottom: 3rem;" data-content-well>
<div class="sc-swiss-12" data-grid="swiss-12">
<article class="sc-article-measure" style="grid-column: span 12 / span 12;">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="sc-stat-label">No article content yet.</p>{% endif %}
</article>
</div>
{% if slots.aside %}<aside class="sc-hairline-top" style="margin-top: 3rem; padding-top: 2rem;">{{ slots.aside|safe }}</aside>{% endif %}
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
<footer style="width: 100%; margin-top: auto;">{{ slots.footer|safe }}</footer>
<script>
/* Sci-Fi Clean MID motion: scroll-reveal, line-draw schematics, ticking
counters. Dependency-free. prefers-reduced-motion => everything static
and counters keep their final text. No JS => nothing runs, all visible. */
(function(){
var root = document.documentElement;
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return;
root.classList.add('sc-js');
var revealSel = '[data-sc-reveal], .sc-schematic';
var items = [].slice.call(document.querySelectorAll(revealSel));
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { e.target.classList.add('sc-in'); io.unobserve(e.target); }
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}
var counters = [].slice.call(document.querySelectorAll('[data-sc-count]'));
function animate(el){
var raw = String(el.getAttribute('data-sc-count') || '');
var m = raw.match(/-?[0-9]+(\.[0-9]+)?/);
if (!m) return;
var target = parseFloat(m[0]);
var decimals = (m[0].split('.')[1] || '').length;
var prefix = raw.slice(0, m.index);
var suffix = raw.slice(m.index + m[0].length);
var start = null, dur = 1100;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = prefix + (target * eased).toFixed(decimals) + suffix;
if (p < 1) requestAnimationFrame(step);
else el.textContent = raw;
}
requestAnimationFrame(step);
}
if ('IntersectionObserver' in window) {
var co = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { animate(e.target); co.unobserve(e.target); }
});
}, { threshold: 0.4 });
counters.forEach(function(el){ co.observe(el); });
} else {
counters.forEach(animate);
}
})();
</script>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,72 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="sc-body" style="margin: 0; min-height: 100vh; display: flex; flex-direction: column; background-color: hsl(var(--muted)); 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: 26rem;">
<a href="/" aria-label="{{ site_settings.Title|default:"Home" }}" style="display: inline-flex; align-items: center; gap: 0.625rem; text-decoration: none; margin-bottom: 1.5rem;"><span class="sc-mark sc-mark-square" aria-hidden="true"></span><span class="sc-brand">{{ site_settings.Title|default:"Sci-Fi Clean" }}</span></a>
<div class="sc-card sc-card-accent sc-reveal" style="padding: 2rem;">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="sc-stat-label">No form assigned to this page.</p>{% endif %}
</div>
{% if slots.footer %}<div class="sc-stat-label" style="text-align: center; margin-top: 1.5rem; text-transform: none; letter-spacing: 0;">{{ slots.footer|safe }}</div>{% endif %}
</div>
</main>
<script>
/* Sci-Fi Clean MID motion: scroll-reveal, line-draw schematics, ticking
counters. Dependency-free. prefers-reduced-motion => everything static
and counters keep their final text. No JS => nothing runs, all visible. */
(function(){
var root = document.documentElement;
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return;
root.classList.add('sc-js');
var revealSel = '[data-sc-reveal], .sc-schematic';
var items = [].slice.call(document.querySelectorAll(revealSel));
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { e.target.classList.add('sc-in'); io.unobserve(e.target); }
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}
var counters = [].slice.call(document.querySelectorAll('[data-sc-count]'));
function animate(el){
var raw = String(el.getAttribute('data-sc-count') || '');
var m = raw.match(/-?[0-9]+(\.[0-9]+)?/);
if (!m) return;
var target = parseFloat(m[0]);
var decimals = (m[0].split('.')[1] || '').length;
var prefix = raw.slice(0, m.index);
var suffix = raw.slice(m.index + m[0].length);
var start = null, dur = 1100;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = prefix + (target * eased).toFixed(decimals) + suffix;
if (p < 1) requestAnimationFrame(step);
else el.textContent = raw;
}
requestAnimationFrame(step);
}
if ('IntersectionObserver' in window) {
var co = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { animate(e.target); co.unobserve(e.target); }
});
}, { threshold: 0.4 });
counters.forEach(function(el){ co.observe(el); });
} else {
counters.forEach(animate);
}
})();
</script>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,72 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="sc-body" style="margin: 0; min-height: 100vh; display: flex; flex-direction: column; background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="sc-hairline-bottom" style="width: 100%;"><div class="sc-content-well">{{ slots.header|safe }}</div></header>{% endif %}
<main style="flex: 1 1 auto; width: 100%;" data-content-well>
<div class="sc-content-well sc-section">
{% if title %}<div class="sc-accent-rule" style="padding-top: 1.5rem; margin-bottom: 3rem;"><p class="sc-eyebrow">Insights</p><h1 class="sc-heading" style="font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1; margin: 0;">{{ title }}</h1>{% if page_meta.MetaDescription %}<p class="sc-lede" style="max-width: 60ch; margin: 1rem 0 0;">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
{% if slots.featured %}<section style="margin-bottom: 3rem;">{{ slots.featured|safe }}</section>{% endif %}
{% if slots.main %}<section>{{ slots.main|safe }}</section>{% else %}<p class="sc-stat-label">No posts published yet.</p>{% endif %}
</div>
</main>
{% if slots.footer %}<footer style="width: 100%; margin-top: auto;">{{ slots.footer|safe }}</footer>{% endif %}
<script>
/* Sci-Fi Clean MID motion: scroll-reveal, line-draw schematics, ticking
counters. Dependency-free. prefers-reduced-motion => everything static
and counters keep their final text. No JS => nothing runs, all visible. */
(function(){
var root = document.documentElement;
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return;
root.classList.add('sc-js');
var revealSel = '[data-sc-reveal], .sc-schematic';
var items = [].slice.call(document.querySelectorAll(revealSel));
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { e.target.classList.add('sc-in'); io.unobserve(e.target); }
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}
var counters = [].slice.call(document.querySelectorAll('[data-sc-count]'));
function animate(el){
var raw = String(el.getAttribute('data-sc-count') || '');
var m = raw.match(/-?[0-9]+(\.[0-9]+)?/);
if (!m) return;
var target = parseFloat(m[0]);
var decimals = (m[0].split('.')[1] || '').length;
var prefix = raw.slice(0, m.index);
var suffix = raw.slice(m.index + m[0].length);
var start = null, dur = 1100;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = prefix + (target * eased).toFixed(decimals) + suffix;
if (p < 1) requestAnimationFrame(step);
else el.textContent = raw;
}
requestAnimationFrame(step);
}
if ('IntersectionObserver' in window) {
var co = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { animate(e.target); co.unobserve(e.target); }
});
}, { threshold: 0.4 });
counters.forEach(function(el){ co.observe(el); });
} else {
counters.forEach(animate);
}
})();
</script>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,78 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="sc-body" style="margin: 0; min-height: 100vh; display: flex; flex-direction: column; background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
{% if slots.header %}<header class="sc-hairline-bottom" style="width: 100%;"><div class="sc-content-well">{{ slots.header|safe }}</div></header>{% endif %}
<main style="flex: 1 1 auto; width: 100%;" data-content-well>
<div class="sc-content-well sc-section">
{% if slots.aside %}
<div class="sc-swiss-12">
<div style="grid-column: span 12 / span 12;" class="sc-contact-main">{{ slots.main|safe }}</div>
<aside style="grid-column: span 12 / span 12;" class="sc-hairline-top sc-contact-aside">{{ slots.aside|safe }}</aside>
</div>
<style>@media (min-width: 1024px){.sc-contact-main{grid-column: span 7 / span 7 !important;} .sc-contact-aside{grid-column: 9 / span 4 !important; border-top: 0 !important; padding-left: 2rem; border-left: 1px solid hsl(var(--border));}}</style>
{% else %}
<div class="sc-swiss-12"><div style="grid-column: span 12 / span 12;" class="sc-measure">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="sc-stat-label">No content blocks assigned to this page.</p>{% endif %}</div></div>
{% endif %}
</div>
</main>
{% if slots.footer %}<footer style="width: 100%; margin-top: auto;">{{ slots.footer|safe }}</footer>{% endif %}
<script>
/* Sci-Fi Clean MID motion: scroll-reveal, line-draw schematics, ticking
counters. Dependency-free. prefers-reduced-motion => everything static
and counters keep their final text. No JS => nothing runs, all visible. */
(function(){
var root = document.documentElement;
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return;
root.classList.add('sc-js');
var revealSel = '[data-sc-reveal], .sc-schematic';
var items = [].slice.call(document.querySelectorAll(revealSel));
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { e.target.classList.add('sc-in'); io.unobserve(e.target); }
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}
var counters = [].slice.call(document.querySelectorAll('[data-sc-count]'));
function animate(el){
var raw = String(el.getAttribute('data-sc-count') || '');
var m = raw.match(/-?[0-9]+(\.[0-9]+)?/);
if (!m) return;
var target = parseFloat(m[0]);
var decimals = (m[0].split('.')[1] || '').length;
var prefix = raw.slice(0, m.index);
var suffix = raw.slice(m.index + m[0].length);
var start = null, dur = 1100;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = prefix + (target * eased).toFixed(decimals) + suffix;
if (p < 1) requestAnimationFrame(step);
else el.textContent = raw;
}
requestAnimationFrame(step);
}
if ('IntersectionObserver' in window) {
var co = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { animate(e.target); co.unobserve(e.target); }
});
}, { threshold: 0.4 });
counters.forEach(function(el){ co.observe(el); });
} else {
counters.forEach(animate);
}
})();
</script>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,13 +1,74 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
<html lang="en">
{{ head_html|safe }}
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
<body class="sc-body" style="margin: 0; min-height: 100vh; display: flex; flex-direction: column; background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow max-w-5xl mx-auto w-full px-4 py-8">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center scifi-mono uppercase tracking-widest text-xs text-muted-foreground">No content blocks assigned to this page.</div>{% endif %}
<header class="sc-hairline-bottom" style="width: 100%;">
<div class="sc-content-well">{{ slots.header|safe }}</div>
</header>
<main class="sc-content-well" style="flex: 1 1 auto; padding-top: 3rem; padding-bottom: 3rem;" data-content-well>
<div class="sc-swiss-12" data-grid="swiss-12">
<div style="grid-column: span 12 / span 12;">
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="sc-stat-label">No content blocks assigned to this page.</p>{% endif %}
</div>
</div>
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
<footer style="width: 100%; margin-top: auto;">{{ slots.footer|safe }}</footer>
<script>
/* Sci-Fi Clean MID motion: scroll-reveal, line-draw schematics, ticking
counters. Dependency-free. prefers-reduced-motion => everything static
and counters keep their final text. No JS => nothing runs, all visible. */
(function(){
var root = document.documentElement;
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return;
root.classList.add('sc-js');
var revealSel = '[data-sc-reveal], .sc-schematic';
var items = [].slice.call(document.querySelectorAll(revealSel));
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { e.target.classList.add('sc-in'); io.unobserve(e.target); }
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}
var counters = [].slice.call(document.querySelectorAll('[data-sc-count]'));
function animate(el){
var raw = String(el.getAttribute('data-sc-count') || '');
var m = raw.match(/-?[0-9]+(\.[0-9]+)?/);
if (!m) return;
var target = parseFloat(m[0]);
var decimals = (m[0].split('.')[1] || '').length;
var prefix = raw.slice(0, m.index);
var suffix = raw.slice(m.index + m[0].length);
var start = null, dur = 1100;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = prefix + (target * eased).toFixed(decimals) + suffix;
if (p < 1) requestAnimationFrame(step);
else el.textContent = raw;
}
requestAnimationFrame(step);
}
if ('IntersectionObserver' in window) {
var co = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { animate(e.target); co.unobserve(e.target); }
});
}, { threshold: 0.4 });
counters.forEach(function(el){ co.observe(el); });
} else {
counters.forEach(animate);
}
})();
</script>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,13 +1,70 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
<html lang="en">
{{ head_html|safe }}
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
<body class="sc-body" style="margin: 0; min-height: 100vh; display: flex; flex-direction: column; background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full bg-grid">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-5xl mx-auto py-20 px-4 text-center scifi-mono uppercase tracking-widest text-xs text-muted-foreground">No content blocks assigned to this page.</div>{% endif %}
<header class="sc-hairline-bottom" style="width: 100%;">
<div class="sc-content-well">{{ slots.header|safe }}</div>
</header>
<main style="flex: 1 1 auto; width: 100%;" data-content-well>
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="sc-content-well" style="padding-top: 4rem; padding-bottom: 4rem;"><p class="sc-stat-label">No content blocks assigned to this page.</p></div>{% endif %}
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
<footer style="width: 100%; margin-top: auto;">{{ slots.footer|safe }}</footer>
<script>
/* Sci-Fi Clean MID motion: scroll-reveal, line-draw schematics, ticking
counters. Dependency-free. prefers-reduced-motion => everything static
and counters keep their final text. No JS => nothing runs, all visible. */
(function(){
var root = document.documentElement;
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return;
root.classList.add('sc-js');
var revealSel = '[data-sc-reveal], .sc-schematic';
var items = [].slice.call(document.querySelectorAll(revealSel));
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { e.target.classList.add('sc-in'); io.unobserve(e.target); }
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}
var counters = [].slice.call(document.querySelectorAll('[data-sc-count]'));
function animate(el){
var raw = String(el.getAttribute('data-sc-count') || '');
var m = raw.match(/-?[0-9]+(\.[0-9]+)?/);
if (!m) return;
var target = parseFloat(m[0]);
var decimals = (m[0].split('.')[1] || '').length;
var prefix = raw.slice(0, m.index);
var suffix = raw.slice(m.index + m[0].length);
var start = null, dur = 1100;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = prefix + (target * eased).toFixed(decimals) + suffix;
if (p < 1) requestAnimationFrame(step);
else el.textContent = raw;
}
requestAnimationFrame(step);
}
if ('IntersectionObserver' in window) {
var co = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { animate(e.target); co.unobserve(e.target); }
});
}, { threshold: 0.4 });
counters.forEach(function(el){ co.observe(el); });
} else {
counters.forEach(animate);
}
})();
</script>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,15 +1,72 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
<html lang="en">
{{ head_html|safe }}
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
<body class="sc-body" style="margin: 0; min-height: 100vh; display: flex; flex-direction: column; background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<section class="w-full">{{ slots.hero|safe }}</section>
{% if slots.specs %}<section class="w-full hairline-b py-12"><div class="max-w-6xl mx-auto px-4"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">{{ slots.specs|safe }}</div></div></section>{% endif %}
<main class="flex-grow w-full">
{% if slots.main %}<div class="max-w-6xl mx-auto px-4 py-16">{{ slots.main|safe }}</div>{% endif %}
<header class="sc-hairline-bottom" style="width: 100%;">
<div class="sc-content-well">{{ slots.header|safe }}</div>
</header>
<section style="width: 100%;">{{ slots.hero|safe }}</section>
<main style="flex: 1 1 auto;" data-content-well>
{% if slots.main %}<div class="sc-content-well"><div class="sc-swiss-12" data-grid="swiss-12" style="padding-top: 3rem; padding-bottom: 3rem;"><div style="grid-column: span 12 / span 12;">{{ slots.main|safe }}</div></div></div>{% endif %}
</main>
{% if slots.cta %}<section class="w-full hairline-t py-16"><div class="max-w-6xl mx-auto px-4">{{ slots.cta|safe }}</div></section>{% endif %}
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
<section style="width: 100%;">{{ slots.cta|safe }}</section>
<footer style="width: 100%; margin-top: auto;">{{ slots.footer|safe }}</footer>
<script>
/* Sci-Fi Clean MID motion: scroll-reveal, line-draw schematics, ticking
counters. Dependency-free. prefers-reduced-motion => everything static
and counters keep their final text. No JS => nothing runs, all visible. */
(function(){
var root = document.documentElement;
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return;
root.classList.add('sc-js');
var revealSel = '[data-sc-reveal], .sc-schematic';
var items = [].slice.call(document.querySelectorAll(revealSel));
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { e.target.classList.add('sc-in'); io.unobserve(e.target); }
});
}, { rootMargin: '0px 0px -8% 0px' });
items.forEach(function(el){ io.observe(el); });
/* Safety net: guarantee full reveal for no-scroll captures / IO gaps. */
setTimeout(function(){ items.forEach(function(el){ el.classList.add("sc-in"); }); }, 1600);
} else {
items.forEach(function(el){ el.classList.add('sc-in'); });
}
var counters = [].slice.call(document.querySelectorAll('[data-sc-count]'));
function animate(el){
var raw = String(el.getAttribute('data-sc-count') || '');
var m = raw.match(/-?[0-9]+(\.[0-9]+)?/);
if (!m) return;
var target = parseFloat(m[0]);
var decimals = (m[0].split('.')[1] || '').length;
var prefix = raw.slice(0, m.index);
var suffix = raw.slice(m.index + m[0].length);
var start = null, dur = 1100;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = prefix + (target * eased).toFixed(decimals) + suffix;
if (p < 1) requestAnimationFrame(step);
else el.textContent = raw;
}
requestAnimationFrame(step);
}
if ('IntersectionObserver' in window) {
var co = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if (e.isIntersecting) { animate(e.target); co.unobserve(e.target); }
});
}, { threshold: 0.4 });
counters.forEach(function(el){ co.observe(el); });
} else {
counters.forEach(animate);
}
})();
</script>
{{ body_end_html|safe }}
</body>
</html>