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:
2026-08-19 09:14:13 -07:00
parent b92097688c
commit b271db1f44
14 changed files with 1076 additions and 1369 deletions
File diff suppressed because one or more lines are too long
+23 -25
View File
@@ -3,31 +3,26 @@
title: ... all my base ...
theme: dark
# `color:` only accepts Homepage's built-in Tailwind ramps, and none of them is
# the Skyfall Sea ramp. slate is the nearest cool neutral and acts as the base
# under conf/custom.css, which is where the real palette lives.
# the Australis Sea ramp. slate is the nearest cool neutral and acts as the
# base under conf/custom.css, which is where the real palette lives.
color: slate
# Background — generated locally, not stock art. Arbo (irv-ml1:8201) rendered
# it from the `t2i-ui-background` workflow: abstract full-bleed, no subject,
# aurora blue->cyan->green on near-black, which is the one decorative motif
# Australis Skyfall permits and its "cool temperature always" imagery rule.
# Job 13f0891f4e42, seed 26, flux2-klein-9b, 2048x1152.
# NO `background:` BLOCK — DELIBERATE, DO NOT RE-ADD WITHOUT ASKING.
#
# Served from /app/public/images via the read-only mount in compose.yaml —
# NOT from the config dir, which Homepage will not serve static files out of.
# This carried a generated full-bleed aurora image (Arbo job 13f0891f4e42,
# seed 26, flux2-klein-9b) at opacity 30. It went in on an explicit request
# for an Arbo-generated background and came out on 2026-08-19 when the
# operator called the result ugly.
#
# ⚠️ opacity is the load-bearing number, not decoration. Skyfall's rule is that
# the aurora motif never sits behind body text; this dashboard breaks that on
# purpose, so the image has to stay far enough back that card contrast never
# suffers. The render is considerably brighter than the "mostly empty" brief,
# which is exactly what this dials out. Raise it and the cards start fighting
# the ribbon.
background:
image: /images/skyfall-aurora.webp
blur: sm
saturate: 90
brightness: 90
opacity: 30
# The image was also against canon on its own terms: Australis says "solid
# fills only on chrome — no full-bleed photography, no decorative gradients",
# and permits the aurora motif "never as a background fill behind text". A
# whole dashboard is text. The predecessor knew and dialled the opacity down
# instead of dropping it, which is how you end up with a quiet version of the
# wrong thing. The asset is still in `images/` if this is ever revisited.
#
# The aurora now appears exactly where canon sanctions it: a 1px accent edge
# under the tab bar, in conf/custom.css.
headerStyle: boxedWidgets
providers:
@@ -43,10 +38,13 @@ quicklaunch:
showStats: false
statusStyle: ""
# Force all cards in a row to match the tallest card's height. Without
# this, widget-bearing cards (Plex stats, Traefik metrics) stand taller
# than plain link cards and the grid looks ragged.
useEqualHeights: true
# Cards size to their own content. This was `true` to stop widget-bearing
# cards (Uptime Kuma's stat strip, Paperless's counts) standing taller than
# plain link cards and leaving the grid ragged — but the cure was worse than
# the disease: every short card in the row inflated into a hollow box to match
# the tall one. A ragged bottom edge reads as content; a stretched empty card
# reads as a rendering fault.
useEqualHeights: false
# Function-first layout, four-tab split:
# Main - daily-use apps, media, bookmarks, monitoring
+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);
}
+64 -31
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Generate conf/custom.css from the Skyfall source in this directory.
"""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
@@ -16,33 +16,40 @@ import re
import sys
HERE = pathlib.Path(__file__).resolve().parent
SRC = HERE / "australis.css.in"
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.
# 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 = [
("Supreme", "Supreme-400.woff2", "400"),
("Supreme", "Supreme-500.woff2", "500"),
("Supreme", "Supreme-700.woff2", "600 700"), # Supreme ships no 600 cut
("Inter", "Inter-Variable.woff2"),
("Space Grotesk", "SpaceGrotesk-Variable.woff2"),
("JetBrains Mono", "JetBrainsMono-Variable.woff2"),
]
# 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"]
# 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, weight in FONTS:
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-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-weight: 100 900;\n" # variable axis
f" font-style: normal;\n"
f" font-display: swap;\n"
f"}}"
@@ -50,29 +57,55 @@ def font_faces() -> str:
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 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:
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")
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())
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")
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)")
-208
View File
@@ -1,208 +0,0 @@
/* ============================================================
Australis Skyfall — Color Tokens v2
A ground-up web palette derived from the Australis Dark
PHILOSOPHY (not its ANSI values). Built in OKLCH so the laws
are explicit and every family stays perceptually consistent:
- All hues are cooler than neutral; nothing warmer than amber.
- One lightness law across every chromatic family:
deep L 0.48 — light-theme fills & colored text
base L 0.66 — dark-theme fills (the working color)
bright L 0.80 — dark-theme colored text & highlights
- Chroma is tuned per hue so no family shouts over another.
- Neutrals (Sea ramp) drift in hue from ice-blue (265°) toward
ocean green (195°) as they brighten — the aurora signature.
- Medium / medium-high contrast for eye comfort.
Families: Aurora (blue, cyan, green — use generously, in that
order) and Dawn (amber, red, violet — semantic accents only:
warning, danger, AI).
============================================================ */
:root {
/* ---- Sea — neutral ramp (hue 265° → 195° as L rises) ---- */
--sea-10: oklch(0.23 0.02 265);
--sea-15: oklch(0.27 0.024 268); /* dark canvas — the Australis anchor */
--sea-20: oklch(0.31 0.022 262);
--sea-25: oklch(0.35 0.02 258);
--sea-30: oklch(0.4 0.018 252);
--sea-40: oklch(0.48 0.016 245);
--sea-50: oklch(0.56 0.015 235);
--sea-60: oklch(0.64 0.014 228);
--sea-70: oklch(0.72 0.014 220);
--sea-75: oklch(0.78 0.015 212);
--sea-80: oklch(0.83 0.017 205);
--sea-90: oklch(0.9 0.02 198);
--sea-94: oklch(0.94 0.014 195);
--sea-96: oklch(0.96 0.01 195);
--sea-98: oklch(0.98 0.006 195);
/* ---- Ice — main colors (aliases into the ramp) ---- */
--ice-black: var(--sea-15);
--ice-white: var(--sea-75);
--ice-bright-white: var(--sea-90);
/* ---- Aurora — primary families (blue > cyan > green) ---- */
--blue-deep: oklch(0.48 0.12 262);
--blue-base: oklch(0.66 0.12 262);
--blue-bright: oklch(0.8 0.09 262);
--cyan-deep: oklch(0.48 0.08 200);
--cyan-base: oklch(0.66 0.1 200);
--cyan-bright: oklch(0.8 0.1 200);
--green-deep: oklch(0.48 0.11 158);
--green-base: oklch(0.66 0.13 158);
--green-bright: oklch(0.8 0.13 158);
/* ---- Dawn — accent families (semantic use only) ---- */
--amber-deep: oklch(0.52 0.1 75);
--amber-base: oklch(0.7 0.12 78);
--amber-bright: oklch(0.82 0.13 82);
--red-deep: oklch(0.5 0.15 25);
--red-base: oklch(0.62 0.16 25);
--red-bright: oklch(0.78 0.11 28);
--violet-deep: oklch(0.5 0.15 292);
--violet-base: oklch(0.66 0.14 292);
--violet-bright: oklch(0.8 0.1 292);
}
/* ============================================================
Semantic aliases — DARK (default, first-class)
============================================================ */
:root {
color-scheme: dark;
/* Surfaces */
--surface-sunken: var(--sea-10);
--surface-base: var(--sea-15);
--surface-raised: var(--sea-20);
--surface-overlay: oklch(0.34 0.022 260);
--surface-card: var(--sea-20);
--surface-input: var(--sea-10);
--surface-scrim: oklch(0.17 0.02 265 / 0.72);
/* Text */
--text-heading: var(--sea-90);
--text-body: var(--sea-75);
--text-muted: var(--sea-60);
--text-faint: var(--sea-50);
--text-inverse: var(--sea-15);
--text-link: var(--blue-bright);
--text-link-hover: var(--sea-90);
/* Borders */
--border-subtle: var(--sea-25);
--border-default: var(--sea-30);
--border-strong: var(--sea-40);
--border-focus: var(--blue-base);
/* Accent (primary = aurora blue) */
--accent: var(--blue-base);
--accent-hover: oklch(0.71 0.11 262);
--accent-active: oklch(0.6 0.13 262);
--accent-bright: var(--blue-bright);
--accent-text: var(--blue-bright);
--accent-contrast: var(--sea-10);
--accent-soft: color-mix(in oklab, var(--blue-base) 16%, transparent);
--accent-soft-hover: color-mix(in oklab, var(--blue-base) 26%, transparent);
/* Secondary accent (aurora cyan) */
--secondary: var(--cyan-base);
--secondary-bright: var(--cyan-bright);
--secondary-text: var(--cyan-bright);
--secondary-soft: color-mix(in oklab, var(--cyan-base) 14%, transparent);
/* Semantic status */
--success: var(--green-base);
--success-text: var(--green-bright);
--success-soft: color-mix(in oklab, var(--green-base) 14%, transparent);
--warning: var(--amber-base);
--warning-text: var(--amber-bright);
--warning-soft: color-mix(in oklab, var(--amber-base) 13%, transparent);
--danger: var(--red-base);
--danger-hover: oklch(0.67 0.15 25);
--danger-text: var(--red-bright);
--danger-contrast: var(--sea-10);
--danger-soft: color-mix(in oklab, var(--red-base) 13%, transparent);
--info: var(--blue-base);
--info-text: var(--blue-bright);
--info-soft: color-mix(in oklab, var(--blue-base) 14%, transparent);
--ai: var(--violet-base);
--ai-text: var(--violet-bright);
--ai-soft: color-mix(in oklab, var(--violet-base) 13%, transparent);
/* Selection */
--selection-bg: var(--blue-base);
--selection-fg: var(--sea-10);
}
/* ============================================================
Semantic aliases — LIGHT ("Skyfall Day")
Same laws, inverted: surfaces at L 0.940.98, text at L 0.270.45,
chromatic fills & colored text drop to the deep (L 0.48) step.
============================================================ */
[data-theme="light"] {
color-scheme: light;
/* Surfaces */
--surface-sunken: var(--sea-94);
--surface-base: var(--sea-96);
--surface-raised: var(--sea-98);
--surface-overlay: #ffffff;
--surface-card: var(--sea-98);
--surface-input: #ffffff;
--surface-scrim: oklch(0.27 0.024 268 / 0.4);
/* Text */
--text-heading: var(--sea-15);
--text-body: oklch(0.38 0.02 255);
--text-muted: var(--sea-50);
--text-faint: var(--sea-60);
--text-inverse: var(--sea-90);
--text-link: var(--blue-deep);
--text-link-hover: var(--sea-15);
/* Borders */
--border-subtle: oklch(0.89 0.014 210);
--border-default: oklch(0.84 0.016 215);
--border-strong: var(--sea-75);
--border-focus: var(--blue-deep);
/* Accent */
--accent: var(--blue-deep);
--accent-hover: oklch(0.53 0.12 262);
--accent-active: oklch(0.44 0.12 262);
--accent-bright: var(--blue-base);
--accent-text: var(--blue-deep);
--accent-contrast: #ffffff;
--accent-soft: color-mix(in oklab, var(--blue-deep) 10%, transparent);
--accent-soft-hover: color-mix(in oklab, var(--blue-deep) 18%, transparent);
/* Secondary accent */
--secondary: var(--cyan-deep);
--secondary-bright: var(--cyan-base);
--secondary-text: var(--cyan-deep);
--secondary-soft: color-mix(in oklab, var(--cyan-deep) 9%, transparent);
/* Semantic status */
--success: var(--green-deep);
--success-text: var(--green-deep);
--success-soft: color-mix(in oklab, var(--green-deep) 10%, transparent);
--warning: var(--amber-deep);
--warning-text: var(--amber-deep);
--warning-soft: color-mix(in oklab, var(--amber-base) 16%, transparent);
--danger: var(--red-deep);
--danger-hover: oklch(0.55 0.16 25);
--danger-text: var(--red-deep);
--danger-contrast: #ffffff;
--danger-soft: color-mix(in oklab, var(--red-deep) 8%, transparent);
--info: var(--blue-deep);
--info-text: var(--blue-deep);
--info-soft: color-mix(in oklab, var(--blue-deep) 9%, transparent);
--ai: var(--violet-deep);
--ai-text: var(--violet-deep);
--ai-soft: color-mix(in oklab, var(--violet-deep) 9%, transparent);
/* Selection */
--selection-bg: var(--blue-base);
--selection-fg: #ffffff;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-72
View File
@@ -1,72 +0,0 @@
/* Australis Skyfall — spacing, radii, shadows, motion (v3 "calm depth") */
:root {
/* Spacing (4px base) */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
--space-24: 96px;
--space-32: 128px;
/* Semantic spacing — airy content, compact chrome */
--pad-chrome: var(--space-2); /* app chrome: rails, toolbars, list rows */
--pad-content: var(--space-8); /* work surfaces: page bodies, card interiors breathe */
--gap-pane: var(--space-4); /* gutter between panes/cards on the canvas */
--measure: 68ch; /* long-form reading width */
/* Radii — moderate, calm */
--radius-xs: 6px;
--radius-sm: 8px;
--radius-md: 10px; /* buttons, inputs, menu items */
--radius-lg: 14px; /* cards, panes, popovers */
--radius-xl: 16px; /* dialogs, drawers, command palette */
--radius-full: 999px;
/* Depth — crisp 1px hairline + layered soft shadow underneath.
Every elevated surface pairs border: 1px solid var(--border-subtle)
with one of these. Shadows are cool-tinted and two-layer:
a tight contact shadow + a wide ambient falloff. */
--shadow-xs: 0 1px 2px oklch(0.13 0.02 265 / 0.22);
--shadow-sm: 0 1px 2px oklch(0.13 0.02 265 / 0.2), 0 2px 8px -2px oklch(0.13 0.02 265 / 0.24);
--shadow-md: 0 2px 4px oklch(0.13 0.02 265 / 0.22), 0 10px 28px -6px oklch(0.13 0.02 265 / 0.36);
--shadow-lg: 0 4px 8px oklch(0.13 0.02 265 / 0.26), 0 28px 64px -12px oklch(0.13 0.02 265 / 0.5);
--shadow-drawer: 0 8px 16px oklch(0.13 0.02 265 / 0.28), 0 32px 80px -8px oklch(0.13 0.02 265 / 0.55);
/* Glows — hero moments ONLY: empty states, featured cards, command palette */
--glow-accent: 0 0 72px -16px color-mix(in oklab, var(--blue-base) 42%, transparent);
--glow-secondary: 0 0 72px -16px color-mix(in oklab, var(--cyan-base) 38%, transparent);
--glow-ai: 0 0 72px -16px color-mix(in oklab, var(--violet-base) 40%, transparent);
/* Active rail — left accent bar on active nav / selected items */
--rail-active: inset 2px 0 0 0 var(--accent);
/* Focus ring */
--focus-ring: 0 0 0 2px var(--surface-base), 0 0 0 4px var(--border-focus);
/* Motion — calm, no bounce */
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* @kind other */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* @kind other */
--duration-fast: 120ms; /* @kind other */
--duration-base: 180ms; /* @kind other */
--duration-slow: 300ms; /* @kind other */
--duration-drawer: 260ms; /* @kind other */
}
[data-theme="light"] {
--shadow-xs: 0 1px 2px oklch(0.3 0.02 255 / 0.07);
--shadow-sm: 0 1px 2px oklch(0.3 0.02 255 / 0.06), 0 2px 8px -2px oklch(0.3 0.02 255 / 0.08);
--shadow-md: 0 2px 4px oklch(0.3 0.02 255 / 0.06), 0 10px 28px -6px oklch(0.3 0.02 255 / 0.12);
--shadow-lg: 0 4px 8px oklch(0.3 0.02 255 / 0.07), 0 28px 64px -12px oklch(0.3 0.02 255 / 0.18);
--shadow-drawer: 0 8px 16px oklch(0.3 0.02 255 / 0.08), 0 32px 80px -8px oklch(0.3 0.02 255 / 0.22);
--glow-accent: 0 0 72px -16px color-mix(in oklab, var(--blue-deep) 26%, transparent);
--glow-secondary: 0 0 72px -16px color-mix(in oklab, var(--cyan-deep) 24%, transparent);
--glow-ai: 0 0 72px -16px color-mix(in oklab, var(--violet-deep) 25%, transparent);
}
-357
View File
@@ -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;
}
}
-40
View File
@@ -1,40 +0,0 @@
/* Australis Skyfall — Typography tokens */
:root {
/* Families */
--font-display: "Bespoke Sans", "Avenir Next", "Segoe UI", sans-serif;
--font-body: "Supreme", "Helvetica Neue", "Segoe UI", sans-serif;
--font-mono: "VictorMono Nerd Font", "SF Mono", "Cascadia Code", monospace;
/* Scale (web-app oriented; 14px UI base) */
--text-2xs: 11px;
--text-xs: 12px;
--text-sm: 13px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 22px;
--text-2xl: 28px;
--text-3xl: 36px;
--text-4xl: 48px;
--text-5xl: 64px;
/* Weights */
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--weight-extrabold: 800;
/* Line heights */
--leading-tight: 1.15;
--leading-snug: 1.35;
--leading-normal: 1.55;
--leading-relaxed: 1.7;
/* Tracking */
--tracking-tight: -0.02em;
--tracking-normal: 0;
--tracking-wide: 0.06em;
--tracking-caps: 0.1em;
}