Files
esh-pfi-infrastructure/stacks/homepage/theme/build.py
T
vh b271db1f44 feat(homepage): rebuild the theme on canonical Australis tokens
The predecessor theme was ugly for two structural reasons, not one.

It did not use the design system's colours. It built a parallel OKLCH
palette "derived from the Australis philosophy" and swapped the canonical
typeface for Supreme -- a fork, not a theme. Every hex here is now copied
verbatim from ~/.claude/skills/australis-design/colors_and_type.css, and
build.py re-checks all 19 against that file at build time and warns on
drift so it cannot quietly fork again. Type is the canonical stack: Space
Grotesk / Inter / JetBrains Mono, vendored as latin-subset VARIABLE woff2
(one file per family, 102 KB total against 56 KB for three static Supreme
cuts, and no Google Fonts request at page load).

It also carried a generated full-bleed aurora image behind the entire
dashboard. Canon forbids exactly that -- "solid fills only on chrome, no
full-bleed photography, no decorative gradients", and the aurora motif
"never as a background fill behind text". The predecessor knew, said so in
its own header, and dialled the opacity down rather than dropping it. The
image is gone; the aurora survives as a 1px accent edge under the tab bar,
which is where canon sanctions it. The asset stays in images/ in case it
is ever revisited.

Direction is instrument panel. Group headings become the Australis mono
eyebrow with a hairline to the right edge -- canon calls the eyebrow a
system signature, and it turns the groups into register bands instead of
headings floating over a grid. Status stops shouting: the filled emerald
chips read louder than the service names they annotated, so they are now a
semantic dot plus a mono micro-label at tertiary contrast. Cards are
bordered and opaque, per canon's border-over-shadow rule for chrome.

Alignment, per operator feedback that pills and cards did not line up:

- The status cluster is centred on the service name's line rather than
  parked in the card's top-right corner, where Homepage's `absolute top-0`
  left it floating ~7px above the title's optical centre. The offsets
  reconstruct the title line box and are documented as moving together.
- Descriptions get a two-line minimum, so the common one-line/two-line mix
  bottom-aligns across a row. This is what made the grid look ragged.
  useEqualHeights stays false: it inflated short cards to match a widget
  card twice their height, which was the worse failure.
- The status dot is flex-centred rather than nudged with vertical-align,
  so it stays centred if the type scale changes.

Retires the Skyfall sources and the Supreme faces; theme/ now has one
source of truth.
2026-08-19 09:14:13 -07:00

115 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Generate conf/custom.css from the Australis 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
SRC = HERE / "australis.css.in"
OUT = HERE.parent / "conf" / "custom.css"
# The canonical token file. Present on a box with the australis-design skill
# installed; absent elsewhere, in which case the drift check is skipped rather
# than failing the build.
CANON = pathlib.Path.home() / ".claude/skills/australis-design/colors_and_type.css"
# One VARIABLE face per family — the whole 100-900 axis in a single file, so
# there is no per-weight fan-out. Latin subset only.
FONTS = [
("Inter", "Inter-Variable.woff2"),
("Space Grotesk", "SpaceGrotesk-Variable.woff2"),
("JetBrains Mono", "JetBrainsMono-Variable.woff2"),
]
# Tokens the generated sheet cannot work without. A vendoring mistake that
# drops the :root block produces a stylesheet that parses fine and renders
# nothing, so assert on the values rather than trusting the copy.
REQUIRED_TOKENS = ["--aus-black", "--aus-cyan", "--bg-1", "--font-mono"]
def font_faces() -> str:
out = []
for family, filename 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: 100 900;\n" # variable axis
f" font-style: normal;\n"
f" font-display: swap;\n"
f"}}"
)
return "\n".join(out)
def check_canon_drift(css: str) -> None:
"""Warn if a token here no longer matches the Australis skill's value.
Non-fatal on purpose: the skill is a per-workstation install, so a missing
file is normal and must not break a build on a box that lacks it. A *value*
mismatch is worth shouting about — that is the palette forking, which is
exactly how the theme this one replaced went wrong.
"""
if not CANON.exists():
print(f"note: {CANON} not present — canonical drift check skipped")
return
canon = dict(re.findall(r"(--aus-[a-z0-9-]+):\s*(#[0-9a-fA-F]{6});", CANON.read_text()))
ours = dict(re.findall(r"(--aus-[a-z0-9-]+):\s*(#[0-9a-fA-F]{6});", css))
if not canon:
print(f"note: parsed no --aus-* tokens from {CANON} — drift check skipped")
return
drift = [(k, v, canon[k]) for k, v in ours.items() if k in canon and v.lower() != canon[k].lower()]
missing = sorted(set(ours) - set(canon))
for key, mine, theirs in drift:
print(f"DRIFT: {key} is {mine} here, {theirs} in the skill", file=sys.stderr)
for key in missing:
print(f"note: {key} is not a canonical token", file=sys.stderr)
if drift:
print(
f"WARNING: {len(drift)} token(s) diverge from the canonical palette. "
"Reconcile before deploying — a derived palette is how the previous "
"theme forked from the design system.",
file=sys.stderr,
)
else:
print(f"canonical palette check: {len(ours)} tokens match {CANON.name}")
def main() -> None:
if not SRC.exists():
sys.exit(f"missing source: {SRC}")
src = SRC.read_text()
if "/* @@FONTS@@ */" not in src:
sys.exit(f"{SRC.name} lost its @@FONTS@@ placeholder")
css = src.replace("/* @@FONTS@@ */", font_faces())
for token in REQUIRED_TOKENS:
if token not in css:
sys.exit(f"generated css has no {token} — the token block did not survive")
check_canon_drift(css)
OUT.write_text(css)
print(f"wrote {OUT} ({len(css) / 1024:.0f} KB)")
if __name__ == "__main__":
main()