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:
@@ -1,357 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* ---------- widget stat wells (Plex, Jellyfin, Paperless, Uptime Kuma) ----------
|
||||
Stock Homepage builds each stat as:
|
||||
<div class="font-thin text-sm">353</div> <- the VALUE
|
||||
<div class="font-bold text-xs uppercase">Movies</div> <- the LABEL
|
||||
which is exactly backwards. The number you actually came to read is
|
||||
weight 100 at 13px; the label shouting beside it is weight 700. Skyfall's
|
||||
rule is that hierarchy comes emphatically from weight AND size, and that
|
||||
numbers are the data — so this inverts the pair: the value goes big, bold
|
||||
and bright, the label drops to a quiet eyebrow.
|
||||
|
||||
The well itself is --surface-input, one step DOWN from the card it sits on,
|
||||
so stats read as inset data rather than as another floating surface. Depth
|
||||
here is a recess, so it takes the hairline without the shadow. */
|
||||
.service-block {
|
||||
background: var(--surface-input) !important;
|
||||
border: 1px solid var(--border-subtle) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
padding: var(--space-2) var(--space-1) !important;
|
||||
}
|
||||
|
||||
/* Sizing note (operator, 2026-08-19): the first pass at this went from
|
||||
font-thin-13px to bold-22px-in-heading-white, which overshot from "whisper"
|
||||
straight to "shout". The number only has to out-rank its own label, not the
|
||||
service name above it. --text-md at medium weight in cyan clears the label
|
||||
comfortably while sitting BELOW the card title in the hierarchy, which is
|
||||
where a stat belongs. Resist re-raising this. */
|
||||
.service-block > div:first-child {
|
||||
color: var(--secondary-text) !important;
|
||||
font-family: var(--font-mono) !important;
|
||||
font-size: var(--text-md) !important;
|
||||
font-weight: var(--weight-medium) !important;
|
||||
line-height: var(--leading-tight) !important;
|
||||
font-variant-numeric: tabular-nums !important;
|
||||
}
|
||||
|
||||
.service-block > div:last-child {
|
||||
color: var(--text-faint) !important;
|
||||
font-family: var(--font-body) !important;
|
||||
font-size: var(--text-2xs) !important;
|
||||
font-weight: var(--weight-medium) !important;
|
||||
letter-spacing: var(--tracking-caps) !important;
|
||||
line-height: var(--leading-snug) !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;
|
||||
}
|
||||
|
||||
/* ---------- the aurora rotation ----------
|
||||
This is where the colour on this dashboard comes from, and it is sanctioned
|
||||
rather than smuggled in: Skyfall names Aurora (blue, cyan, green) the
|
||||
PRIMARY families, "used generously, in that order". What is restricted is
|
||||
Dawn — amber, red, violet — which stays semantic-only (warning, danger, AI)
|
||||
and is therefore never used decoratively anywhere in this file.
|
||||
|
||||
Section markers cycle blue -> cyan -> green down the page, so a group reads
|
||||
as a coloured band rather than another row of grey. The icons carry it at
|
||||
full strength; the names take the same hue at 0.72 so the eyebrow stays an
|
||||
eyebrow.
|
||||
|
||||
Homepage renders mdi icons as a gradient behind an SVG mask, which is why
|
||||
these are `background` rules and not `color` — setting color does nothing.
|
||||
Vendor logos (si-*) are real images and keep their brand colours; that mix
|
||||
is inherent to Homepage's icon model, not a choice made here. */
|
||||
.services-group:nth-of-type(3n + 1) .service-group-icon > div {
|
||||
background: linear-gradient(180deg, var(--blue-bright), var(--blue-base)) !important;
|
||||
}
|
||||
.services-group:nth-of-type(3n + 2) .service-group-icon > div {
|
||||
background: linear-gradient(180deg, var(--cyan-bright), var(--cyan-base)) !important;
|
||||
}
|
||||
.services-group:nth-of-type(3n + 3) .service-group-icon > div {
|
||||
background: linear-gradient(180deg, var(--green-bright), var(--green-base)) !important;
|
||||
}
|
||||
|
||||
.services-group:nth-of-type(3n + 1) .service-group-name {
|
||||
color: var(--blue-bright) !important;
|
||||
opacity: 0.72 !important;
|
||||
}
|
||||
.services-group:nth-of-type(3n + 2) .service-group-name {
|
||||
color: var(--cyan-bright) !important;
|
||||
opacity: 0.72 !important;
|
||||
}
|
||||
.services-group:nth-of-type(3n + 3) .service-group-name {
|
||||
color: var(--green-bright) !important;
|
||||
opacity: 0.72 !important;
|
||||
}
|
||||
|
||||
/* Per-service mdi icons take a single cool wash rather than joining the
|
||||
rotation — 60-odd cards cycling three hues would be noise, and the card
|
||||
grid is not the thing being sectioned. */
|
||||
.service-icon > div {
|
||||
background: linear-gradient(180deg, var(--sea-80), var(--cyan-base)) !important;
|
||||
}
|
||||
|
||||
/* Header resource icons (CPU / RAM / disk). */
|
||||
.information-widget-resource .resource-icon {
|
||||
color: var(--cyan-bright) !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-block is deliberately NOT in this list — the stat wells get their
|
||||
own treatment above, and folding them in here is what made every number
|
||||
render at --text-2xs. */
|
||||
.service-tag,
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ⚠️ The real class Homepage emits is `docker-status-<state>`, NOT
|
||||
`status-<state>`. The bare `.status-*` selectors below were written from a
|
||||
guess and matched nothing — the green pills in the first screenshots were
|
||||
stock Homepage colouring, not this file. They are kept only because the
|
||||
site-monitor path does use them; the docker-status ones are what actually
|
||||
bind on container cards. Verify against the DOM before adding more. */
|
||||
.docker-status-healthy,
|
||||
.docker-status-running,
|
||||
.docker-status-started,
|
||||
.status-online,
|
||||
.status-healthy,
|
||||
.status-running {
|
||||
color: var(--success-text) !important;
|
||||
background: var(--success-soft) !important;
|
||||
}
|
||||
|
||||
.docker-status-unhealthy,
|
||||
.docker-status-dead,
|
||||
.status-offline,
|
||||
.status-unhealthy,
|
||||
.status-error {
|
||||
color: var(--danger-text) !important;
|
||||
background: var(--danger-soft) !important;
|
||||
}
|
||||
|
||||
.docker-status-exited,
|
||||
.docker-status-paused,
|
||||
.docker-status-created,
|
||||
.status-exited,
|
||||
.status-paused,
|
||||
.status-partial,
|
||||
.status-unknown,
|
||||
.status-not-found {
|
||||
color: var(--warning-text) !important;
|
||||
background: var(--warning-soft) !important;
|
||||
}
|
||||
|
||||
/* Latency / reachability readings are telemetry, not status — giving them the
|
||||
info family keeps "how fast" visually separate from "is it alive", which
|
||||
otherwise both render as the same pill in the same corner. */
|
||||
.service-tag.service-site-monitor,
|
||||
.site-monitor-status,
|
||||
.service-tag.service-ping {
|
||||
color: var(--info-text) !important;
|
||||
background: var(--info-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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user