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.
This commit is contained in:
vh
2026-08-19 09:14:13 -07:00
parent b92097688c
commit b271db1f44
14 changed files with 1076 additions and 1369 deletions
+517
View File
@@ -0,0 +1,517 @@
/* Homepage — Australis.
*
* SOURCE OF TRUTH: this file. Do not hand-edit `conf/custom.css`; it is
* generated. Run `theme/build.py` after changing anything here, then
* `scripts/deploy-stack.sh esh-docker-vm homepage --conf`.
*
* WHY A BUILD STEP: 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/*.woff2` would 404 — the faces have to arrive inside the
* stylesheet as data: URIs. The build inlines them.
*
* TOKENS ARE CANONICAL, NOT DERIVED. Every hex below is copied verbatim from
* `~/.claude/skills/australis-design/colors_and_type.css`. The predecessor
* theme built a parallel OKLCH palette "derived from the Australis
* philosophy" rather than using the system's own values, which is how a
* design system quietly forks. `build.py` re-checks these against the skill
* file at build time when it is present and warns on drift.
*
* DIRECTION: instrument panel. This is engineering chrome, not a page with
* wallpaper. Canon: "the screen is the polar sky — empty, with light coming
* through it." Three moves carry it:
*
* 1. Group headings become the Australis eyebrow (mono / uppercase / 11px /
* 0.08em, cyan) with a hairline running to the right edge. Canon names
* the eyebrow a system signature and says to use it often; here it turns
* the groups into register bands instead of headings floating over a
* grid.
* 2. Status stops shouting. The filled emerald chips read louder than the
* service names they annotate; they become a semantic dot plus a mono
* micro-label at tertiary contrast.
* 3. Cards are bordered and opaque, sized to their own content. Canon puts
* border above shadow for grouping in chrome, and equal-height rows turn
* short cards into hollow boxes.
*
* WHAT WAS REMOVED, AND WHY IT IS NOT A REGRESSION: the previous theme
* carried a generated full-bleed aurora image behind the whole dashboard,
* added on the operator explicitly asking for an Arbo-generated background.
* 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 operator called the result ugly on
* 2026-08-19 and asked for a canonical-colors rebuild, so the image is gone.
* The aurora survives where canon actually sanctions it: a single 1px accent
* edge under the tab bar. If a background is ever wanted again, that is the
* conversation to reopen — not a quiet re-add.
*
* TYPE: Space Grotesk (display) / Inter (UI, body) / JetBrains Mono (all
* metadata) — canonical per the system README. Vendored as latin-subset
* VARIABLE woff2, one file per family covering the whole weight axis: 102 KB
* for three families against 56 KB for the three static Supreme cuts the
* predecessor shipped, and no Google Fonts request at page load.
*/
/* @@FONTS@@ */
:root {
/* Ice */
--aus-black: #222531;
--aus-white: #a9bcc3;
--aus-bright-white: #cce7ec;
/* Sea */
--aus-bright-black: #373b46;
--aus-dark-30: #414751;
--aus-dark-40: #565f69;
--aus-dark-50: #6e7882;
--aus-dark-60: #86929d;
--aus-bright-70: #9daeb6;
--aus-bright-80: #b3cbcf;
/* Aurora */
--aus-blue: #6388d8;
--aus-bright-blue: #a4c4ff;
--aus-cyan: #00b1a8;
--aus-bright-cyan: #42dcd1;
--aus-green: #16b866;
--aus-bright-green: #51e08a;
/* Dawn */
--aus-red: #ff491a;
--aus-yellow: #e1c631;
--aus-magenta: #9d78ff;
--bg-0: var(--aus-black);
--bg-1: var(--aus-bright-black);
--bg-2: var(--aus-dark-30);
--border-subtle: var(--aus-dark-30);
--border-default: var(--aus-dark-40);
--fg-0: var(--aus-bright-white);
--fg-1: var(--aus-white);
--fg-2: var(--aus-bright-70);
--fg-3: var(--aus-dark-60);
--fg-muted: var(--aus-dark-50);
--font-display: "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
--tracking-caps: 0.08em;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 10px;
--shadow-1: 0 1px 2px rgba(10, 12, 18, 0.4);
--dur-fast: 120ms;
--ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* ---- Canvas --------------------------------------------------------------
Solid fill. Canon forbids full-bleed imagery and decorative gradient fills
behind text. The single sanctioned aurora moment is the hairline under the
header chrome, further down. */
html,
body,
#page_wrapper,
body > div {
background-color: var(--bg-0) !important;
background-image: none !important;
}
body {
font-family: var(--font-sans) !important;
color: var(--fg-1);
font-size: 15px;
-webkit-font-smoothing: antialiased;
}
/* ---- Group heading — the Australis eyebrow --------------------------------
Canon calls mono/uppercase/11px/0.08em the system's signature and says to
use it often. It replaces the oversized sans heading, and a hairline runs
from the label to the right edge so the groups read as register bands
rather than as titles floating over a grid. */
.services-group > button.group {
gap: 0;
margin-bottom: 4px;
margin-top: 10px;
}
.service-group-icon {
display: none !important;
}
h2.service-group-name {
font-family: var(--font-mono) !important;
font-size: 11px !important;
font-weight: 500 !important;
line-height: 1 !important;
text-transform: uppercase;
letter-spacing: var(--tracking-caps);
color: var(--aus-cyan) !important;
white-space: nowrap;
}
.services-group > button.group::after {
content: "";
flex: 1;
height: 1px;
margin-left: 12px;
background: var(--border-subtle);
}
/* The chevron sits after the rule, muted. */
.services-group > button.group svg {
color: var(--fg-muted);
margin-left: 8px;
width: 14px;
height: 14px;
}
/* ---- Service cards -------------------------------------------------------
Border > shadow for grouping in chrome (canon). Opaque fill, 1px border,
one soft shadow. Height is content-driven — equal-height stretching turns
short cards into hollow boxes. */
ul.services-list {
align-items: start !important;
row-gap: 8px;
}
li.service > .service-card {
height: auto !important;
background: var(--bg-1) !important;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg) !important;
box-shadow: var(--shadow-1) !important;
margin-bottom: 0 !important;
transition: background-color var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out);
}
li.service > .service-card:hover {
background: var(--bg-2) !important;
border-color: var(--border-default);
}
/* Service name — display face, tight. Description — body face, secondary. */
.service-name {
font-family: var(--font-display) !important;
font-size: 14px !important;
font-weight: 500 !important;
/* Pinned so the status cluster can be centred on this exact line rather
than floating against the card's top edge. Changing one without the
other breaks the alignment. */
line-height: 17px !important;
letter-spacing: -0.015em;
color: var(--fg-0) !important;
/* Reserve the status gutter. Matches --status-gutter below; a long name
ellipsises into it instead of colliding with the pill. */
padding-right: 78px !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
p.service-description {
font-family: var(--font-sans) !important;
font-size: 12.5px !important;
font-weight: 400 !important;
line-height: 17px !important;
color: var(--fg-3) !important;
margin-top: 3px;
/* Two lines minimum. Cards in a row now bottom-align for the common
one-line/two-line mix, which is what made the grid look ragged, without
bringing back equal-height stretching — that inflated short cards to
match a widget card twice their height, which was far worse. Descriptions
longer than two lines still grow; nothing is truncated. */
min-height: 34px;
white-space: normal;
}
.service-icon {
opacity: 0.9;
}
/* ---- Status — dots, not chips --------------------------------------------
The filled emerald pills read louder than the service names they annotate.
Replaced with a semantic dot plus a mono micro-label at tertiary contrast:
present when you look for it, silent when you are not. */
/* The status cluster is CENTRED ON THE SERVICE NAME'S LINE, not parked in the
card's top-right corner. Homepage positions it `absolute top-0 right-0`,
which floats it ~7px above the title's optical centre and reads as a
misalignment on every card. The offsets below reconstruct the title line's
geometry: .service-name sits 5px below the card's top edge and carries 8px
of its own top padding, so its 17px line box starts at 13px and centres at
21.5px. Giving the cluster that top and that height, and centring inside it,
puts the pill and the name on one optical line.
These three numbers move together — if the name's font-size or line-height
changes, recompute all of them. */
.service-tags {
top: 13px !important;
height: 17px;
align-items: center !important;
gap: 12px !important;
margin-top: 0 !important;
/* 14px right inset, against the 8px of text padding on the left — the
optical match, since the pill has no glyph hard against its edge. */
margin-right: 14px !important;
}
/* Every child of the cluster shares one baseline: status pills, latency
badges and container-stat buttons alike. */
.service-tag {
display: flex !important;
align-items: center !important;
height: 17px;
line-height: 17px !important;
}
.service-tag,
.docker-status,
.site-monitor-status,
.service-tag > div:not(.sr-only) {
background: transparent !important;
background-color: transparent !important;
padding: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.docker-status > div,
.service-tag .text-\[8px\] {
display: flex !important;
align-items: center !important;
font-family: var(--font-mono) !important;
font-size: 9px !important;
font-weight: 500 !important;
line-height: 1 !important;
letter-spacing: var(--tracking-caps);
color: var(--fg-3) !important;
}
/* Flex-centred rather than nudged with vertical-align: the dot is an optical
round on a cap-height uppercase run, so it needs true centring, not a magic
pixel offset that only looks right at one font size. */
.docker-status > div::before {
content: "\25CF"; /* ● */
display: inline-block;
margin-right: 6px;
font-size: 7px;
line-height: 1;
color: var(--aus-dark-50);
}
.docker-status-healthy > div::before,
.docker-status-running > div::before {
color: var(--aus-green);
}
.docker-status-unhealthy > div::before,
.docker-status-exited > div::before,
.docker-status-dead > div::before {
color: var(--aus-red);
}
.docker-status-starting > div::before,
.docker-status-paused > div::before {
color: var(--aus-yellow);
}
/* Ping / latency — mono metadata, tabular, no chip. */
.site-monitor-status > div {
font-family: var(--font-mono) !important;
font-size: 9px !important;
font-weight: 400 !important;
letter-spacing: 0.04em;
text-transform: lowercase !important;
font-variant-numeric: tabular-nums;
color: var(--fg-muted) !important;
}
/* ---- Widget stat strip ---------------------------------------------------
Was nested boxes-in-a-box. Now a footer strip: hairline above, cells
divided by hairlines, value in the display face, label as a mono eyebrow.
The card stays one object instead of three. */
.service-container {
margin-top: 8px;
padding-top: 2px;
border-top: 1px solid var(--border-subtle);
gap: 0 !important;
}
.service-container > div {
background: transparent !important;
border-radius: 0 !important;
margin: 0 !important;
padding: 8px 4px 6px !important;
border-left: 1px solid var(--border-subtle);
}
.service-container > div:first-child {
border-left: 0;
}
/* value — nowrap because a long one (AdGuard's "32.767 ms") otherwise wraps
mid-cell and drags the whole card taller than its row-mates. */
.service-container > div > div:not(.uppercase) {
font-family: var(--font-display) !important;
font-size: 17px !important;
font-weight: 600 !important;
letter-spacing: -0.015em;
line-height: 1.2 !important;
white-space: nowrap;
font-variant-numeric: tabular-nums;
color: var(--fg-0) !important;
}
.service-container > div {
min-width: 0; /* let a long value shrink the cell instead of overflowing */
}
/* label */
.service-container > div > div.uppercase {
font-family: var(--font-mono) !important;
font-size: 9px !important;
font-weight: 400 !important;
letter-spacing: var(--tracking-caps);
color: var(--fg-3) !important;
margin-top: 2px;
}
/* ---- Tab bar -------------------------------------------------------------
The filled pill is replaced by a 2px top edge in aurora blue — canon's one
sanctioned accent-border pattern (and explicitly never a left border). */
ul.sm\:flex {
position: relative;
background: transparent !important;
background-color: transparent !important;
border-radius: 0 !important;
gap: 0;
overflow: visible;
}
/* The one moment of colour on the page. Canon sanctions the aurora glow —
bright-blue → bright-cyan → green — as an accent EDGE, never as a fill
behind text. A 1px rule under the tab bar is that edge, and it is the only
place the full aurora appears. */
ul.sm\:flex::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background: linear-gradient(
90deg,
var(--aus-blue) 0%,
var(--aus-cyan) 42%,
var(--aus-green) 68%,
var(--border-subtle) 100%
);
opacity: 0.9;
}
ul.sm\:flex > li {
height: 40px !important;
border-radius: 0 !important;
background: transparent !important;
overflow: visible;
}
ul.sm\:flex > li > button {
margin: 0 !important;
height: 100%;
border-radius: 0 !important;
background: transparent !important;
background-color: transparent !important;
box-shadow: none !important;
border-top: 2px solid transparent;
font-family: var(--font-mono) !important;
font-size: 11px !important;
font-weight: 500 !important;
text-transform: uppercase;
letter-spacing: var(--tracking-caps);
color: var(--fg-3) !important;
transition: color var(--dur-fast) var(--ease-out);
}
ul.sm\:flex > li > button:hover {
color: var(--fg-1) !important;
background: transparent !important;
}
ul.sm\:flex > li > button.dark\:bg-white\/10 {
border-top-color: var(--aus-blue) !important;
color: var(--fg-0) !important;
background: linear-gradient(
180deg,
rgba(99, 136, 216, 0.12) 0%,
rgba(99, 136, 216, 0) 72%
) !important;
}
/* ---- Header chrome: resource widget + search -----------------------------
Same card treatment as everything else, so the header belongs to the page
instead of hovering above it. */
.information-widget-resource,
div.flex.flex-col.justify-center.mt-2 {
background: var(--bg-1) !important;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg) !important;
box-shadow: var(--shadow-1) !important;
}
.information-widget-resource {
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
/* The header strip that holds the resource widgets. */
div.flex.flex-row.self-center.flex-wrap.justify-between {
background: var(--bg-1) !important;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-1);
}
.information-widget-resource .text-xs,
.information-widget-resource span {
font-family: var(--font-mono) !important;
font-size: 11px !important;
letter-spacing: 0.02em;
color: var(--fg-2) !important;
}
.resource-usage,
[class*="resource-usage"] {
background: var(--aus-black) !important;
height: 3px !important;
border-radius: 2px !important;
}
.resource-usage > div,
[class*="resource-usage"] > div {
background: var(--aus-cyan) !important;
border-radius: 2px !important;
}
/* Search input */
input[type="text"],
input[type="search"] {
font-family: var(--font-sans) !important;
font-size: 14px !important;
color: var(--fg-1) !important;
}
input::placeholder {
color: var(--fg-muted) !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
letter-spacing: 0.04em;
}
/* ---- Focus — the aurora glow, canon's signature interaction motif -------- */
a:focus-visible,
button:focus-visible,
input:focus-visible {
outline: none !important;
box-shadow: 0 0 0 3px rgba(99, 136, 216, 0.35) !important;
border-radius: var(--radius-sm);
}