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:
vh
2026-08-18 23:30:27 -07:00
parent c3de7dbd58
commit 45c1995d7a
13 changed files with 1253 additions and 119 deletions
+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()