feat(homepage): Australis Skyfall theme + Arbo-generated aurora background
Replaces the previous theme attempt, which was built on a misread: the ask was to use Arbo as an IMAGE-GEN ENGINE for the background, with the operator's Australis Skyfall design system supplying the palette. theme/ holds the source — colors/layout/typography vendored verbatim from the Skyfall handoff bundle, Supreme 400/500/700 woff2, the Homepage bindings in skyfall.css.in, and build.py which inlines fonts + tokens into conf/custom.css. custom.css is GENERATED; edit the .in file and rebuild. The build exists because Homepage serves only custom.css and custom.js out of its config dir, so a @font-face pointing at a vendored woff2 would 404 — the face has to arrive as a data: URI. The background image takes the other route: /app/public/images is a real static route, so compose.yaml now mounts images/ there read-only and settings.yaml points at /images/. Bindings map Skyfall's semantic layer onto Homepage's DOM: Sea surfaces, the depth recipe (hairline AND two-layer shadow, never one alone), uppercase eyebrow group headers, the sanctioned accent-rail on the active tab rather than a glow, and semantic status colour so a green pill means the service is actually serving. Background generated by Arbo (irv-ml1:8201) workflow t2i-ui-background, job 13f0891f4e42, seed 26, flux2-klein-9b, 2048x1152 — abstract, no subject, cool-temperature aurora. 1.6 MB PNG -> 22 KB WebP. Two deviations are documented rather than hidden: Skyfall forbids imagery behind body text (held at opacity 30 as mitigation), and service icons stay full-colour vendor logos. NOT DEPLOYED — live still runs the old theme. Prototype on :5199.
This commit is contained in:
+69
-13
@@ -146,22 +146,78 @@ render are different questions.
|
||||
Before/after evidence: `~/booth-data/homepage-cleanup/` on nh3-dev →
|
||||
`http://10.100.10.50:8090/b/homepage-cleanup/` (24h TTL).
|
||||
|
||||
## Theme — the Arbo "Raven" palette (`conf/custom.css`)
|
||||
## Theme — Australis Skyfall
|
||||
|
||||
`conf/custom.css` carries a port of Arbo's design tokens (irv-ml1:8201): flat
|
||||
raven ink `#021425`, card surface `#112333` on `#1B2E3D` borders, Manrope, and
|
||||
mint `#2FFC89` reserved for signal — a status pill is green only when the
|
||||
service is actually serving. Values were read off Arbo's running `:root`
|
||||
custom properties, not sampled from a screenshot, so drift checks are a diff.
|
||||
The dashboard is themed with **Australis Skyfall**, the operator-supplied
|
||||
design system (handoff bundle, 2026-08-18). Skyfall is a dual-theme web system
|
||||
built on the OKLCH rebuild of the Australis terminal palette: one lightness law
|
||||
across every chromatic family, all hues cooler than neutral, a Sea neutral ramp
|
||||
that drifts from ice-blue to ocean green as it brightens, and a "calm depth"
|
||||
language of hairline + two-layer shadow on every elevated surface.
|
||||
|
||||
It lives in CSS rather than `settings.yaml` because Homepage's `color:` setting
|
||||
only accepts built-in Tailwind ramps and has no separate surface/border/accent
|
||||
knobs. `color: slate` stays as the base ramp. The `!important` flags are
|
||||
load-bearing against Tailwind utility specificity.
|
||||
### `theme/` is the source; `conf/custom.css` is generated
|
||||
|
||||
Pairs with removing the `background:` block from `settings.yaml` — Arbo has no
|
||||
background image, and the wallpaper fought the card surfaces, which sit only a
|
||||
little lighter than the page.
|
||||
```
|
||||
theme/colors.css vendored VERBATIM from the bundle
|
||||
theme/layout.css ditto — radii, the shadow/glow/rail system, motion
|
||||
theme/typography.css ditto — families, scale, weights, tracking
|
||||
theme/fonts/ Supreme 400/500/700 woff2 (Fontshare ITF license)
|
||||
theme/skyfall.css.in the Homepage bindings — the only part that is ours
|
||||
theme/build.py inlines the fonts + tokens -> conf/custom.css
|
||||
```
|
||||
|
||||
**Do not hand-edit `conf/custom.css`.** Change `skyfall.css.in`, run
|
||||
`python3 stacks/homepage/theme/build.py`, then deploy.
|
||||
|
||||
The build exists for one reason: Homepage serves exactly two files out of its
|
||||
config directory, `custom.css` and `custom.js`. There is no static route for
|
||||
anything beside them, so a `@font-face` pointing at a vendored `.woff2` would
|
||||
404 — the face has to arrive inside the stylesheet as a data: URI. That is also
|
||||
why the background image is mounted at `/app/public/images` instead (see
|
||||
`compose.yaml`), which *is* a real static route.
|
||||
|
||||
Only Supreme (body/UI) is embedded. A link dashboard has no display type, so
|
||||
Bespoke Sans buys nothing; Victor Mono ships as 2.4 MB TTF statics per cut,
|
||||
which is 30x the whole stylesheet for the handful of latency figures on the
|
||||
cards. Both are named in the font stacks so they resolve if vendored later.
|
||||
|
||||
### The background is generated, not stock
|
||||
|
||||
`images/skyfall-aurora.webp` came out of **Arbo** (irv-ml1:8201) via its
|
||||
`t2i-ui-background` workflow — "abstract full-bleed backgrounds, no subject".
|
||||
Job `13f0891f4e42`, seed 26, flux2-klein-9b, 2048x1152, 1.6 MB PNG re-encoded
|
||||
to a 22 KB WebP (smooth gradients compress absurdly well).
|
||||
|
||||
Regenerate with a different seed, or reword, via:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://10.100.79.3:8201/workflows/t2i-ui-background/run \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"prompt":{"kind":"raw","text":"...","negative":"..."},
|
||||
"width":2048,"height":1152,"seed":26}'
|
||||
# poll GET /jobs/<id>, then GET /artifacts/<id>/0
|
||||
```
|
||||
|
||||
Note the prompt field is a discriminated union, not a string — a bare string
|
||||
gets a 422. `{"kind":"raw","text":...}` is the shape.
|
||||
|
||||
### Two deviations from the system, on the record
|
||||
|
||||
1. **Skyfall forbids this background.** Its rule is "flat semantic surfaces; no
|
||||
photography, no textures", with exactly one permitted decorative motif — a
|
||||
subtle aurora gradient, on hero and empty-state areas only, *"never behind
|
||||
body text blocks"*. A dashboard is a body-text block. The background is here
|
||||
on the operator's explicit instruction to generate one with Arbo, and it is
|
||||
mitigated rather than excused: abstract with no subject, strictly cool
|
||||
temperature per the imagery rule, and held at `opacity: 14` so card contrast
|
||||
never pays for it. **That opacity is load-bearing, not decoration** — the
|
||||
render is brighter than the "mostly empty negative space" brief, and turning
|
||||
it up is what would make the cards fight the ribbon. If it ever reads busy,
|
||||
the system-faithful answer is to drop the image and let `--surface-sunken`
|
||||
be the canvas.
|
||||
2. **Service icons stay full-colour vendor logos.** Skyfall wants one cool,
|
||||
technical icon family. Desaturating every logo from CSS would just make them
|
||||
illegible, so this one is simply not fixed.
|
||||
|
||||
## Open question
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@ services:
|
||||
- 5100:3000
|
||||
volumes:
|
||||
- /opt/docker/conf/homepage:/app/config # Make sure your local config directory exists
|
||||
# Background art. Homepage serves ONLY custom.css and custom.js out of
|
||||
# /app/config — there is no static route for anything else in there — so
|
||||
# an image referenced from the config dir 404s. /app/public/images is the
|
||||
# documented drop point; files here are reachable as /images/<name>, which
|
||||
# is what settings.yaml's `background:` points at. Read-only: nothing in
|
||||
# the container should ever write to it.
|
||||
- /opt/docker/compose/homepage/images:/app/public/images:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # optional, for docker integrations
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
|
||||
+515
-104
File diff suppressed because one or more lines are too long
@@ -2,11 +2,32 @@
|
||||
# https://gethomepage.dev/latest/configs/settings
|
||||
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.
|
||||
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.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# ⚠️ 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: https://w.wallhaven.cc/full/p9/wallhaven-p9gr2p.jpg
|
||||
image: /images/skyfall-aurora.webp
|
||||
blur: sm
|
||||
opacity: 20
|
||||
saturate: 90
|
||||
brightness: 90
|
||||
opacity: 30
|
||||
headerStyle: boxedWidgets
|
||||
|
||||
providers:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate conf/custom.css from the Skyfall 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
|
||||
pointing at a vendored .woff2 would therefore 404, so the faces have to be
|
||||
inlined as data: URIs. That is the whole reason this build step exists.
|
||||
|
||||
Usage: python3 stacks/homepage/theme/build.py
|
||||
Then: scripts/deploy-stack.sh esh-docker-vm homepage --conf
|
||||
"""
|
||||
|
||||
import base64
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
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.
|
||||
FONTS = [
|
||||
("Supreme", "Supreme-400.woff2", "400"),
|
||||
("Supreme", "Supreme-500.woff2", "500"),
|
||||
("Supreme", "Supreme-700.woff2", "600 700"), # Supreme ships no 600 cut
|
||||
]
|
||||
|
||||
# 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"]
|
||||
|
||||
|
||||
def font_faces() -> str:
|
||||
out = []
|
||||
for family, filename, weight 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-family: "{family}";\n'
|
||||
f" src: url(data:font/woff2;base64,{b64}) format('woff2');\n"
|
||||
f" font-weight: {weight};\n"
|
||||
f" font-style: normal;\n"
|
||||
f" font-display: swap;\n"
|
||||
f"}}"
|
||||
)
|
||||
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 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")
|
||||
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")
|
||||
OUT.write_text(css)
|
||||
print(f"wrote {OUT} ({len(css) / 1024:.0f} KB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,208 @@
|
||||
/* ============================================================
|
||||
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.
@@ -0,0 +1,72 @@
|
||||
/* 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);
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* ---------- 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;
|
||||
}
|
||||
|
||||
/* ---------- 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-tag,
|
||||
.service-block,
|
||||
.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;
|
||||
}
|
||||
|
||||
.status-online,
|
||||
.status-healthy,
|
||||
.status-running,
|
||||
.status-started {
|
||||
color: var(--success-text) !important;
|
||||
background: var(--success-soft) !important;
|
||||
}
|
||||
|
||||
.status-offline,
|
||||
.status-unhealthy,
|
||||
.status-dead,
|
||||
.status-error {
|
||||
color: var(--danger-text) !important;
|
||||
background: var(--danger-soft) !important;
|
||||
}
|
||||
|
||||
.status-exited,
|
||||
.status-paused,
|
||||
.status-partial,
|
||||
.status-unknown,
|
||||
.status-not-found {
|
||||
color: var(--warning-text) !important;
|
||||
background: var(--warning-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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* 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