/* 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; } }