# `[2026-08-19]` Homepage cleaned up, then themed with Australis Skyfall + an Arbo-generated background

Commits `9d92c4b`, `c3de7db`, `45c1995`, `f38cf69`, `df68dd2`.

## The cleanup (three real defects)

- **UltraSeedbox rendered on all four tabs.** The bookmark group had no entry in
  `settings.yaml`'s `layout:` block at all, and Homepage's documented behaviour
  is that a group with no `tab:` is shown on **every** tab. Pinned to Main.
  ⚠️ This will happen again to the next group added without a `tab:` — the rule
  is now written at the top of the layout block.
- **Uptime Kuma rendered twice** — a manual `services.yaml` entry under
  Monitoring *and* `homepage.group=Apps` on the container. Exactly the
  "never list a labelled container manually" failure the stack README warns
  about; it survived the previous day's audit because a duplicate reads as two
  plausible cards rather than as an error. Manual block deleted, label moved to
  `Monitoring`, `homepage.siteMonitor` added.
- **Column counts were fiction** — several groups declared more columns than
  they had members, so the last row of each was dead space (Notes: 1 card in a
  4-wide row). Columns now track member counts; `GET /api/services` prints the
  live per-group counts and is the check.

Later, on operator instruction, the **AI tab was reordered by clickability**:
Gateways & Chat → Image & Media → Audio Tools on top, then the vLLM `/docs`
seats and TTS endpoints. Reasoning written into the config so it survives:
order by "would I click this?", not by how central the service is.

## ⚠️ The expensive red herring — the tab bar after a recreate

After a recreate the client render comes up with **no tab bar, no wallpaper and
no i18n** (search box shows the raw key `search.search`), groups falling back to
side-by-side columns. **It restores itself with no intervention.**

Timing, measured rather than assumed: a fresh container was still tab-less at
**4m30s, twice**; it was healthy again after roughly an hour. `docker ps`
reporting `healthy` says nothing about it — the container is serving, the page
is just wrong.

An hour went into ruling out four causes that were never the cause:

1. **Not the config** — restoring `settings.yaml` *and* `services.yaml` to
   their committed versions reproduces it, as does the pre-adoption backup in
   `/opt/docker-bu/conf/homepage/`.
2. **Not the v2.0.0 release** — a throwaway container on `v1.13.2` shows
   identical symptoms, and the image never changed anyway (working and broken
   both report `v2.0.0` / rev `17456f2`).
3. **Not `PUID`/`PGID`**, and not Docker discovery — tested both, and with the
   socket unmounted entirely.
4. **Not server-side** — the server-rendered HTML still contains the tab
   markup, the background URL and `useEqualHeights`; `GET /api/validate`
   returns `[]`. The loss is client-side, with no page error, no failed chunk
   and no non-200.

Every throwaway container in that list was judged within ~30s of starting, so
they were all inside the same window — and that consistency **read as a
reproduction when it was the same measurement mistake five times over.**

**Operative rule: recreate, walk away, re-check later. Do not chase it.**

## ⚠️ The iteration loop that would have prevented the overcook

`custom.css` is served **per request** from `/api/config/custom.css`, so a CSS
change needs a **browser reload** — not a container recreate, and it never owed
the layout warm-up above. Conflating the two costs ~10 operator-visible minutes
per attempt (operator called this out directly).

Faster still, and how the final pass was done: **inject candidate CSS into the
running page and screenshot it** —
`await p.addStyleTag({content: css})` in Playwright against the live
dashboard. Seconds per iteration, no deploy. Build + deploy only once the
render looks right.

## The theme — Australis Skyfall

Operator supplied a Claude Design handoff bundle via the Booth (`26-copper`).
Skyfall is a dual-theme OKLCH system: one lightness law across every chromatic
family (deep 0.48 / base 0.66 / bright 0.80), all hues cooler than neutral, a
Sea neutral ramp drifting ice-blue→ocean-green as it brightens, and a
"calm depth" language of **hairline + two-layer shadow on every elevated
surface, never one without the other**.

```
theme/colors.css layout.css typography.css   vendored VERBATIM from the bundle
theme/fonts/Supreme-{400,500,700}.woff2      the body/UI face
theme/skyfall.css.in                         the Homepage bindings (ours)
theme/build.py            → conf/custom.css  (generated — do not hand-edit)
```

The build step exists for one reason: **Homepage serves only `custom.css` and
`custom.js` out of its config dir**, with no static route beside them, so a
`@font-face` pointing at a vendored `.woff2` would 404 — the face must arrive
as a data URI. The background image takes the other road, because
`/app/public/images` **is** a real static route (mounted read-only in
`compose.yaml`).

Only Supreme is embedded: a link dashboard has no display type, and Victor
Mono ships as 2.4 MB TTF statics per cut — 30x the whole stylesheet for a
handful of latency figures.

## The background is generated, not stock

**Arbo as an image-gen engine** (the operator's actual ask, which I first
misread as "use Arbo's palette" and had to redo). Arbo's `t2i-ui-background`
workflow is purpose-built: *"abstract full-bleed backgrounds, no subject"*.
Job `13f0891f4e42`, seed 26, flux2-klein-9b, 2048×1152, 1.6 MB PNG → **22 KB
WebP** (smooth gradients compress absurdly well).

⚠️ Arbo API gotcha: `prompt` is a **discriminated union, not a string** — a
bare string 422s. `{"kind":"raw","text":…,"negative":…}` is the shape.

## Two documented deviations from the design system

1. **Skyfall forbids this background.** Its rule is "flat semantic surfaces; no
   photography, no textures", with one permitted motif — a subtle aurora
   gradient on hero/empty-state areas only, *"never behind body text blocks"*.
   A dashboard is a body-text block. Present on the operator's explicit
   instruction, mitigated rather than excused: abstract, no subject, strictly
   cool temperature, held at **`opacity: 30`**. That number is load-bearing —
   at 14 the aurora was invisible, and turning it up makes the cards fight the
   ribbon.
2. **Service icons stay full-colour vendor logos.** Desaturating them from CSS
   only makes them illegible.

## Overcorrection, and the colour pass

First stat-well pass went from `font-thin` 13px straight to **bold 22px in
heading white** — operator: *"went from subtle to BASH YOU OVER THE HEAD."*
The principle missed: a stat only has to out-rank **its own label**, not the
service name above it. Now `--text-md` medium in cyan.

Colour was then lifted **from inside the system**: Skyfall names Aurora (blue,
cyan, green) the *primary* families, "used generously, in that order", while
Dawn (amber/red/violet) is semantic-only. So group markers cycle
blue→cyan→green down the page (icons full strength, names at 0.72), service
icons take a single cool wash, latency tags move to the info family so
"how fast" stops looking like "is it alive". **No Dawn colour is used
decoratively anywhere.**

Two DOM findings that made it possible:

- **Homepage renders mdi icons as a gradient behind an SVG mask** — recolour
  via `background`, not `color`.
- **Homepage emits `docker-status-<state>`, not `status-<state>`.** The
  original selectors matched nothing, so every green pill up to that point was
  stock colouring rather than the theme. Both forms are now matched.
