/* 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:
353
<- the VALUE
Movies
<- 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-`, NOT
`status-`. 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;
}
}