Compare commits
No commits in common. "6af6950f23e0767f2963952cc3ad6f240ce47214" and "dc4cc2d8b3cac77a02dd42bf749704c1b71d2214" have entirely different histories.
6af6950f23
...
dc4cc2d8b3
218
BUILD_REPORT.md
218
BUILD_REPORT.md
@ -1,52 +1,182 @@
|
||||
# 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).
|
||||
# Sci-Fi Clean — Build Report (v0.1.0, wave-1 pass)
|
||||
|
||||
## What landed
|
||||
|
||||
- `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).
|
||||
### 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()`.
|
||||
|
||||
## Verification
|
||||
### 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.
|
||||
|
||||
- `make build` -> exit 0 (codeless `.bnp`, 4 blocks, no wasm).
|
||||
- `check-safety` -> exit 0 (32 checks: 19 ok, 13 skip).
|
||||
### Master pages
|
||||
`DefaultMasterPages()` returns two entries:
|
||||
|
||||
## Known platform limits (per OVERRIDE-CONTRACT §11/§12)
|
||||
- `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.
|
||||
|
||||
- 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.
|
||||
### 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.
|
||||
|
||||
Visual quality is UNVERIFIED until Wave B screenshots.
|
||||
### 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.
|
||||
|
||||
24
README.md
24
README.md
@ -1,24 +0,0 @@
|
||||
# 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.
|
||||
@ -1,32 +1,33 @@
|
||||
# Recommended fonts for Sci-Fi Clean
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
| 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. |
|
||||
Open **Admin → Settings → Typography**, switch to the **Google Fonts** tab, and assign:
|
||||
|
||||
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>)`.
|
||||
| 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.
|
||||
|
||||
## CSS fallback stacks
|
||||
|
||||
Until (or unless) the admin reassigns a slot, templates fall through to these
|
||||
CSS-variable fallbacks (declared on `:root` in the theme CSS manifest):
|
||||
Until the admin assigns fonts, templates fall through to these CSS-variable fallbacks
|
||||
(declared in the theme's CSS manifest, applied to `: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`
|
||||
- `--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`
|
||||
|
||||
## Good alternates
|
||||
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.
|
||||
|
||||
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.
|
||||
## 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.
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
# 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).
|
||||
@ -1,15 +0,0 @@
|
||||
# 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.
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1,44 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,32 +1,42 @@
|
||||
# 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/.
|
||||
# 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.
|
||||
blocks:
|
||||
- key: scifi-clean:tech_spec
|
||||
title: Tech Spec Table
|
||||
description: Specification sheet with right-aligned mono numerals, units, and hairline rows.
|
||||
description: Specification table with right-aligned mono numerals and hairline rows.
|
||||
category: content
|
||||
schema: tech_spec.schema.json
|
||||
template: tech_spec.ninjatpl
|
||||
- key: scifi-clean:benchmark_table
|
||||
title: Benchmark Table
|
||||
description: Labelled benchmark bars with mono values and a technical-blue fill track.
|
||||
- key: scifi-clean:diagram_caption
|
||||
title: Diagram Caption
|
||||
description: Annotated technical figure with a Fig. NN. heading and optional callout pins.
|
||||
category: content
|
||||
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.
|
||||
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.
|
||||
category: content
|
||||
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
|
||||
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
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1,43 +0,0 @@
|
||||
{
|
||||
"$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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
blocks/diagram_caption.ninjatpl
Normal file
9
blocks/diagram_caption.ninjatpl
Normal file
@ -0,0 +1,9 @@
|
||||
<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>
|
||||
67
blocks/diagram_caption.schema.json
Normal file
67
blocks/diagram_caption.schema.json
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"$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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
blocks/footer.ninjatpl
Normal file
24
blocks/footer.ninjatpl
Normal file
@ -0,0 +1,24 @@
|
||||
<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>
|
||||
46
blocks/footer.schema.json
Normal file
46
blocks/footer.schema.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"$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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
blocks/mission_stat.ninjatpl
Normal file
8
blocks/mission_stat.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
||||
<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>
|
||||
40
blocks/mission_stat.schema.json
Normal file
40
blocks/mission_stat.schema.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
blocks/schematic_hero.ninjatpl
Normal file
13
blocks/schematic_hero.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
<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>
|
||||
62
blocks/schematic_hero.schema.json
Normal file
62
blocks/schematic_hero.schema.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
blocks/status_bar.ninjatpl
Normal file
27
blocks/status_bar.ninjatpl
Normal file
@ -0,0 +1,27 @@
|
||||
<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>
|
||||
28
blocks/status_bar.schema.json
Normal file
28
blocks/status_bar.schema.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +1,10 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1,45 +0,0 @@
|
||||
{
|
||||
"$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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
fonts.json
27
fonts.json
@ -1,26 +1 @@
|
||||
[
|
||||
{
|
||||
"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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
[]
|
||||
|
||||
543
manifest.yaml
543
manifest.yaml
@ -1,120 +1,40 @@
|
||||
# Sci-Fi Clean theme — codeless .bnp manifest (WO-TF-024, Wave A).
|
||||
# Sci-Fi Clean theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
|
||||
# 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: "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."
|
||||
description: "Minimalist futurist theme for aerospace, robotics, and R&D — off-white surfaces, technical blue, signal-orange accents, mono numerals."
|
||||
|
||||
page_templates:
|
||||
- system: scifi-clean
|
||||
key: default
|
||||
title: Default
|
||||
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."
|
||||
description: "Standard cleanroom layout with header rail, main column, and footer instrument panel."
|
||||
slots: [header, main, footer]
|
||||
- system: scifi-clean
|
||||
key: landing
|
||||
title: Landing
|
||||
description: "Hero band + main + closing CTA + footer."
|
||||
slots: [hero, main, cta, footer]
|
||||
description: "Hero diagram + spec strip + CTA banner."
|
||||
slots: [hero, specs, main, cta, footer]
|
||||
- system: scifi-clean
|
||||
key: article
|
||||
title: Article
|
||||
description: "Narrow measure for long-form technical writing, with a rail aside."
|
||||
slots: [header, main, aside, footer]
|
||||
description: "Narrow column with rail metadata and figure margin."
|
||||
slots: [header, rail, main, footer]
|
||||
- system: scifi-clean
|
||||
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]
|
||||
key: full-width
|
||||
title: Full Width
|
||||
description: "Edge-to-edge schematic surface for diagrams and dashboards."
|
||||
slots: [header, 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
|
||||
@ -122,378 +42,99 @@ email_wrappers:
|
||||
css:
|
||||
input_css_append: |
|
||||
/* === Sci-Fi Clean: font-family fallback variables ===
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
: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.
|
||||
============================================================ */
|
||||
|
||||
/* --- 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; }
|
||||
/* === 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));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
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-t {
|
||||
border-top: 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); }
|
||||
.hairline-b {
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
/* --- 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)); }
|
||||
|
||||
/* --- 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; }
|
||||
|
||||
/* --- 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; }
|
||||
|
||||
/* --- 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)); }
|
||||
|
||||
/* --- 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)); }
|
||||
|
||||
/* --- 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)); }
|
||||
|
||||
/* --- Chrome: footer top rule = HUD blue --- */
|
||||
.sc-footer { border-top: 2px solid hsl(var(--primary)); }
|
||||
|
||||
/* --- Blueprint grid ground (used by page-template mains) --- */
|
||||
.sc-blueprint {
|
||||
.bg-grid {
|
||||
background-color: hsl(var(--background));
|
||||
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;
|
||||
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;
|
||||
}
|
||||
.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)); }
|
||||
|
||||
/* --- 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; }
|
||||
|
||||
.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)); }
|
||||
|
||||
.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; }
|
||||
.mono-numerals {
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.tabular-nums {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.scrim {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scrim::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: hsl(var(--background) / 0.65);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scrim > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* === 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;
|
||||
}
|
||||
|
||||
/* === 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;
|
||||
}
|
||||
|
||||
/* === 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;
|
||||
}
|
||||
|
||||
.scifi-body {
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.scifi-mono {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
{
|
||||
"key": "scifi-clean:default-master",
|
||||
"title": "Sci-Fi Clean Default Master",
|
||||
"page_templates": ["default", "article", "blog-index", "full-width"],
|
||||
"page_templates": ["default", "article", "full-width"],
|
||||
"blocks": [
|
||||
{ "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 }
|
||||
{ "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 }
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -14,19 +15,11 @@
|
||||
"title": "Sci-Fi Clean Landing Master",
|
||||
"page_templates": ["landing"],
|
||||
"blocks": [
|
||||
{ "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 }
|
||||
{ "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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
name = "scifi-clean"
|
||||
display_name = "Sci-Fi Clean"
|
||||
scope = "@themes"
|
||||
version = "0.2.1"
|
||||
version = "0.2.0"
|
||||
description = "Minimalist futurist theme for aerospace, robotics, and R&D — off-white surfaces, technical blue, signal-orange accents, mono numerals."
|
||||
kind = "theme"
|
||||
categories = ["templates"]
|
||||
|
||||
126
presets.json
126
presets.json
@ -2,9 +2,9 @@
|
||||
{
|
||||
"id": "flightline",
|
||||
"name": "Flightline",
|
||||
"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.",
|
||||
"description": "Off-white paper with technical blue primary and signal-orange accent — the default Sci-Fi Clean look.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"mode": "light",
|
||||
"lightColors": {
|
||||
"background": "210 14% 97%",
|
||||
"foreground": "215 30% 12%",
|
||||
@ -25,7 +25,15 @@
|
||||
"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%",
|
||||
@ -33,7 +41,7 @@
|
||||
"cardForeground": "210 16% 92%",
|
||||
"popover": "220 16% 10%",
|
||||
"popoverForeground": "210 16% 92%",
|
||||
"primary": "210 100% 62%",
|
||||
"primary": "210 100% 60%",
|
||||
"primaryForeground": "220 16% 7%",
|
||||
"secondary": "220 14% 16%",
|
||||
"secondaryForeground": "210 16% 92%",
|
||||
@ -45,14 +53,14 @@
|
||||
"destructiveForeground": "0 0% 100%",
|
||||
"border": "220 14% 20%",
|
||||
"input": "220 14% 18%",
|
||||
"ring": "210 100% 62%"
|
||||
"ring": "210 100% 60%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cleanroom",
|
||||
"name": "Cleanroom",
|
||||
"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.",
|
||||
"description": "Highest-contrast surgical white with deep navy ink — sister dark mode for night-lab work.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"lightColors": {
|
||||
@ -62,7 +70,7 @@
|
||||
"cardForeground": "220 40% 10%",
|
||||
"popover": "0 0% 100%",
|
||||
"popoverForeground": "220 40% 10%",
|
||||
"primary": "220 85% 38%",
|
||||
"primary": "220 85% 35%",
|
||||
"primaryForeground": "0 0% 100%",
|
||||
"secondary": "210 18% 94%",
|
||||
"secondaryForeground": "220 40% 10%",
|
||||
@ -74,7 +82,7 @@
|
||||
"destructiveForeground": "0 0% 100%",
|
||||
"border": "215 16% 88%",
|
||||
"input": "215 16% 90%",
|
||||
"ring": "220 85% 38%"
|
||||
"ring": "220 85% 35%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "220 30% 5%",
|
||||
@ -83,7 +91,7 @@
|
||||
"cardForeground": "210 20% 96%",
|
||||
"popover": "220 26% 9%",
|
||||
"popoverForeground": "210 20% 96%",
|
||||
"primary": "210 100% 66%",
|
||||
"primary": "210 100% 65%",
|
||||
"primaryForeground": "220 30% 5%",
|
||||
"secondary": "220 20% 16%",
|
||||
"secondaryForeground": "210 20% 96%",
|
||||
@ -95,107 +103,7 @@
|
||||
"destructiveForeground": "0 0% 100%",
|
||||
"border": "220 18% 20%",
|
||||
"input": "220 18% 18%",
|
||||
"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%"
|
||||
"ring": "210 100% 65%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
121
seed/seed.json
121
seed/seed.json
@ -1,121 +0,0 @@
|
||||
{
|
||||
"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 }
|
||||
]
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,56 +1,53 @@
|
||||
<!doctype html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<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">
|
||||
: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 {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
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; }
|
||||
img { border: 0; height: auto; 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; }
|
||||
/* 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) {
|
||||
.sc-email-container { width: 100% !important; max-width: 100% !important; }
|
||||
.sc-email-padding { padding-left: 24px !important; padding-right: 24px !important; }
|
||||
.scifi-email-container { width: 100% !important; max-width: 100% !important; }
|
||||
.scifi-pad { padding-left: 20px !important; padding-right: 20px !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<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 %}
|
||||
<body style="background-color: {{ colors.background|default:"#F6F7F8" }}; margin: 0; padding: 0; font-family: &#39;Inter&#39;, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, sans-serif; color: {{ colors.foreground|default:"#16202E" }};">
|
||||
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden;">{{ 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: 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 -->
|
||||
<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" }};">
|
||||
<tr>
|
||||
<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>
|
||||
<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: &#39;Space Grotesk&#39;, &#39;Inter&#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, &#39;JetBrains Mono&#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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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;"> </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;"> </div></td>
|
||||
<td class="scifi-pad" style="padding: 28px; font-size: 15px; line-height: 1.55; color: {{ colors.foreground|default:"#16202E" }};">{{ body|safe }}</td>
|
||||
</tr>
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<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 -->
|
||||
<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">
|
||||
<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 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>
|
||||
<td class="scifi-num" align="left" style="font-family: ui-monospace, &#39;JetBrains Mono&#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, &#39;JetBrains Mono&#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 %}
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,14 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,22 +0,0 @@
|
||||
<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>
|
||||
@ -1 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1,6 +1 @@
|
||||
{% 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>
|
||||
{% 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 %}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1,14 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,34 +0,0 @@
|
||||
{# 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 %}
|
||||
@ -1,44 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,8 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,2 +0,0 @@
|
||||
{# 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 %}
|
||||
@ -1,31 +0,0 @@
|
||||
{# 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 %} – {{ 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>
|
||||
@ -1,6 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,7 +0,0 @@
|
||||
{# 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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1,17 +0,0 @@
|
||||
{# 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 %}© {{ copyright }}{% else %}© {{ 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>
|
||||
@ -1,6 +0,0 @@
|
||||
{# 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>
|
||||
@ -1 +1 @@
|
||||
{% 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 %}
|
||||
<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>
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
{# 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 %}
|
||||
@ -1,18 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,5 +0,0 @@
|
||||
{# 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 %}
|
||||
@ -1,5 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,21 +0,0 @@
|
||||
{# 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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1,11 +0,0 @@
|
||||
<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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1,14 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,6 +0,0 @@
|
||||
{# 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>
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
@ -1,10 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,2 +0,0 @@
|
||||
{# 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 %}
|
||||
@ -1,3 +0,0 @@
|
||||
<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>
|
||||
@ -1,7 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,6 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,12 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,9 +0,0 @@
|
||||
{# 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>
|
||||
@ -1 +1 @@
|
||||
<div class="sc-text {{ class }}">{{ text|safe }}</div>
|
||||
<div class="scifi-body prose max-w-none text-foreground tabular-nums {{ class }}" style="font-family: var(--font-body);">{{ text|safe }}</div>
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,9 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,11 +0,0 @@
|
||||
{# 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>
|
||||
@ -1,75 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ 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));">
|
||||
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
|
||||
{{ admin_banner_html|safe }}
|
||||
<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 %}
|
||||
<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 %}
|
||||
</article>
|
||||
</div>
|
||||
{% if slots.aside %}<aside class="sc-hairline-top" style="margin-top: 3rem; padding-top: 2rem;">{{ slots.aside|safe }}</aside>{% endif %}
|
||||
</main>
|
||||
<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>
|
||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
<!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>
|
||||
@ -1,72 +0,0 @@
|
||||
<!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>
|
||||
@ -1,78 +0,0 @@
|
||||
<!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>
|
||||
@ -1,74 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ 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));">
|
||||
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
|
||||
{{ admin_banner_html|safe }}
|
||||
<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>
|
||||
<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 %}
|
||||
</main>
|
||||
<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>
|
||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,70 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ 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));">
|
||||
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
|
||||
{{ admin_banner_html|safe }}
|
||||
<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 %}
|
||||
<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 %}
|
||||
</main>
|
||||
<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>
|
||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,72 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ 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));">
|
||||
<body class="scifi-body bg-background text-foreground antialiased min-h-screen flex flex-col" style="font-family: var(--font-body);">
|
||||
{{ admin_banner_html|safe }}
|
||||
<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 %}
|
||||
<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 %}
|
||||
</main>
|
||||
<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>
|
||||
{% 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>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user