feat(homepage): Australis Skyfall theme + Arbo-generated aurora background

Replaces the previous theme attempt, which was built on a misread: the ask was
to use Arbo as an IMAGE-GEN ENGINE for the background, with the operator's
Australis Skyfall design system supplying the palette.

theme/ holds the source — colors/layout/typography vendored verbatim from the
Skyfall handoff bundle, Supreme 400/500/700 woff2, the Homepage bindings in
skyfall.css.in, and build.py which inlines fonts + tokens into
conf/custom.css. custom.css is GENERATED; edit the .in file and rebuild.

The build exists because Homepage serves only custom.css and custom.js out of
its config dir, so a @font-face pointing at a vendored woff2 would 404 — the
face has to arrive as a data: URI. The background image takes the other route:
/app/public/images is a real static route, so compose.yaml now mounts
images/ there read-only and settings.yaml points at /images/.

Bindings map Skyfall's semantic layer onto Homepage's DOM: Sea surfaces, the
depth recipe (hairline AND two-layer shadow, never one alone), uppercase
eyebrow group headers, the sanctioned accent-rail on the active tab rather
than a glow, and semantic status colour so a green pill means the service is
actually serving.

Background generated by Arbo (irv-ml1:8201) workflow t2i-ui-background, job
13f0891f4e42, seed 26, flux2-klein-9b, 2048x1152 — abstract, no subject,
cool-temperature aurora. 1.6 MB PNG -> 22 KB WebP.

Two deviations are documented rather than hidden: Skyfall forbids imagery
behind body text (held at opacity 30 as mitigation), and service icons stay
full-colour vendor logos.

