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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user