Files
esh-pfi-infrastructure/stacks/homepage/theme/australis.css.in
T
vh 23cccf5f53 fix(homepage): force the canvas clear of the cached wallpaper div
Removing the `background:` block from settings.yaml was not sufficient.
Homepage server-renders the wallpaper as an INLINE style on
`<div id="background">` and Next.js caches the rendered page, so the aurora
survived both the config removal and a container restart. Only a full
recreate clears that cache, and recreating this container costs an hour of
missing tab bar and i18n before it heals itself.

Adding `#background` to the canvas reset is deterministic and immediate,
and it also keeps the canvas correct if the setting is ever re-added by
accident. The existing selector missed it: the DOM is
body > div#__next > div#background, so `body > div` matched the Next.js
root, not the wallpaper layer.

Verified live rather than locally: the served page now reports no
background image, with all three canonical faces loaded and the group
eyebrows rendering as JetBrains Mono in Australis cyan.
2026-08-19 09:17:37 -07:00

529 lines
17 KiB
Plaintext

/* 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,
#background,
body > div {
background-color: var(--bg-0) !important;
background-image: none !important;
}
/* `#background` above is load-bearing, not defensive tidiness. Homepage
server-renders the wallpaper as an INLINE style on `<div id="background">`,
and Next.js caches that rendered page — so removing the `background:` block
from settings.yaml is not enough on its own. A restart does not clear it;
only a full recreate does, and recreating this container has its own cost
(the tab bar and i18n go missing for up to an hour and then heal on their
own). Enforcing it in the stylesheet is deterministic, immediate, and also
means the canvas stays correct if the setting is ever re-added by accident.
The deliberate absence of the config block is documented in settings.yaml. */
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);
}