NOT DEPLOYED — live still runs the old theme. Prototype on :5199.
This commit is contained in:
2026-08-18 23:30:27 -07:00
parent c3de7dbd58
commit 45c1995d7a
13 changed files with 1253 additions and 119 deletions
+69 -13
View File
@@ -146,22 +146,78 @@ render are different questions.
Before/after evidence: `~/booth-data/homepage-cleanup/` on nh3-dev →
`http://10.100.10.50:8090/b/homepage-cleanup/` (24h TTL).
## Theme — the Arbo "Raven" palette (`conf/custom.css`)
## Theme — Australis Skyfall
`conf/custom.css` carries a port of Arbo's design tokens (irv-ml1:8201): flat
raven ink `#021425`, card surface `#112333` on `#1B2E3D` borders, Manrope, and
mint `#2FFC89` reserved for signal — a status pill is green only when the
service is actually serving. Values were read off Arbo's running `:root`
custom properties, not sampled from a screenshot, so drift checks are a diff.
The dashboard is themed with **Australis Skyfall**, the operator-supplied
design system (handoff bundle, 2026-08-18). Skyfall is a dual-theme web system
built on the OKLCH rebuild of the Australis terminal palette: one lightness law
across every chromatic family, all hues cooler than neutral, a Sea neutral ramp
that drifts from ice-blue to ocean green as it brightens, and a "calm depth"
language of hairline + two-layer shadow on every elevated surface.
It lives in CSS rather than `settings.yaml` because Homepage's `color:` setting
only accepts built-in Tailwind ramps and has no separate surface/border/accent
knobs. `color: slate` stays as the base ramp. The `!important` flags are
load-bearing against Tailwind utility specificity.
### `theme/` is the source; `conf/custom.css` is generated
Pairs with removing the `background:` block from `settings.yaml` — Arbo has no
background image, and the wallpaper fought the card surfaces, which sit only a
little lighter than the page.
```
theme/colors.css vendored VERBATIM from the bundle
theme/layout.css ditto — radii, the shadow/glow/rail system, motion
theme/typography.css ditto — families, scale, weights, tracking
theme/fonts/ Supreme 400/500/700 woff2 (Fontshare ITF license)
theme/skyfall.css.in the Homepage bindings — the only part that is ours
theme/build.py inlines the fonts + tokens -> conf/custom.css
```
**Do not hand-edit `conf/custom.css`.** Change `skyfall.css.in`, run
`python3 stacks/homepage/theme/build.py`, then deploy.
The build exists for one reason: Homepage serves exactly two files out of its
config directory, `custom.css` and `custom.js`. There is no static route for
anything beside them, so a `@font-face` pointing at a vendored `.woff2` would
404 — the face has to arrive inside the stylesheet as a data: URI. That is also
why the background image is mounted at `/app/public/images` instead (see
`compose.yaml`), which *is* a real static route.
Only Supreme (body/UI) is embedded. A link dashboard has no display type, so
Bespoke Sans buys nothing; Victor Mono ships as 2.4 MB TTF statics per cut,
which is 30x the whole stylesheet for the handful of latency figures on the
cards. Both are named in the font stacks so they resolve if vendored later.
### The background is generated, not stock
`images/skyfall-aurora.webp` came out of **Arbo** (irv-ml1:8201) via its
`t2i-ui-background` workflow — "abstract full-bleed backgrounds, no subject".
Job `13f0891f4e42`, seed 26, flux2-klein-9b, 2048x1152, 1.6 MB PNG re-encoded
to a 22 KB WebP (smooth gradients compress absurdly well).
Regenerate with a different seed, or reword, via:
```bash
curl -s -X POST http://10.100.79.3:8201/workflows/t2i-ui-background/run \
-H 'content-type: application/json' \
-d '{"prompt":{"kind":"raw","text":"...","negative":"..."},
"width":2048,"height":1152,"seed":26}'
# poll GET /jobs/<id>, then GET /artifacts/<id>/0
```
Note the prompt field is a discriminated union, not a string — a bare string
gets a 422. `{"kind":"raw","text":...}` is the shape.
### Two deviations from the system, on the record
1. **Skyfall forbids this background.** Its rule is "flat semantic surfaces; no
photography, no textures", with exactly one permitted decorative motif — a
subtle aurora gradient, on hero and empty-state areas only, *"never behind
body text blocks"*. A dashboard is a body-text block. The background is here
on the operator's explicit instruction to generate one with Arbo, and it is
mitigated rather than excused: abstract with no subject, strictly cool
temperature per the imagery rule, and held at `opacity: 14` so card contrast
never pays for it. **That opacity is load-bearing, not decoration** — the
render is brighter than the "mostly empty negative space" brief, and turning
it up is what would make the cards fight the ribbon. If it ever reads busy,
the system-faithful answer is to drop the image and let `--surface-sunken`
be the canvas.
2. **Service icons stay full-colour vendor logos.** Skyfall wants one cool,
technical icon family. Desaturating every logo from CSS would just make them
illegible, so this one is simply not fixed.
## Open question
+7
View File
@@ -9,6 +9,13 @@ services:
- 5100:3000
volumes:
- /opt/docker/conf/homepage:/app/config # Make sure your local config directory exists
# Background art. Homepage serves ONLY custom.css and custom.js out of
# /app/config — there is no static route for anything else in there — so
# an image referenced from the config dir 404s. /app/public/images is the
# documented drop point; files here are reachable as /images/<name>, which
# is what settings.yaml's `background:` points at. Read-only: nothing in
# the container should ever write to it.
- /opt/docker/compose/homepage/images:/app/public/images:ro
- /var/run/docker.sock:/var/run/docker.sock:ro # optional, for docker integrations
restart: unless-stopped
networks:
File diff suppressed because one or more lines are too long
+23 -2
View File
@@ -2,11 +2,32 @@
# https://gethomepage.dev/latest/configs/settings
title: ... all my base ...
theme: dark
# `color:` only accepts Homepage's built-in Tailwind ramps, and none of them is
# the Skyfall Sea ramp. slate is the nearest cool neutral and acts as the base
# under conf/custom.css, which is where the real palette lives.
color: slate
# Background — generated locally, not stock art. Arbo (irv-ml1:8201) rendered
# it from the `t2i-ui-background` workflow: abstract full-bleed, no subject,
# aurora blue->cyan->green on near-black, which is the one decorative motif
# Australis Skyfall permits and its "cool temperature always" imagery rule.
# Job 13f0891f4e42, seed 26, flux2-klein-9b, 2048x1152.
#
# Served from /app/public/images via the read-only mount in compose.yaml —
# NOT from the config dir, which Homepage will not serve static files out of.
#
# ⚠️ opacity is the load-bearing number, not decoration. Skyfall's rule is that
# the aurora motif never sits behind body text; this dashboard breaks that on
# purpose, so the image has to stay far enough back that card contrast never
# suffers. The render is considerably brighter than the "mostly empty" brief,
# which is exactly what this dials out. Raise it and the cards start fighting
# the ribbon.
background:
image: https://w.wallhaven.cc/full/p9/wallhaven-p9gr2p.jpg
image: /images/skyfall-aurora.webp
blur: sm
opacity: 20
saturate: 90
brightness: 90
opacity: 30
headerStyle: boxedWidgets
providers:
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Generate conf/custom.css from the Skyfall source in this directory.
Homepage serves exactly two files out of its config dir — custom.css and
custom.js — with no static route for anything alongside them. A @font-face
pointing at a vendored .woff2 would therefore 404, so the faces have to be
inlined as data: URIs. That is the whole reason this build step exists.
Usage: python3 stacks/homepage/theme/build.py
Then: scripts/deploy-stack.sh esh-docker-vm homepage --conf
"""
import base64
import pathlib
import re
import sys
HERE = pathlib.Path(__file__).resolve().parent
OUT = HERE.parent / "conf" / "custom.css"
# Only the body/UI face is embedded. See the header comment in skyfall.css.in
# for why Bespoke Sans and Victor Mono are deliberately left out.
FONTS = [
("Supreme", "Supreme-400.woff2", "400"),
("Supreme", "Supreme-500.woff2", "500"),
("Supreme", "Supreme-700.woff2", "600 700"), # Supreme ships no 600 cut
]
# Vendored verbatim from the handoff bundle; order matters (colors defines the
# families that layout and typography reference).
TOKEN_FILES = ["colors.css", "layout.css", "typography.css"]
def font_faces() -> str:
out = []
for family, filename, weight in FONTS:
path = HERE / "fonts" / filename
if not path.exists():
sys.exit(f"missing font: {path}")
b64 = base64.b64encode(path.read_bytes()).decode("ascii")
out.append(
f'@font-face {{\n'
f' font-family: "{family}";\n'
f" src: url(data:font/woff2;base64,{b64}) format('woff2');\n"
f" font-weight: {weight};\n"
f" font-style: normal;\n"
f" font-display: swap;\n"
f"}}"
)
return "\n".join(out)
def tokens() -> str:
out = []
for name in TOKEN_FILES:
path = HERE / name
if not path.exists():
sys.exit(f"missing token file: {path}")
text = path.read_text()
# The bundle's fonts.css is not vendored, so typography.css's family
# stacks would reference faces that never load. Nothing to strip here
# today, but keep the read verbatim so drift is a plain diff.
out.append(f"/* ---- {name} (vendored verbatim) ---- */\n{text.strip()}")
return "\n\n".join(out)
def main() -> None:
src = (HERE / "skyfall.css.in").read_text()
if "@@FONTS@@" not in src or "@@TOKENS@@" not in src:
sys.exit("skyfall.css.in lost one of its placeholders")
css = src.replace("/* @@FONTS@@ */", font_faces())
css = css.replace("/* @@TOKENS@@ */", tokens())
# Guard the one rule that silently breaks the theme if it regresses.
if not re.search(r"--surface-sunken", css):
sys.exit("generated css has no surface tokens — token vendoring failed")
OUT.write_text(css)
print(f"wrote {OUT} ({len(css) / 1024:.0f} KB)")
if __name__ == "__main__":
main()
+208
View File
@@ -0,0 +1,208 @@
/* ============================================================
Australis Skyfall — Color Tokens v2
A ground-up web palette derived from the Australis Dark
PHILOSOPHY (not its ANSI values). Built in OKLCH so the laws
are explicit and every family stays perceptually consistent:
- All hues are cooler than neutral; nothing warmer than amber.
- One lightness law across every chromatic family:
deep L 0.48 — light-theme fills & colored text
base L 0.66 — dark-theme fills (the working color)
bright L 0.80 — dark-theme colored text & highlights
- Chroma is tuned per hue so no family shouts over another.
- Neutrals (Sea ramp) drift in hue from ice-blue (265°) toward
ocean green (195°) as they brighten — the aurora signature.
- Medium / medium-high contrast for eye comfort.
Families: Aurora (blue, cyan, green — use generously, in that
order) and Dawn (amber, red, violet — semantic accents only:
warning, danger, AI).
============================================================ */
:root {
/* ---- Sea — neutral ramp (hue 265° → 195° as L rises) ---- */
--sea-10: oklch(0.23 0.02 265);
--sea-15: oklch(0.27 0.024 268); /* dark canvas — the Australis anchor */
--sea-20: oklch(0.31 0.022 262);
--sea-25: oklch(0.35 0.02 258);
--sea-30: oklch(0.4 0.018 252);
--sea-40: oklch(0.48 0.016 245);
--sea-50: oklch(0.56 0.015 235);
--sea-60: oklch(0.64 0.014 228);
--sea-70: oklch(0.72 0.014 220);
--sea-75: oklch(0.78 0.015 212);
--sea-80: oklch(0.83 0.017 205);
--sea-90: oklch(0.9 0.02 198);
--sea-94: oklch(0.94 0.014 195);
--sea-96: oklch(0.96 0.01 195);
--sea-98: oklch(0.98 0.006 195);
/* ---- Ice — main colors (aliases into the ramp) ---- */
--ice-black: var(--sea-15);
--ice-white: var(--sea-75);
--ice-bright-white: var(--sea-90);
/* ---- Aurora — primary families (blue > cyan > green) ---- */
--blue-deep: oklch(0.48 0.12 262);
--blue-base: oklch(0.66 0.12 262);
--blue-bright: oklch(0.8 0.09 262);
--cyan-deep: oklch(0.48 0.08 200);
--cyan-base: oklch(0.66 0.1 200);
--cyan-bright: oklch(0.8 0.1 200);
--green-deep: oklch(0.48 0.11 158);
--green-base: oklch(0.66 0.13 158);
--green-bright: oklch(0.8 0.13 158);
/* ---- Dawn — accent families (semantic use only) ---- */
--amber-deep: oklch(0.52 0.1 75);
--amber-base: oklch(0.7 0.12 78);
--amber-bright: oklch(0.82 0.13 82);
--red-deep: oklch(0.5 0.15 25);
--red-base: oklch(0.62 0.16 25);
--red-bright: oklch(0.78 0.11 28);
--violet-deep: oklch(0.5 0.15 292);
--violet-base: oklch(0.66 0.14 292);
--violet-bright: oklch(0.8 0.1 292);
}
/* ============================================================
Semantic aliases — DARK (default, first-class)
============================================================ */
:root {
color-scheme: dark;
/* Surfaces */
--surface-sunken: var(--sea-10);
--surface-base: var(--sea-15);
--surface-raised: var(--sea-20);
--surface-overlay: oklch(0.34 0.022 260);
--surface-card: var(--sea-20);
--surface-input: var(--sea-10);
--surface-scrim: oklch(0.17 0.02 265 / 0.72);
/* Text */
--text-heading: var(--sea-90);
--text-body: var(--sea-75);
--text-muted: var(--sea-60);
--text-faint: var(--sea-50);
--text-inverse: var(--sea-15);
--text-link: var(--blue-bright);
--text-link-hover: var(--sea-90);
/* Borders */
--border-subtle: var(--sea-25);
--border-default: var(--sea-30);
--border-strong: var(--sea-40);
--border-focus: var(--blue-base);
/* Accent (primary = aurora blue) */
--accent: var(--blue-base);
--accent-hover: oklch(0.71 0.11 262);
--accent-active: oklch(0.6 0.13 262);
--accent-bright: var(--blue-bright);
--accent-text: var(--blue-bright);
--accent-contrast: var(--sea-10);
--accent-soft: color-mix(in oklab, var(--blue-base) 16%, transparent);
--accent-soft-hover: color-mix(in oklab, var(--blue-base) 26%, transparent);
/* Secondary accent (aurora cyan) */
--secondary: var(--cyan-base);
--secondary-bright: var(--cyan-bright);
--secondary-text: var(--cyan-bright);
--secondary-soft: color-mix(in oklab, var(--cyan-base) 14%, transparent);
/* Semantic status */
--success: var(--green-base);
--success-text: var(--green-bright);
--success-soft: color-mix(in oklab, var(--green-base) 14%, transparent);
--warning: var(--amber-base);
--warning-text: var(--amber-bright);
--warning-soft: color-mix(in oklab, var(--amber-base) 13%, transparent);
--danger: var(--red-base);
--danger-hover: oklch(0.67 0.15 25);
--danger-text: var(--red-bright);
--danger-contrast: var(--sea-10);
--danger-soft: color-mix(in oklab, var(--red-base) 13%, transparent);
--info: var(--blue-base);
--info-text: var(--blue-bright);
--info-soft: color-mix(in oklab, var(--blue-base) 14%, transparent);
--ai: var(--violet-base);
--ai-text: var(--violet-bright);
--ai-soft: color-mix(in oklab, var(--violet-base) 13%, transparent);
/* Selection */
--selection-bg: var(--blue-base);
--selection-fg: var(--sea-10);
}
/* ============================================================
Semantic aliases — LIGHT ("Skyfall Day")
Same laws, inverted: surfaces at L 0.940.98, text at L 0.270.45,
chromatic fills & colored text drop to the deep (L 0.48) step.
============================================================ */
[data-theme="light"] {
color-scheme: light;
/* Surfaces */
--surface-sunken: var(--sea-94);
--surface-base: var(--sea-96);
--surface-raised: var(--sea-98);
--surface-overlay: #ffffff;
--surface-card: var(--sea-98);
--surface-input: #ffffff;
--surface-scrim: oklch(0.27 0.024 268 / 0.4);
/* Text */
--text-heading: var(--sea-15);
--text-body: oklch(0.38 0.02 255);
--text-muted: var(--sea-50);
--text-faint: var(--sea-60);
--text-inverse: var(--sea-90);
--text-link: var(--blue-deep);
--text-link-hover: var(--sea-15);
/* Borders */
--border-subtle: oklch(0.89 0.014 210);
--border-default: oklch(0.84 0.016 215);
--border-strong: var(--sea-75);
--border-focus: var(--blue-deep);
/* Accent */
--accent: var(--blue-deep);
--accent-hover: oklch(0.53 0.12 262);
--accent-active: oklch(0.44 0.12 262);
--accent-bright: var(--blue-base);
--accent-text: var(--blue-deep);
--accent-contrast: #ffffff;
--accent-soft: color-mix(in oklab, var(--blue-deep) 10%, transparent);
--accent-soft-hover: color-mix(in oklab, var(--blue-deep) 18%, transparent);
/* Secondary accent */
--secondary: var(--cyan-deep);
--secondary-bright: var(--cyan-base);
--secondary-text: var(--cyan-deep);
--secondary-soft: color-mix(in oklab, var(--cyan-deep) 9%, transparent);
/* Semantic status */
--success: var(--green-deep);
--success-text: var(--green-deep);
--success-soft: color-mix(in oklab, var(--green-deep) 10%, transparent);
--warning: var(--amber-deep);
--warning-text: var(--amber-deep);
--warning-soft: color-mix(in oklab, var(--amber-base) 16%, transparent);
--danger: var(--red-deep);
--danger-hover: oklch(0.55 0.16 25);
--danger-text: var(--red-deep);
--danger-contrast: #ffffff;
--danger-soft: color-mix(in oklab, var(--red-deep) 8%, transparent);
--info: var(--blue-deep);
--info-text: var(--blue-deep);
--info-soft: color-mix(in oklab, var(--blue-deep) 9%, transparent);
--ai: var(--violet-deep);
--ai-text: var(--violet-deep);
--ai-soft: color-mix(in oklab, var(--violet-deep) 9%, transparent);
/* Selection */
--selection-bg: var(--blue-base);
--selection-fg: #ffffff;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
/* Australis Skyfall — spacing, radii, shadows, motion (v3 "calm depth") */
:root {
/* Spacing (4px base) */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
--space-24: 96px;
--space-32: 128px;
/* Semantic spacing — airy content, compact chrome */
--pad-chrome: var(--space-2); /* app chrome: rails, toolbars, list rows */
--pad-content: var(--space-8); /* work surfaces: page bodies, card interiors breathe */
--gap-pane: var(--space-4); /* gutter between panes/cards on the canvas */
--measure: 68ch; /* long-form reading width */
/* Radii — moderate, calm */
--radius-xs: 6px;
--radius-sm: 8px;
--radius-md: 10px; /* buttons, inputs, menu items */
--radius-lg: 14px; /* cards, panes, popovers */
--radius-xl: 16px; /* dialogs, drawers, command palette */
--radius-full: 999px;
/* Depth — crisp 1px hairline + layered soft shadow underneath.
Every elevated surface pairs border: 1px solid var(--border-subtle)
with one of these. Shadows are cool-tinted and two-layer:
a tight contact shadow + a wide ambient falloff. */
--shadow-xs: 0 1px 2px oklch(0.13 0.02 265 / 0.22);
--shadow-sm: 0 1px 2px oklch(0.13 0.02 265 / 0.2), 0 2px 8px -2px oklch(0.13 0.02 265 / 0.24);
--shadow-md: 0 2px 4px oklch(0.13 0.02 265 / 0.22), 0 10px 28px -6px oklch(0.13 0.02 265 / 0.36);
--shadow-lg: 0 4px 8px oklch(0.13 0.02 265 / 0.26), 0 28px 64px -12px oklch(0.13 0.02 265 / 0.5);
--shadow-drawer: 0 8px 16px oklch(0.13 0.02 265 / 0.28), 0 32px 80px -8px oklch(0.13 0.02 265 / 0.55);
/* Glows — hero moments ONLY: empty states, featured cards, command palette */
--glow-accent: 0 0 72px -16px color-mix(in oklab, var(--blue-base) 42%, transparent);
--glow-secondary: 0 0 72px -16px color-mix(in oklab, var(--cyan-base) 38%, transparent);
--glow-ai: 0 0 72px -16px color-mix(in oklab, var(--violet-base) 40%, transparent);
/* Active rail — left accent bar on active nav / selected items */
--rail-active: inset 2px 0 0 0 var(--accent);
/* Focus ring */
--focus-ring: 0 0 0 2px var(--surface-base), 0 0 0 4px var(--border-focus);
/* Motion — calm, no bounce */
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* @kind other */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* @kind other */
--duration-fast: 120ms; /* @kind other */
--duration-base: 180ms; /* @kind other */
--duration-slow: 300ms; /* @kind other */
--duration-drawer: 260ms; /* @kind other */
}
[data-theme="light"] {
--shadow-xs: 0 1px 2px oklch(0.3 0.02 255 / 0.07);
--shadow-sm: 0 1px 2px oklch(0.3 0.02 255 / 0.06), 0 2px 8px -2px oklch(0.3 0.02 255 / 0.08);
--shadow-md: 0 2px 4px oklch(0.3 0.02 255 / 0.06), 0 10px 28px -6px oklch(0.3 0.02 255 / 0.12);
--shadow-lg: 0 4px 8px oklch(0.3 0.02 255 / 0.07), 0 28px 64px -12px oklch(0.3 0.02 255 / 0.18);
--shadow-drawer: 0 8px 16px oklch(0.3 0.02 255 / 0.08), 0 32px 80px -8px oklch(0.3 0.02 255 / 0.22);
--glow-accent: 0 0 72px -16px color-mix(in oklab, var(--blue-deep) 26%, transparent);
--glow-secondary: 0 0 72px -16px color-mix(in oklab, var(--cyan-deep) 24%, transparent);
--glow-ai: 0 0 72px -16px color-mix(in oklab, var(--violet-deep) 25%, transparent);
}
+238
View File
@@ -0,0 +1,238 @@
/* Homepage — Australis Skyfall theme.
*
* SOURCE OF TRUTH: this file plus `theme/colors.css`, `theme/layout.css` and
* `theme/typography.css`, which are vendored VERBATIM from the Australis
* Skyfall handoff bundle. Do not hand-edit `conf/custom.css` — it is
* generated. Run `theme/build.py` after changing anything here.
*
* WHY A BUILD STEP AT ALL: Homepage serves exactly two files out of its
* config directory, `/api/config/custom.css` and `/api/config/custom.js`.
* There is no static route for anything else, so a `@font-face` pointing at
* `theme/fonts/Supreme-400.woff2` would 404 — the face has to arrive inside
* the stylesheet as a data: URI. The build inlines it.
*
* WHICH FACES, AND WHY NOT ALL THREE: Skyfall specifies Bespoke Sans
* (display), Supreme (body/UI) and Victor Mono (code/data). Only Supreme is
* embedded. A link dashboard has no display type — nothing here is a headline
* or a hero number — and Victor Mono ships as 2.4 MB TTF statics per cut,
* which is 30x this whole stylesheet for the handful of latency figures on
* the cards. Both are named in the stacks below so they resolve if they ever
* get vendored.
*
* DEVIATIONS FROM THE SYSTEM, STATED PLAINLY:
*
* 1. Skyfall says "no photography, no textures" for backgrounds, and permits
* exactly one decorative motif: a subtle aurora gradient, on hero and
* empty-state areas ONLY, "never behind body text blocks". This dashboard
* IS a body-text block, and it carries a generated aurora image behind it.
* That is a real departure, taken on the operator's explicit instruction
* to generate a background with Arbo. It is mitigated, not waved away:
* the image is abstract with no subject (Arbo's `t2i-ui-background`
* workflow), strictly cool-temperature per the imagery rule, and pinned
* to low opacity so card contrast is never the thing that suffers.
* If it still reads as busy, the honest fix is to drop the image and let
* `--surface-sunken` be the canvas, which is what the system actually asks
* for.
*
* 2. Homepage renders service icons as full-colour vendor logos. Skyfall
* wants one cool, technical icon family. Not fixable from CSS without
* desaturating every logo into illegibility, so the icons stay as they
* are.
*
* SPECIFICITY: Homepage is Tailwind-generated, so utilities win on equal
* specificity. Every `!important` here is load-bearing; removing one silently
* reverts that rule to the stock theme.
*/
/* @@FONTS@@ */
/* ============================================================
Skyfall tokens — vendored verbatim from the handoff bundle.
============================================================ */
/* @@TOKENS@@ */
/* ============================================================
Homepage bindings — the only part of this file that is ours.
Everything above is Skyfall; everything below maps Skyfall's
semantic layer onto Homepage's DOM. Never hardcode a hex here:
the system's rule is that UI consumes --surface-*/--text-*/
--border-*/--accent*/status tokens, not raw values.
============================================================ */
body,
#page_wrapper,
#page_container {
background-color: var(--surface-sunken) !important;
color: var(--text-body) !important;
font-family: var(--font-body) !important;
font-size: var(--text-base) !important;
line-height: var(--leading-normal) !important;
}
::selection {
background: var(--selection-bg);
color: var(--selection-fg);
}
/* ---------- header chrome ----------
Compact chrome per the negative-space rule: --pad-chrome, not --pad-content. */
#information-widgets .widget-container,
#information-widgets .information-widget-search {
background: var(--surface-card) !important;
border: 1px solid var(--border-subtle) !important;
border-radius: var(--radius-lg) !important;
box-shadow: var(--shadow-sm) !important;
padding: var(--pad-chrome) !important;
}
#information-widgets .information-widget-search input {
background: transparent !important;
color: var(--text-body) !important;
font-family: var(--font-body) !important;
}
#information-widgets .information-widget-search input::placeholder {
color: var(--text-faint) !important;
}
/* Telemetry is data — the system says data is always mono. */
.information-widget-resource .resource-value,
.service-block,
.service-stats {
font-family: var(--font-mono) !important;
font-variant-numeric: tabular-nums !important;
}
/* ---------- tabs ----------
The active rail is one of only two sanctioned accent expressions:
a 2px accent bar plus an --accent-soft fill. No glow — glows are
reserved for hero moments, and a tab is not one. */
#myTab li button {
color: var(--text-muted) !important;
font-family: var(--font-body) !important;
font-weight: var(--weight-medium) !important;
border-radius: var(--radius-md) !important;
transition: color var(--duration-fast) var(--ease-out),
background var(--duration-fast) var(--ease-out) !important;
}
#myTab li button:hover {
color: var(--text-body) !important;
background: var(--accent-soft) !important;
}
#myTab li button[aria-selected="true"] {
background: var(--accent-soft) !important;
color: var(--text-heading) !important;
box-shadow: var(--rail-active) !important;
}
/* ---------- group headers ----------
ALL-CAPS is permitted only for tiny eyebrow labels — this is that case. */
.service-group-name,
.bookmark-group-name {
color: var(--text-faint) !important;
font-family: var(--font-body) !important;
font-size: var(--text-xs) !important;
font-weight: var(--weight-semibold) !important;
letter-spacing: var(--tracking-caps) !important;
text-transform: uppercase !important;
}
/* ---------- cards ----------
The depth recipe, and it is not optional: crisp 1px hairline AND a
two-layer soft shadow. Never one without the other on a floating
surface — a card with only a border reads flat, one with only a
shadow reads smudged. */
.service-card,
.bookmark-list li > a {
background: var(--surface-card) !important;
border: 1px solid var(--border-subtle) !important;
border-radius: var(--radius-lg) !important;
box-shadow: var(--shadow-sm) !important;
transition: background var(--duration-fast) var(--ease-out),
border-color var(--duration-fast) var(--ease-out),
box-shadow var(--duration-base) var(--ease-out) !important;
}
/* Hover: surfaces lighten one step, interactive cards lift sm -> md. */
.service-card:hover,
.bookmark-list li > a:hover {
background: var(--surface-raised) !important;
border-color: var(--border-default) !important;
box-shadow: var(--shadow-md) !important;
}
.service-name,
.bookmark-name {
color: var(--text-heading) !important;
font-weight: var(--weight-semibold) !important;
letter-spacing: var(--tracking-normal) !important;
}
.service-description,
.bookmark-description {
color: var(--text-muted) !important;
font-size: var(--text-sm) !important;
line-height: var(--leading-snug) !important;
}
/* ---------- status ----------
Semantic colour is the whole point: the system communicates status with
colour and icons, never emoji, and accents are semantic-ONLY. Stock
Homepage paints every pill the same slate, which throws away the signal. */
.service-tag,
.service-block,
.service-stats {
background: var(--surface-raised) !important;
border: 1px solid var(--border-subtle) !important;
border-radius: var(--radius-md) !important;
color: var(--text-muted) !important;
font-size: var(--text-2xs) !important;
}
.status-online,
.status-healthy,
.status-running,
.status-started {
color: var(--success-text) !important;
background: var(--success-soft) !important;
}
.status-offline,
.status-unhealthy,
.status-dead,
.status-error {
color: var(--danger-text) !important;
background: var(--danger-soft) !important;
}
.status-exited,
.status-paused,
.status-partial,
.status-unknown,
.status-not-found {
color: var(--warning-text) !important;
background: var(--warning-soft) !important;
}
/* ---------- focus ---------- */
a:focus-visible,
button:focus-visible,
input:focus-visible {
outline: none !important;
box-shadow: var(--focus-ring) !important;
border-radius: var(--radius-sm) !important;
}
/* ---------- motion ----------
"Respect reduced-motion" is in the system's motion rule, not an extra. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
+40
View File
@@ -0,0 +1,40 @@
/* Australis Skyfall — Typography tokens */
:root {
/* Families */
--font-display: "Bespoke Sans", "Avenir Next", "Segoe UI", sans-serif;
--font-body: "Supreme", "Helvetica Neue", "Segoe UI", sans-serif;
--font-mono: "VictorMono Nerd Font", "SF Mono", "Cascadia Code", monospace;
/* Scale (web-app oriented; 14px UI base) */
--text-2xs: 11px;
--text-xs: 12px;
--text-sm: 13px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 22px;
--text-2xl: 28px;
--text-3xl: 36px;
--text-4xl: 48px;
--text-5xl: 64px;
/* Weights */
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--weight-extrabold: 800;
/* Line heights */
--leading-tight: 1.15;
--leading-snug: 1.35;
--leading-normal: 1.55;
--leading-relaxed: 1.7;
/* Tracking */
--tracking-tight: -0.02em;
--tracking-normal: 0;
--tracking-wide: 0.06em;
--tracking-caps: 0.1em;
}