The board was on the Australis TERMINAL palette, which is dark-only by design
("Always dark first. No light mode in this system"). Skyfall is the dual-theme
web derivative of the same science, and its bundle turned out to be sitting in
this repo's own git history: a predecessor vendored it on 2026-08-19 and a
later commit deleted it. `git show 45c1995:...` returns colors.css with both
`:root` (dark) and `[data-theme="light"]` (Skyfall Day) intact, plus the
calm-depth layout tokens, the typography scale and Supreme 400/500/700. So the
light ramp is canonical rather than derived, which was the entire objection to
building one.
The visual language moves with the palette. Depth is now the recipe and not a
choice — every elevated surface carries a 1px hairline AND a two-layer shadow,
never one without the other. Radii move to Skyfall's scale, cards at
--radius-lg. Widget stat values move from the display face to mono, because
Skyfall is explicit that numbers and telemetry are always --font-mono. The
full-width aurora ribbon under the tab bar is gone: Skyfall sanctions exactly
two accent expressions, the active rail and hero-only glows, and a decorative
gradient across the chrome is neither — so the colour it carried now lands on
the active tab as a 2px accent bar plus an --accent-soft fill, which is the
rail. Every binding is written against the semantic layer; there are no raw
family tokens and no colour literals left in our own file.
build.py now guards the vendoring instead of advising it. The three token files
are hashed and a mismatch FAILS the build — a vendored file is either
byte-identical to the bundle or it is a fork wearing the bundle's name, and the
theme this one replaces had to be torn out twice for exactly that.
⚠ Homepage's own theme toggle is unreachable, and reaching for it breaks the
dashboard. It renders only when settings.yaml leaves `theme:` unpinned, and
with the key absent the page's data loader throws and its catch branch serves
`initialSettings: {}` — no tab bar, no layout, no i18n. Six force-recreates
over seven minutes all came up empty; restoring `theme: dark` rendered
correctly on the next recreate in 12 seconds, while /api/services returned 200
with fully correct content the whole time. That is the first confirmed cause of
the long-running "tab bar goes missing after a recreate" symptom, and it also
retires the homepage.log-size lead recorded earlier today: rolling the log
aside did nothing during this episode, so that coincidence was intermittency.
So the toggle is ours. conf/custom.js renders it and stores the choice;
build.py re-emits each vendored light block twice, once for an explicit
`data-theme` and once inside a prefers-color-scheme media query scoped to
`html:not([data-theme="dark"]):not([data-theme="light"])` — that :not() pair is
what lets a stored dark choice survive a light-mode OS. Verified against both
OS preferences: load, click, click back, reload, all four correct. `data-theme`
is the control surface; Homepage's own `dark` class stays on <html> and does
not fight, because our rules carry !important on the surfaces Tailwind's
`dark:` variants would otherwise claim.
Two font substitutions, both documented rather than silent: Space Grotesk for
Bespoke Sans and JetBrains Mono for Victor Mono. Only Supreme was ever vendored
here and Skyfall's own notes call Victor Mono user-supplied, so this is a
two-line swap when the real faces arrive.
Dark and light, all four tabs: http://10.100.10.50:8090/b/homepage-skyfall/
719 lines
28 KiB
Plaintext
719 lines
28 KiB
Plaintext
/* Homepage — Australis Skyfall, dual theme.
|
|
*
|
|
* SOURCE OF TRUTH: this file plus the three VENDORED token files beside it.
|
|
* 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`.
|
|
*
|
|
* ── WHAT IS VENDORED, AND WHY IT IS UNTOUCHABLE ──────────────────────────
|
|
* colors.css Skyfall v2 palette — Sea/Ice/Aurora/Dawn in OKLCH, plus
|
|
* the semantic layer for BOTH themes.
|
|
* layout.css v3 "calm depth" — spacing, radii, the shadow system,
|
|
* glows, the active rail, motion.
|
|
* typography.css families, scale, weights, leading, tracking.
|
|
*
|
|
* These three are copied VERBATIM from the Skyfall bundle and must stay that
|
|
* way. `build.py` hashes them and fails the build if a byte moves. Everything
|
|
* this dashboard needs on top of them lives in THIS file, expressed through
|
|
* the semantic layer — never against a raw family token, and never a literal
|
|
* colour. That rule is not fussiness: the theme this one replaces built its
|
|
* own parallel palette "derived from the philosophy", which is how a design
|
|
* system quietly forks, and it had to be torn out twice.
|
|
*
|
|
* ── DUAL THEME, AND THE ONE PIECE OF PLUMBING IT NEEDS ───────────────────
|
|
* Skyfall keys light mode off `[data-theme="light"]`. Homepage keys it off a
|
|
* `light` / `dark` CLASS on <html>, and does not touch data attributes. Rather
|
|
* than edit the vendored file (fork) or duplicate its light block by hand
|
|
* (drift), `build.py` rewrites the selector at build time so both spellings
|
|
* are honoured. See BRIDGE_SELECTOR there.
|
|
*
|
|
* Dark is `:root` and remains first-class. Light is Skyfall Day. Both are
|
|
* reachable from Homepage's own theme toggle, which appears because
|
|
* settings.yaml no longer pins `theme:`.
|
|
*
|
|
* ── TYPE: TWO DOCUMENTED SUBSTITUTIONS ───────────────────────────────────
|
|
* Skyfall names Bespoke Sans (display) / Supreme (body, UI) / Victor Mono
|
|
* Nerd Font (data, code). Only Supreme was ever vendored into this repo, and
|
|
* Skyfall's own notes call Victor Mono "user-supplied", so substituting is
|
|
* expected rather than a deviation:
|
|
* Supreme 400/500/700 — CANONICAL, the body and UI face.
|
|
* Space Grotesk variable — stands in for Bespoke Sans (display).
|
|
* JetBrains Mono variable — stands in for Victor Mono (data, metadata).
|
|
* Both stand-ins are neutral grotesques in the right register. Swap them the
|
|
* moment the real faces arrive: it is a two-line change in build.py's FONTS
|
|
* and the --font-display / --font-mono overrides below.
|
|
*
|
|
* ── WHERE THIS DEPARTS FROM THE PREVIOUS THEME, DELIBERATELY ─────────────
|
|
* 1. Depth is now the RECIPE, not a choice: every elevated surface carries a
|
|
* 1px hairline AND a two-layer shadow, never one without the other.
|
|
* 2. Radii move to Skyfall's scale — cards at --radius-lg (14px), controls at
|
|
* --radius-md (10px).
|
|
* 3. The full-width aurora ribbon under the tab bar is GONE. Skyfall sanctions
|
|
* exactly two accent expressions — the active rail and hero-only glows —
|
|
* and a decorative gradient across the chrome is neither. The colour it was
|
|
* carrying now lands where canon puts it: a 2px accent bar plus an
|
|
* --accent-soft fill on the active tab. If you want the ribbon back, that
|
|
* is a deliberate documented deviation, not an oversight.
|
|
* 4. Widget stat values move from the display face to mono. Skyfall is
|
|
* explicit: numbers and telemetry are always --font-mono.
|
|
*/
|
|
|
|
/* @@FONTS@@ */
|
|
|
|
/* ---- Family substitutions ------------------------------------------------
|
|
Overriding typography.css's declarations rather than editing it, so the
|
|
vendored file stays byte-identical and the swap is visible in one place. */
|
|
:root {
|
|
--font-display: "Space Grotesk", "Bespoke Sans", ui-sans-serif, sans-serif;
|
|
--font-body: "Supreme", ui-sans-serif, system-ui, sans-serif;
|
|
--font-mono: "JetBrains Mono", "VictorMono Nerd Font", ui-monospace, monospace;
|
|
|
|
/* Width the status cluster occupies in a card's top-right. Load-bearing in
|
|
two places that must agree: the inset reserved on the title line, and the
|
|
negative margin the description uses to opt back out of it. Sized for the
|
|
widest real cluster — a latency badge plus a status word. */
|
|
--status-gutter: 96px;
|
|
}
|
|
|
|
/* ---- Icon colour ---------------------------------------------------------
|
|
Homepage draws every mdi and simple-icons glyph as a MASK over a gradient
|
|
fill, so this ramp IS the icon colour — `color:` does nothing to them.
|
|
|
|
⚠ These must be space-separated sRGB CHANNELS, because Homepage consumes
|
|
them as `rgb(var(--color-logo-start))`. An oklch() token cannot be fed
|
|
through that, so the four values below are the exact sRGB conversions of
|
|
real Skyfall tokens rather than colours anyone picked by eye:
|
|
dark --sea-80 oklch(0.83 0.017 205) -> 187 203 204
|
|
--blue-base oklch(0.66 0.12 262) -> 105 145 220
|
|
light --sea-40 oklch(0.48 0.016 245) -> 86 95 102
|
|
--blue-deep oklch(0.48 0.12 262) -> 54 91 161
|
|
Recompute with theme/oklch_to_srgb.py if a token ever changes.
|
|
Light uses the deep (L 0.48) step for both stops, per Skyfall's lightness
|
|
law: deep is the light-theme value for fills and coloured marks.
|
|
|
|
⚠ NOT `:root`. Homepage sets these same two variables on `.theme-slate`,
|
|
and that class sits on <html> — the very element `:root` matches. Same
|
|
element, so the cascade decides, and `.theme-slate` (0,1,0) outranks
|
|
`:root` (0,0,1): a `:root` override here is silently ignored. `html[class]`
|
|
is (0,1,1) and wins without hard-coding which `theme-*` class is active.
|
|
Specificity alone is not enough either — a custom property resolves from
|
|
the NEAREST ancestor that sets it, so this has to land on <html>. */
|
|
html[class] {
|
|
--color-logo-start: 187 203 204;
|
|
--color-logo-stop: 105 145 220;
|
|
}
|
|
|
|
html[data-theme="light"],
|
|
html.light {
|
|
--color-logo-start: 86 95 102;
|
|
--color-logo-stop: 54 91 161;
|
|
}
|
|
|
|
/* ---- Canvas --------------------------------------------------------------
|
|
The app canvas is --surface-sunken and work floats on it as cards. Flat
|
|
semantic fill only: Skyfall forbids full-bleed imagery and decorative
|
|
gradients behind text.
|
|
|
|
`#background` 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. Enforcing it here is deterministic and immediate,
|
|
and keeps the canvas correct if the setting is ever re-added by accident. */
|
|
html,
|
|
body,
|
|
#page_wrapper,
|
|
#background,
|
|
body > div {
|
|
background-color: var(--surface-sunken) !important;
|
|
background-image: none !important;
|
|
}
|
|
|
|
body {
|
|
font-family: var(--font-body) !important;
|
|
font-size: var(--text-base);
|
|
line-height: var(--leading-normal);
|
|
color: var(--text-body);
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
/* ---- Group heading — the Skyfall eyebrow ---------------------------------
|
|
Mono, uppercase, --text-2xs, --tracking-caps, in the secondary accent, with
|
|
a hairline running to the right edge so groups read as register bands
|
|
rather than titles floating over a grid. ALL-CAPS is sanctioned here
|
|
specifically: Skyfall allows it for tiny tracked eyebrow labels and table
|
|
headers, and nowhere else.
|
|
|
|
Bookmark groups get the identical treatment. They are a separate component
|
|
with a parallel class family, and none of these selectors matched it before
|
|
— so UltraSeedbox rendered with Homepage's stock 20px sans heading while
|
|
every service group above it wore the eyebrow. One un-themed heading in a
|
|
column of themed ones is more conspicuous than the whole page being
|
|
un-themed. */
|
|
.services-group > button.group,
|
|
.bookmark-group > button.group {
|
|
gap: 0;
|
|
margin-bottom: var(--space-1);
|
|
margin-top: var(--space-6);
|
|
}
|
|
|
|
/* …except the first group on a tab, which should sit up against the tab bar
|
|
rather than floating below it. */
|
|
.services-group:first-of-type > button.group {
|
|
margin-top: var(--space-2);
|
|
}
|
|
|
|
.service-group-icon,
|
|
.bookmark-group-icon {
|
|
display: none !important;
|
|
}
|
|
|
|
h2.service-group-name,
|
|
h2.bookmark-group-name {
|
|
font-family: var(--font-mono) !important;
|
|
font-size: var(--text-2xs) !important;
|
|
font-weight: var(--weight-medium) !important;
|
|
line-height: 1 !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: var(--tracking-caps);
|
|
color: var(--secondary-text) !important;
|
|
white-space: nowrap;
|
|
display: flex;
|
|
}
|
|
|
|
.services-group > button.group::after,
|
|
.bookmark-group > button.group::after {
|
|
content: "";
|
|
flex: 1;
|
|
height: 1px;
|
|
margin-left: var(--space-3);
|
|
background: var(--border-subtle);
|
|
}
|
|
|
|
/* The chevron sits after the rule, muted. */
|
|
.services-group > button.group svg,
|
|
.bookmark-group > button.group svg {
|
|
color: var(--text-faint);
|
|
margin-left: var(--space-2);
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
/* ---- Service cards — the calm-depth recipe -------------------------------
|
|
HAIRLINE + TWO-LAYER SHADOW, NEVER ONE WITHOUT THE OTHER. That pairing is
|
|
Skyfall's defining move; a card with a border and no shadow, or a shadow
|
|
and no border, is off-system. Height stays content-driven — equal-height
|
|
stretching turns short cards into hollow boxes. */
|
|
ul.services-list {
|
|
align-items: start !important;
|
|
row-gap: var(--space-2);
|
|
}
|
|
|
|
li.service > .service-card {
|
|
height: auto !important;
|
|
background: var(--surface-card) !important;
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: var(--radius-lg) !important;
|
|
box-shadow: var(--shadow-sm) !important;
|
|
margin-bottom: 0 !important;
|
|
transition: background-color var(--duration-fast) var(--ease-out),
|
|
border-color var(--duration-fast) var(--ease-out),
|
|
box-shadow var(--duration-fast) var(--ease-out);
|
|
}
|
|
|
|
/* Hover: surface lifts a step and the shadow deepens sm -> md, per canon.
|
|
No scale, no translate — Skyfall has no bounce anywhere. */
|
|
li.service > .service-card:hover {
|
|
background: var(--surface-raised) !important;
|
|
border-color: var(--border-default);
|
|
box-shadow: var(--shadow-md) !important;
|
|
}
|
|
|
|
/* Service name — display face, tight. Description — body face, muted.
|
|
*
|
|
* ⚠ `.service-name` IS NOT THE TITLE. Homepage puts the title in as a bare
|
|
* text node and then nests `<p class="service-description">` as a sibling
|
|
* inside the SAME div, so every rule here lands on a box containing both. The
|
|
* title has no element of its own and cannot be selected — which is why it
|
|
* cannot be given its own truncation, and why the description needs an
|
|
* explicit escape hatch from anything the title needs. */
|
|
.service-name {
|
|
font-family: var(--font-display) !important;
|
|
font-size: var(--text-base) !important;
|
|
font-weight: var(--weight-medium) !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: var(--tracking-tight);
|
|
color: var(--text-heading) !important;
|
|
|
|
/* THE GUTTER MUST BE HELD BY WRAPPING, NOT BY CLIPPING.
|
|
This rule used to read `padding-right: 78px; overflow: hidden;
|
|
text-overflow: ellipsis; white-space: nowrap` and it did not work: CSS
|
|
clips overflow at the PADDING box, not the content box, so the reserved
|
|
78px was spill room the title printed straight through. Six cards on the
|
|
AI tab rendered their name underneath their own status pill. The ellipsis
|
|
never appeared either: it is painted by the block whose own line
|
|
overflows, and that block here is the anonymous box around the text node,
|
|
which does not carry the `overflow`. Wrapping has neither problem — line
|
|
breaking respects the content box, so the gutter genuinely holds. */
|
|
padding-right: var(--status-gutter) !important;
|
|
white-space: normal;
|
|
overflow: visible;
|
|
}
|
|
|
|
p.service-description {
|
|
font-family: var(--font-body) !important;
|
|
font-size: var(--text-xs) !important;
|
|
font-weight: var(--weight-regular) !important;
|
|
line-height: 17px !important;
|
|
color: var(--text-muted) !important;
|
|
margin-top: 3px;
|
|
white-space: normal;
|
|
|
|
/* Opt back out of the title's gutter. The status cluster is absolutely
|
|
positioned over the FIRST line only (top: 13px, height: 17px, so it
|
|
occupies y 13-30); the description never starts above y 33. Nothing can
|
|
collide, so it takes the full card width back and the gutter costs no
|
|
description space at all. */
|
|
margin-right: calc(var(--space-2) - var(--status-gutter));
|
|
|
|
/* Floor of two lines, ceiling of three. The floor keeps the common
|
|
one-line/two-line mix bottom-aligned, which is most of what made rows
|
|
look ragged. The ceiling is the other half: a handful of four- and
|
|
five-line descriptions were dragging their entire row 30-50px taller than
|
|
every card beside them, and at four cards across that gap is the most
|
|
visible defect on the page. Not equal-heights — that inflates short cards
|
|
into hollow boxes to match a widget card twice their height. */
|
|
min-height: 34px;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ---- Status — semantic dots, not chips -----------------------------------
|
|
The stock filled emerald pills read louder than the service names they
|
|
annotate. A semantic dot plus a mono micro-label at faint contrast: present
|
|
when you look for it, silent when you are not. The dot colours come from
|
|
--success/--danger/--warning-text, so they take the deep step automatically
|
|
in Skyfall Day rather than staying dark-theme bright on a white card.
|
|
|
|
The 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.
|
|
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: var(--space-3) !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: var(--weight-medium) !important;
|
|
line-height: 1 !important;
|
|
letter-spacing: var(--tracking-caps);
|
|
color: var(--text-muted) !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(--text-faint);
|
|
}
|
|
|
|
/* ⚠ Homepage emits `docker-status-<state>`, not `status-<state>`. Selectors
|
|
matching the latter silently hit nothing — which is how an earlier pass
|
|
shipped "themed" status pills that were still stock green. */
|
|
.docker-status-healthy > div::before,
|
|
.docker-status-running > div::before {
|
|
color: var(--success-text);
|
|
}
|
|
|
|
.docker-status-unhealthy > div::before,
|
|
.docker-status-exited > div::before,
|
|
.docker-status-dead > div::before {
|
|
color: var(--danger-text);
|
|
}
|
|
|
|
.docker-status-starting > div::before,
|
|
.docker-status-paused > div::before {
|
|
color: var(--warning-text);
|
|
}
|
|
|
|
/* Ping / latency — mono metadata, tabular, no chip. Deliberately the info
|
|
family and not a status colour: "how fast" must not read as "is it alive". */
|
|
.site-monitor-status > div {
|
|
font-family: var(--font-mono) !important;
|
|
font-size: 9px !important;
|
|
font-weight: var(--weight-regular) !important;
|
|
letter-spacing: var(--tracking-wide);
|
|
text-transform: lowercase !important;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--text-faint) !important;
|
|
}
|
|
|
|
/* ---- Widget stat strip ---------------------------------------------------
|
|
A footer strip inside the card: hairline above, cells divided by hairlines,
|
|
value then a mono eyebrow label. The card stays one object instead of three
|
|
nested boxes.
|
|
|
|
Values are MONO, not the display face. Skyfall is explicit that numbers and
|
|
telemetry always take --font-mono; hierarchy against the label comes from
|
|
weight and size, which is the same tool the type system uses everywhere
|
|
else. Padding is tight because a stat strip adds its full height to its own
|
|
card and to nothing else in the row — at four cards across, one AdGuard
|
|
card opens a void beside three ordinary ones. It cannot be made free, but
|
|
it can be made small. */
|
|
.service-container {
|
|
margin-top: var(--space-2);
|
|
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: 6px 4px 5px !important;
|
|
border-left: 1px solid var(--border-subtle);
|
|
min-width: 0; /* let a long value shrink the cell instead of overflowing */
|
|
}
|
|
|
|
.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-mono) !important;
|
|
font-size: var(--text-sm) !important;
|
|
font-weight: var(--weight-bold) !important;
|
|
letter-spacing: var(--tracking-tight);
|
|
line-height: 1.2 !important;
|
|
white-space: nowrap;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--text-heading) !important;
|
|
}
|
|
|
|
/* label */
|
|
.service-container > div > div.uppercase {
|
|
font-family: var(--font-mono) !important;
|
|
font-size: 9px !important;
|
|
font-weight: var(--weight-regular) !important;
|
|
letter-spacing: var(--tracking-caps);
|
|
color: var(--text-muted) !important;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* Trailing widget rows — Jellyfin's "No Active Streams" strip and its empty
|
|
second row, which Homepage emits AFTER .service-container as its own block
|
|
of Tailwind-tinted pills. Untouched they broke out of the card's language:
|
|
two light grey bars with their own radius hanging under an otherwise
|
|
hairline-ruled footer. Not hidden — "no active streams" is the answer to
|
|
the question the card exists to answer. */
|
|
li.service .service-container ~ div > div {
|
|
background: transparent !important;
|
|
border-radius: 0 !important;
|
|
border-top: 1px solid var(--border-subtle);
|
|
height: auto !important;
|
|
min-height: 16px;
|
|
margin-top: 0 !important;
|
|
}
|
|
|
|
li.service .service-container ~ div span {
|
|
position: static !important;
|
|
display: block;
|
|
padding: 3px 4px 2px !important;
|
|
margin: 0 !important;
|
|
font-family: var(--font-mono) !important;
|
|
font-size: 9px !important;
|
|
letter-spacing: var(--tracking-caps);
|
|
text-transform: uppercase;
|
|
color: var(--text-muted) !important;
|
|
}
|
|
|
|
/* ---- Bookmarks -----------------------------------------------------------
|
|
Same depth recipe as a service card. The name takes the display face and
|
|
the description drops to mono metadata, so a bookmark row reads as a
|
|
quieter sibling of a service card rather than a different widget that
|
|
wandered in. */
|
|
li.bookmark > a {
|
|
background: var(--surface-card) !important;
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: var(--radius-lg) !important;
|
|
box-shadow: var(--shadow-sm) !important;
|
|
margin-bottom: var(--space-2) !important;
|
|
overflow: hidden;
|
|
transition: background-color var(--duration-fast) var(--ease-out),
|
|
border-color var(--duration-fast) var(--ease-out),
|
|
box-shadow var(--duration-fast) var(--ease-out);
|
|
}
|
|
|
|
li.bookmark > a:hover {
|
|
background: var(--surface-raised) !important;
|
|
border-color: var(--border-default);
|
|
box-shadow: var(--shadow-md) !important;
|
|
}
|
|
|
|
.bookmark-icon {
|
|
background: transparent !important;
|
|
border-radius: 0 !important;
|
|
border-right: 1px solid var(--border-subtle);
|
|
}
|
|
|
|
.bookmark-name {
|
|
font-family: var(--font-display) !important;
|
|
font-size: var(--text-sm) !important;
|
|
font-weight: var(--weight-medium) !important;
|
|
color: var(--text-heading) !important;
|
|
}
|
|
|
|
.bookmark-description {
|
|
font-family: var(--font-mono) !important;
|
|
font-size: 10px !important;
|
|
letter-spacing: var(--tracking-wide);
|
|
color: var(--text-muted) !important;
|
|
}
|
|
|
|
/* ---- Tab bar — the active rail -------------------------------------------
|
|
Skyfall sanctions exactly two accent expressions: the active rail (a 2px
|
|
accent bar on the active item, paired with an --accent-soft fill) and
|
|
hero-only glows. This is the rail, turned through 90° for a horizontal bar.
|
|
|
|
The previous theme ran a full-width blue -> cyan -> green aurora gradient
|
|
under this bar. It was the nicest thing on the page and it is gone on
|
|
purpose: a decorative gradient across the chrome is neither of the two
|
|
sanctioned expressions, and Skyfall permits the aurora motif only on
|
|
hero/empty-state areas, never behind body text. A plain hairline separates
|
|
chrome from content instead, and the colour lands on the active tab. */
|
|
ul.sm\:flex {
|
|
position: relative;
|
|
background: transparent !important;
|
|
background-color: transparent !important;
|
|
border-radius: 0 !important;
|
|
gap: 0;
|
|
overflow: visible;
|
|
}
|
|
|
|
ul.sm\:flex::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
height: 1px;
|
|
background: var(--border-subtle);
|
|
}
|
|
|
|
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: var(--text-2xs) !important;
|
|
font-weight: var(--weight-medium) !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: var(--tracking-caps);
|
|
color: var(--text-muted) !important;
|
|
transition: color var(--duration-fast) var(--ease-out),
|
|
background-color var(--duration-fast) var(--ease-out);
|
|
}
|
|
|
|
ul.sm\:flex > li > button:hover {
|
|
color: var(--text-body) !important;
|
|
background: var(--accent-soft) !important;
|
|
}
|
|
|
|
/* The active rail: 2px accent bar + --accent-soft fill. `dark:bg-white/10` is
|
|
the class Homepage puts on the selected tab; it is the only handle it
|
|
gives. */
|
|
ul.sm\:flex > li > button.dark\:bg-white\/10 {
|
|
border-top-color: var(--accent) !important;
|
|
color: var(--text-heading) !important;
|
|
background: var(--accent-soft) !important;
|
|
}
|
|
|
|
/* ---- Header chrome: resource widget + search -----------------------------
|
|
Same depth recipe as everything else, so the header belongs to the page
|
|
instead of hovering above it. --pad-chrome, not --pad-content: this is app
|
|
chrome, and Skyfall's negative-space rule is compact chrome / airy content. */
|
|
div.flex.flex-col.justify-center.mt-2,
|
|
div.flex.flex-row.self-center.flex-wrap.justify-between {
|
|
background: var(--surface-card) !important;
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: var(--radius-lg) !important;
|
|
box-shadow: var(--shadow-sm) !important;
|
|
}
|
|
|
|
/* The resource widget is INSIDE that strip, so it must not repeat the recipe
|
|
— nested hairline-and-shadow reads as a box in a box. */
|
|
.information-widget-resource {
|
|
border: 0 !important;
|
|
border-radius: 0 !important;
|
|
box-shadow: none !important;
|
|
background: transparent !important;
|
|
}
|
|
|
|
/* The CPU / memory / disk glyphs. Homepage colours them from its own Tailwind
|
|
theme, which lands almost invisible on Skyfall Day's near-white chrome. */
|
|
.information-widget-resource svg {
|
|
color: var(--text-muted) !important;
|
|
opacity: 1 !important;
|
|
}
|
|
|
|
.information-widget-resource .text-xs,
|
|
.information-widget-resource span {
|
|
font-family: var(--font-mono) !important;
|
|
font-size: var(--text-2xs) !important;
|
|
letter-spacing: var(--tracking-wide);
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--text-muted) !important;
|
|
}
|
|
|
|
.resource-usage,
|
|
[class*="resource-usage"] {
|
|
background: var(--surface-input) !important;
|
|
height: 3px !important;
|
|
border-radius: var(--radius-full) !important;
|
|
}
|
|
|
|
.resource-usage > div,
|
|
[class*="resource-usage"] > div {
|
|
background: var(--secondary) !important;
|
|
border-radius: var(--radius-full) !important;
|
|
}
|
|
|
|
/* Search input */
|
|
input[type="text"],
|
|
input[type="search"] {
|
|
font-family: var(--font-body) !important;
|
|
font-size: var(--text-base) !important;
|
|
color: var(--text-body) !important;
|
|
background: transparent !important;
|
|
}
|
|
|
|
input::placeholder {
|
|
color: var(--text-faint) !important;
|
|
font-family: var(--font-mono) !important;
|
|
font-size: var(--text-xs) !important;
|
|
letter-spacing: var(--tracking-wide);
|
|
}
|
|
|
|
/* ---- Theme toggle --------------------------------------------------------
|
|
Our own switch, injected by conf/custom.js. Homepage's built-in one is
|
|
unavailable: rendering it needs `theme:` unpinned in settings.yaml, and an
|
|
unpinned `theme:` makes the page's data loader throw — see the note in
|
|
custom.js for the measurement. Sized and shaped like an IconButton from the
|
|
Skyfall component set: --radius-md, chrome padding, hairline, and the same
|
|
accent-soft hover every interactive surface uses. */
|
|
#skyfall-theme-toggle {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 30px;
|
|
height: 30px;
|
|
margin-left: auto;
|
|
align-self: center;
|
|
flex: 0 0 auto;
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: var(--radius-md);
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
transition: color var(--duration-fast) var(--ease-out),
|
|
background-color var(--duration-fast) var(--ease-out),
|
|
border-color var(--duration-fast) var(--ease-out);
|
|
}
|
|
|
|
#skyfall-theme-toggle:hover {
|
|
color: var(--text-heading);
|
|
background: var(--accent-soft);
|
|
border-color: var(--border-default);
|
|
}
|
|
|
|
#skyfall-theme-toggle:active {
|
|
background: var(--accent-soft-hover);
|
|
}
|
|
|
|
/* ---- Focus — Skyfall's ring ---------------------------------------------- */
|
|
a:focus-visible,
|
|
button:focus-visible,
|
|
input:focus-visible {
|
|
outline: none !important;
|
|
box-shadow: var(--focus-ring) !important;
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
/* ---- Version / footer ---------------------------------------------------- */
|
|
#version,
|
|
#version * {
|
|
font-family: var(--font-mono) !important;
|
|
font-size: var(--text-2xs) !important;
|
|
color: var(--text-faint) !important;
|
|
}
|
|
|
|
/* ---- Reduced motion — canon requires honouring it ------------------------ */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
transition-duration: 0.01ms !important;
|
|
animation-duration: 0.01ms !important;
|
|
}
|
|
}
|