Apply the theme: tokens, Oswald, song cards, toasts

The theme handoff encoded as CSS custom properties rather than a Tailwind
config, since there is no Tailwind here. Palette, spacing, radii, shadows and
motion follow it as written; docs/theme.md lists what differs and why.

- Oswald vendored as a 21KB variable woff2, latin subset, no CDN. Its phantom
  weight 900 resolved to 700 — loading a weight you don't have is what made
  the brand render differently per platform
- color-scheme: dark makes the native audio element fit the palette, which
  was the handoff's complaint about it
- Songs are text cards rather than artwork tiles, because there is no
  artwork. The unreviewed state keeps its red-brown border and gains a badge,
  so it is never carried by colour alone
- Nav is the three-column grid; the mobile menu is <details>, no JS
- Flash messages became bottom-right toasts

Favicon carried over from the Nuxt project.
This commit is contained in:
Esa Kataja
2026-07-31 22:16:29 +03:00
parent 91e136055c
commit f33f4fa4d6
16 changed files with 724 additions and 232 deletions
+53
View File
@@ -0,0 +1,53 @@
# Theme
Dark-only. Rock/metal club poster, not SaaS dashboard: near-black surfaces, warm bronze/amber
accents, condensed uppercase display type, one-tone-lighter surfaces instead of borders everywhere.
Restrained motion — 150 ms, one easing curve, no bounce. **There is no light theme and none is
wanted.**
The tokens themselves live in [`static/style.css`](../static/style.css) as CSS custom properties, and
that file is the source of truth. This page records the decisions behind them and the places the
implementation deliberately differs from the theme handoff it came from.
## Rules
- **Nothing outside `:root` invents a value.** No colour, spacing step, radius or duration appears in
a rule unless it is declared as a token first. Six spacing steps (432 px), one radius (4 px, plus
6 px for toasts and a pill), one duration, one curve.
- **Headings step downward in brightness with level** — h1 lightest gold, h3 the primary bronze.
- **Status colours are desaturated on purpose.** A pure red error would break the palette.
- **`color-scheme: dark`** is set on `:root`, which is what keeps the native `<audio>` element,
checkboxes, range inputs and scrollbars from rendering as white slabs.
- **One focus treatment, everywhere:** a soft gold ring via `box-shadow` on `:focus-visible` only.
Not optional — keyboard navigation is the only way through some admin tables.
- **`prefers-reduced-motion`** drops the card lift and the panel slide, and keeps the fades.
## Type
Body is a system stack; no webfont for body text. Display is **Oswald**, vendored as a variable
`.woff2` (latin subset, 21 KB) in `static/fonts/` — no CDN, matching the no-npm rule for HTMX.
The handoff asked for weight 900 in five places while loading only 400/600/700, so browsers were
synthesising a fake bold that differed per platform. **Resolved as 700 being the top weight.** The
variable font covers 400700 and nothing asks for more.
## The unreviewed state
A song the viewer has not reviewed gets a red-brown border — it is the single most important state
in the app, since it is how you see what still needs a review. It is **never carried by colour
alone**: the card also shows an `arvostelematta` badge.
## Deviations from the handoff
| Handoff said | Here | Why |
|---|---|---|
| Tailwind theme config, utility classes | CSS custom properties, semantic classes | Decisions 4 and 8 — no Tailwind, no bundler, no npm |
| Song cards are 16:9 tiles with cover artwork and a gradient scrim | Text cards, same borders, badges, hover glow and score badge | Songs have no artwork. There is no upload for one and nothing to derive it from |
| Fixed bottom audio player bar | Player inline on the song page | Nothing plays across navigation, so a persistent bar would be an empty bar on every other page |
| A styleguide page rendering every variant | Not built | The real pages cover every component; a second copy of them would drift |
| Custom audio player skin | Native `<audio controls>` | Decision 14. It is keyboard-operable, screen-reader labelled and media-key aware for free, and replacing it later is one template partial — see [later.md](./later.md) |
| Nav dropdown for the user block, hamburger with animated bars | User block is a plain row; mobile menu is `<details>` | No JS for either. The app has no dropdown-worthy menu yet: logout is one button |
Everything else — the palette, spacing, radii, shadows, motion, the 1450 px content width, the
420 px auth column, badges, review cards, the admin section cards, bottom-right toasts — follows the
handoff as written.