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:
+472
-636
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)")
|
||||
|
||||
|
||||
@@ -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.94–0.98, text at L 0.27–0.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.
Binary file not shown.
Binary file not shown.
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user