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
+7
View File
@@ -162,6 +162,13 @@ says so.
40. **`main` is release code, `dev` is development.** Work lands on `dev` and reaches `main` by merge
at release, so `main` is always a list of things that shipped. Nightly builds, if any, come off
`dev`.
42. **The theme handoff is implemented as CSS custom properties, not a Tailwind config.** Its
palette, spacing, shadows, motion and component shapes are followed as written; the parts that
assumed Tailwind, Pico or cover artwork are adapted rather than dropped, and each adaptation is
listed in [theme.md](./theme.md). Oswald's phantom weight 900 resolved to 700 — loading a weight
you do not have is what made the brand render differently per platform. The custom audio player
stays deferred: `color-scheme: dark` makes the native control fit the palette, which was the
actual complaint.
41. **No password minimum; rate limit logins instead.** A length policy protects against guessing,
and guessing is better answered directly: 10 failures per email in 15 minutes, then a 15-minute
lockout, cleared by a correct password. The floor was rejected because typing an 8-character
+21
View File
@@ -130,6 +130,27 @@ Also here: pruning the count-based leaderboards once the queue has drained and t
---
## Review form as a mixer channel
Idea for the UI polish pass, not now: put the score slider and the review textarea **on one row**,
with the slider **vertical** like a channel fader on a mixing desk. The score stops being a form
field and becomes the instrument the app is actually about, and the two things you do at once —
decide a number, write why — stop being stacked a screenful apart.
Notes for whoever builds it:
- A vertical `<input type="range">` is native now: `writing-mode: vertical-lr; direction: rtl` gives
bottom-to-top travel with no JS and no custom widget, so keyboard support and the value stay free.
- Keep the live `<output>` — on a fader it wants to sit at the top of the track, reading like a
channel's gain display.
- The row needs a mobile answer: below ~640px, either keep the fader and shrink the textarea beside
it, or fall back to the current stacked layout. A short vertical fader is worse than a horizontal
one, so measure before choosing.
- Tick marks along the track (1 / 25 / 50 / 75 / 100) replace today's `.scorescale` row, and are
what make it read as equipment rather than decoration.
---
## Filters on the browse list
`/songs` is newest-first with no filters. Once there are a couple of hundred songs, "which ones
+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.