Files
Levyraati26_go/static/style.css
T
Esa Kataja 400b5d3833 Release 2026.08.01-1
Lyrics, versioning, and the song page's fact line.

- Lyrics are suggested at submission and never imposed: the worker makes one
  LRCLIB lookup, and Hae sanoitukset re-queries with whatever title and artist
  are typed. Neither overwrites what the submitter wrote. They live on the
  submission, travel to the song at publish, and stay editable after the song
  locks — the lock freezes what a song claims to be, and nobody reviewed the
  lyrics
- The review strip reads and writes side by side: lyrics left, review right.
  Synced LRC highlights the playing line and seeks on click; plain text scrolls
  continuously with a nudge knob. Following can be turned off
- CalVer YYYY.MM.DD-N, injected from the git tag with -ldflags, shown in the
  footer, the startup log and /healthz
- The song page's metadata became four labelled cells instead of one flat run
  of five different kinds of fact

Fixes: publishing wiped lyrics the worker had just fetched (a request that
omitted a field cleared it), lyric auto-scroll landed in the wrong place, and
the fader shifted the deck sideways at score 100.
2026-08-01 00:45:55 +03:00

1087 lines
36 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Levyraati — dark-only rock club poster. Tokens first; nothing below invents a colour, a spacing
step, a radius or a duration that isn't declared here. */
/* Oswald variable, latin subset, vendored — no CDN, and no webfont for body text. 400700 only:
asking for 900 makes the browser synthesise a fake bold that differs per platform. */
@font-face {
font-family: "Oswald";
src: url("/static/fonts/oswald.woff2") format("woff2");
font-weight: 400 700;
font-display: swap;
font-style: normal;
}
:root {
/* Surfaces */
--bg: #111111;
--surface: #1a1a1a;
--surface-raised: #222222;
--surface-header: #1e1e1e;
--bar: #0d0d0d;
--hairline: #2a2a2a;
--divider: #333333;
--input-border: #444444;
--track: #2a2a2a;
/* Text */
--text: #e0e0e0;
--muted: #888888;
--text-strong: #f0f0f0;
/* Accent — bronze/amber, not neon */
--primary: #a0693a;
--primary-hover: #b57848;
--primary-wash: rgba(160, 105, 58, 0.2);
--primary-inverse: #111111;
--gold-1: #c4a96a;
--gold-2: #b89558;
/* Status, deliberately desaturated */
--error: #e05959;
--error-bg: rgba(224, 89, 89, 0.08);
--success: #7db07d;
--success-bg: rgba(125, 176, 125, 0.08);
--pending: #8f4a44;
--pending-bg: #2e2020;
--unreviewed: #7a3a36;
--unreviewed-hover: #a04540;
/* Scale — 4px base, six steps, nothing in between */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--radius: 4px;
--radius-toast: 6px;
--radius-pill: 99px;
--duration-fast: 150ms;
--ease-out: cubic-bezier(0.4, 0, 0.2, 1);
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.03);
--shadow-card-hover: 0 4px 16px rgba(0, 0, 0, 0.5), 0 0 24px rgba(180, 120, 72, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
--shadow-focus: 0 0 0 3px rgba(196, 169, 106, 0.35);
/* Native controls (audio, checkbox, range, scrollbars) follow this — it is what stops the
<audio> element rendering as a white slab against the dark page. */
color-scheme: dark;
--content: 1450px;
--font-display: "Oswald", Impact, system-ui, sans-serif;
--font-body: system-ui, -apple-system, "Segoe UI", sans-serif;
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
line-height: 1.5;
}
/* One focus treatment everywhere, on :focus-visible only. Keyboard navigation is the only way
through some admin tables — this ring is not optional. */
a:focus-visible, button:focus-visible, input:focus-visible,
textarea:focus-visible, select:focus-visible, summary:focus-visible,
[role="button"]:focus-visible {
outline: none;
box-shadow: var(--shadow-focus);
border-radius: var(--radius);
}
h1, h2, h3 {
font-family: var(--font-display);
font-weight: 700;
letter-spacing: -0.01em;
margin: 0 0 var(--space-3);
}
/* Headings step downward in brightness with level. */
h1 { font-size: 2rem; color: var(--gold-1); }
h2 { font-size: 1.5rem; color: var(--gold-2); }
h3 { font-size: 1.2rem; color: var(--primary); }
h4 { font-family: var(--font-body); font-weight: 800; letter-spacing: -0.02em; }
a { color: var(--primary); text-decoration-color: var(--primary-wash); }
a:hover { color: var(--primary-hover); }
/* --- top bar --- */
header.topbar {
background: var(--bar);
border-bottom: 2px solid var(--divider);
padding: var(--space-3) var(--space-5);
position: relative;
}
/* Three columns, not space-between: this is what keeps the links optically centred whatever the
brand and user block weigh. */
.topbar-inner {
max-width: var(--content);
margin: 0 auto;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: var(--space-4);
}
.brand {
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 700;
letter-spacing: -0.03em;
text-transform: uppercase;
color: var(--text);
text-decoration: none;
}
.navlinks { display: flex; gap: var(--space-2); justify-content: center; }
.navlinks a {
padding: 0.4rem var(--space-3);
border-radius: var(--radius);
color: var(--text);
text-decoration: none;
font-size: 0.9rem;
transition: background var(--duration-fast) var(--ease-out),
color var(--duration-fast) var(--ease-out);
}
/* Hover and current route look the same, on purpose. */
.navlinks a:hover, .navlinks a[aria-current="page"] {
background: var(--surface-raised);
color: var(--primary);
}
.userblock {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--space-3);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius);
line-height: 1.2;
}
.userblock .lines { text-align: right; display: flex; flex-direction: column; }
.userblock .name { font-size: 0.85rem; font-weight: 600; }
.userblock .email { font-size: 0.7rem; color: var(--muted); }
.avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
flex: none;
border-radius: 50%;
background: var(--surface-raised);
border: 1px solid var(--input-border);
color: var(--gold-1);
font-family: var(--font-display);
font-size: 0.9rem;
font-weight: 600;
}
.avatar.small { width: 1.8rem; height: 1.8rem; font-size: 0.75rem; }
/* --- layout --- */
main { max-width: var(--content); margin: 0 auto; padding: var(--space-6) var(--space-5); }
main.narrow { max-width: 420px; padding-top: 8vh; }
section { margin-bottom: var(--space-6); }
footer.sitefooter {
text-align: center;
color: var(--muted);
font-size: 0.8rem;
padding: var(--space-6) var(--space-4) var(--space-4);
}
.muted { color: var(--muted); }
.small { font-size: 0.8rem; }
.nowrap { white-space: nowrap; }
.error { color: var(--error); display: block; font-size: 0.85rem; }
.empty { font-family: var(--font-display); color: var(--muted); padding: var(--space-6) 0; }
/* --- badges --- */
.badge {
display: inline-block;
border-radius: var(--radius-pill);
padding: 0.15rem var(--space-2);
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.badge.genre { background: var(--surface-raised); color: var(--primary); border: 1px solid var(--input-border); }
.badge.admin { background: #3a1a1a; color: var(--gold-2); }
.badge.reviewed { background: var(--success-bg); color: var(--success); border: 1px solid var(--success); }
.badge.pending { background: var(--pending-bg); color: var(--pending); }
/* --- song grid --- */
.songgrid { display: grid; grid-template-columns: 1fr; gap: var(--space-4); }
@media (min-width: 640px) { .songgrid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .songgrid { grid-template-columns: repeat(3, 1fr); } }
.songcard {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-1);
background: var(--surface);
border: 1px solid var(--hairline);
border-radius: var(--radius);
padding: var(--space-4);
padding-right: var(--space-6);
text-decoration: none;
color: var(--text);
box-shadow: var(--shadow-card);
transition: transform var(--duration-fast) var(--ease-out),
border-color var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out);
}
.songcard:hover {
transform: translateY(-2px);
border-color: var(--primary);
box-shadow: var(--shadow-card-hover);
}
/* The single most important state in the app: what still needs a review. Carried by the border
AND the pending badge — never by colour alone. */
.songcard.unreviewed { border-color: var(--unreviewed); }
.songcard.unreviewed:hover { border-color: var(--unreviewed-hover); }
.songcard .title { font-size: 1.1rem; font-weight: 700; color: var(--text-strong); }
.songcard .artist { font-size: 0.9rem; color: #cccccc; }
.songcard .meta { font-size: 0.75rem; color: var(--muted); display: flex; gap: var(--space-2);
align-items: center; flex-wrap: wrap; margin-top: var(--space-2); }
.songcard .scorebadge {
position: absolute;
top: var(--space-3);
right: var(--space-3);
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 700;
color: var(--gold-1);
background: rgba(0, 0, 0, 0.5);
border-radius: var(--radius);
padding: 0 var(--space-2);
}
/* --- song page and reviews --- */
/* The artist is the second most important thing on the page, so it gets the display face and a
line of its own rather than being one bold word inside a run of metadata. */
.by {
font-family: var(--font-display);
font-size: 1.35rem;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--text);
margin: 0 0 var(--space-3);
}
/* Four kinds of fact, four labelled cells — the spine of a cassette insert. */
.spec {
display: flex;
flex-wrap: wrap;
margin: 0 0 var(--space-5);
background: var(--surface);
border: 1px solid var(--hairline);
border-radius: var(--radius);
overflow: hidden;
}
.spec > div {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 6rem;
padding: var(--space-2) var(--space-4);
border-right: 1px solid var(--hairline);
}
.spec > div:last-child { border-right: 0; }
.spec dt {
font-family: var(--font-display);
font-size: 0.65rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.spec dd { margin: 0; font-size: 0.95rem; }
.intro { white-space: pre-wrap; background: var(--surface); padding: var(--space-4);
border-left: 3px solid var(--hairline); border-radius: 0 var(--radius) var(--radius) 0; }
.average { font-family: var(--font-display); font-size: 1.2rem; color: var(--gold-1); }
.lyrics {
background: var(--surface);
border: 1px solid var(--hairline);
border-radius: var(--radius);
padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-5);
}
.lyrics > summary {
cursor: pointer;
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--primary);
}
/* Lyrics are typed with intent: line breaks and indentation are the content. */
.lyricstext {
font-family: inherit;
font-size: 0.95rem;
line-height: 1.7;
white-space: pre-wrap;
margin: var(--space-4) 0 0;
max-height: 26rem;
overflow-y: auto;
}
.lyrics form { margin-top: var(--space-4); }
.review {
padding: var(--space-4);
border-left: 3px solid var(--input-border);
background: var(--surface);
border-radius: 0 var(--radius) var(--radius) 0;
margin-bottom: var(--space-2);
}
.review.own { border-left-color: var(--primary); }
.review header { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); }
.review .who { font-weight: 700; color: var(--primary); }
.review .score { font-family: var(--font-display); font-size: 1.8rem; font-weight: 700; color: var(--gold-1); }
.review p { white-space: pre-wrap; line-height: 1.6; margin: 0; }
.review .meta { font-size: 0.8rem; color: var(--muted); }
.player { width: 100%; margin: var(--space-3) 0 var(--space-5); }
/* --- forms --- */
form.stack { display: flex; flex-direction: column; gap: var(--space-4); }
form.stack label { display: flex; flex-direction: column; gap: var(--space-1); }
form.stack label.row { flex-direction: row; align-items: center; gap: var(--space-2); }
/* Buttons size to their label rather than stretching across the column. */
form.stack > button, form.stack > .actions { align-self: flex-start; }
input, select, textarea {
background: var(--surface);
color: var(--text);
border: 1px solid var(--input-border);
border-radius: var(--radius);
padding: var(--space-2) var(--space-3);
font: inherit;
width: 100%;
transition: border-color var(--duration-fast) var(--ease-out);
}
input[type="checkbox"], input[type="range"] { width: auto; }
input:hover, select:hover, textarea:hover { border-color: var(--muted); }
input:focus, select:focus, textarea:focus { border-color: var(--primary); }
textarea { resize: vertical; line-height: 1.6; }
button, .btn {
background: var(--primary);
color: var(--primary-inverse);
border: 1px solid var(--primary);
border-radius: var(--radius);
padding: var(--space-2) var(--space-4);
font: inherit;
font-weight: 600;
cursor: pointer;
transition: background var(--duration-fast) var(--ease-out),
border-color var(--duration-fast) var(--ease-out),
color var(--duration-fast) var(--ease-out);
}
button:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
button:disabled { opacity: 0.5; cursor: not-allowed; background: var(--surface-raised);
border-color: var(--input-border); color: var(--muted); }
button.ghost {
background: transparent;
border-color: var(--input-border);
color: var(--text);
font-size: 0.85rem;
}
button.ghost:hover { border-color: var(--primary); color: var(--primary);
background: rgba(180, 120, 72, 0.05); }
button.danger { background: transparent; border-color: var(--pending); color: var(--pending); }
button.danger:hover { background: var(--pending-bg); border-color: var(--unreviewed-hover);
color: var(--error); }
button.link { background: none; border: 0; color: var(--primary); padding: 0;
font-weight: 400; font-size: 0.9rem; text-decoration: underline; }
button.link:hover { background: none; color: var(--primary-hover); }
/* --- the channel strip --- */
/* Score and text side by side: the two things you do at once stop being a screen apart. */
.strip {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-5);
background: var(--surface);
border: 1px solid var(--hairline);
border-radius: var(--radius);
padding: var(--space-5);
box-shadow: var(--shadow-card);
margin-bottom: var(--space-5);
}
.deck { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.deck .grow { flex: 1; }
/* Read on the left, write on the right. One column when the song has no lyrics — the pane is
absent rather than empty. */
.panes { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); flex: 1; min-height: 0; }
.panes.solo { grid-template-columns: 1fr; }
.lyricspane, .writepane { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.writepane .grow { display: flex; flex-direction: column; gap: var(--space-1); }
.writepane textarea { flex: 1; min-height: 12rem; }
.cap {
font-family: var(--font-display);
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.lyricsbox {
/* Grows with the pane but stops before it can push the page: long lyrics scroll inside the box
rather than stretching the strip past the screen. */
flex: 1 1 auto;
min-height: 10rem;
max-height: 24rem;
overflow-y: auto;
padding: var(--space-3) var(--space-4);
background: var(--bar);
border: 1px solid var(--hairline);
border-radius: var(--radius);
white-space: pre-wrap;
line-height: 1.7;
font-size: 0.95rem;
/* Scrolling is smoothed in JS, which also knows when to skip it. Doing it here as well makes two
mechanisms fight over the same element. */
}
/* Synced lyrics: the line that is playing is the only bright one, and clicking any line seeks. */
.lyricsbox.synced { white-space: normal; }
.lline {
margin: 0;
padding: 0.1rem 0;
color: var(--muted);
cursor: pointer;
transition: color var(--duration-fast) var(--ease-out);
}
.lline:hover { color: var(--text); }
.lline.on {
color: var(--gold-1);
font-weight: 600;
}
/* Uniform distribution models a song no real song obeys, so the reader gets a knob. */
.follow { display: flex; align-items: center; gap: var(--space-2); font-size: 0.8rem;
color: var(--muted); cursor: pointer; }
.follow input { width: auto; accent-color: var(--primary); }
.nudge { display: flex; align-items: center; gap: var(--space-2); font-size: 0.75rem;
color: var(--muted); font-family: var(--font-display); text-transform: uppercase;
letter-spacing: 0.06em; }
.nudge input { flex: 1; accent-color: var(--primary); }
.nudge output { min-width: 4ch; text-align: right; color: var(--text); }
.deck .player { margin: 0; }
.deckfoot { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
/* Fixed columns, not auto: the readout spans both, so an auto track would widen the whole fader
when the score reaches three digits and shove the deck sideways mid-drag. */
.fader { display: grid; grid-template-columns: 2rem 2rem; grid-template-rows: 1fr auto;
gap: var(--space-2); align-items: stretch; }
.ticks { display: flex; flex-direction: column; justify-content: space-between; text-align: right;
font-size: 0.7rem; color: var(--muted); font-family: var(--font-display); }
/* Vertical is native: writing-mode plus rtl gives bottom-to-top travel with no custom widget. */
.fader input[type="range"] {
writing-mode: vertical-lr;
direction: rtl;
-webkit-appearance: none;
appearance: none;
width: 2rem;
height: 100%;
min-height: 15rem;
padding: 0;
border: 0;
background: transparent;
cursor: ns-resize;
}
.fader input[type="range"]::-webkit-slider-runnable-track {
width: 8px;
background: var(--track);
border: 1px solid var(--input-border);
border-radius: var(--radius-pill);
}
.fader input[type="range"]::-moz-range-track {
width: 8px;
background: var(--track);
border: 1px solid var(--input-border);
border-radius: var(--radius-pill);
}
/* A cap, not a knob: this is a fader. */
.fader input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 2rem;
height: 14px;
margin-left: -12px;
border-radius: 2px;
background: linear-gradient(var(--gold-1), var(--gold-2));
border: 1px solid var(--bar);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
}
.fader input[type="range"]::-moz-range-thumb {
width: 2rem;
height: 14px;
border-radius: 2px;
background: linear-gradient(var(--gold-1), var(--gold-2));
border: 1px solid var(--bar);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
}
.readout {
grid-column: 1 / -1;
font-family: var(--font-display);
font-size: 1.8rem;
font-weight: 700;
/* Digits of equal width, so 99 → 100 does not shift anything inside the box either. */
font-variant-numeric: tabular-nums;
color: var(--gold-1);
text-align: center;
background: var(--bar);
border: 1px solid var(--input-border);
border-radius: var(--radius);
padding: 0 var(--space-1);
}
/* --- the reveal: one channel per reviewer --- */
.channels { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: flex-end;
margin-bottom: var(--space-5); }
.chan { display: flex; flex-direction: column; align-items: center; gap: var(--space-1); width: 2.6rem; }
.chan-track {
position: relative;
width: 8px;
height: 8rem;
background: var(--track);
border: 1px solid var(--input-border);
border-radius: var(--radius-pill);
animation: chan-rise var(--duration-fast) var(--ease-out) backwards;
animation-delay: calc(var(--i, 0) * 40ms);
}
/* The cap sits at the score: the row's silhouette is the spread. */
.chan-cap {
position: absolute;
left: 50%;
bottom: calc(var(--v) * 1%);
transform: translate(-50%, 50%);
width: 1.8rem;
height: 10px;
border-radius: 2px;
background: var(--primary);
border: 1px solid var(--bar);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}
/* The filled part below the cap reads as level. */
.chan-cap::after {
content: "";
position: absolute;
left: 50%;
top: 100%;
transform: translateX(-50%);
width: 4px;
height: 8rem;
background: linear-gradient(var(--primary), transparent);
opacity: 0.5;
}
.chan.own .chan-cap { background: var(--gold-1); }
.chan.own .chan-cap::after { background: linear-gradient(var(--gold-1), transparent); }
.chan.own .chan-score { color: var(--gold-1); }
.chan.avg .chan-track { background: transparent; border-left: 1px dashed var(--input-border); width: 1px; }
.chan.avg .chan-cap { background: var(--gold-2); width: 2rem; height: 2px; border: 0; }
.chan.avg .chan-cap::after { display: none; }
.chan-score { font-family: var(--font-display); font-size: 0.95rem; color: var(--text); }
.chan-who { font-size: 0.7rem; color: var(--muted); }
@keyframes chan-rise {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
/* Hidden scores are hatched rather than absent: "locked", not "no data". */
.sealed {
display: inline-block;
width: 2.2rem;
height: 1rem;
vertical-align: -2px;
border-radius: 2px;
background: repeating-linear-gradient(-45deg, rgba(224, 224, 224, 0.18) 0 6px, transparent 6px 12px),
var(--bar);
border: 1px solid var(--input-border);
}
.sealed-note { color: var(--muted); display: flex; align-items: center; gap: var(--space-2); }
.reviewtext { white-space: pre-wrap; line-height: 1.6; }
.nextup { font-family: var(--font-display); text-transform: uppercase; font-size: 1.1rem; }
/* --- drop zone --- */
.dropzone {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-1);
padding: var(--space-6) var(--space-4);
border: 2px dashed var(--input-border);
border-radius: var(--radius);
background: var(--surface);
cursor: pointer;
text-align: center;
transition: border-color var(--duration-fast) var(--ease-out),
background var(--duration-fast) var(--ease-out);
}
.dropzone:hover { border-color: var(--primary); }
.dropzone.over { border-color: var(--primary); background: var(--surface-raised); }
.dropzone.has-file { border-style: solid; border-color: var(--primary); }
.dropzone input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.dropzone:focus-within { box-shadow: var(--shadow-focus); }
.dz-title { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.02em; }
.filename { color: var(--primary); word-break: break-all; }
.or { text-align: center; color: var(--muted); text-transform: uppercase;
font-family: var(--font-display); margin: 0; }
/* --- submission status --- */
.status { background: var(--surface); border-left: 3px solid var(--primary);
border-radius: 0 var(--radius) var(--radius) 0; padding: var(--space-4);
margin-bottom: var(--space-5); }
.status p { margin: 0 0 var(--space-2); }
.status.failed { border-left-color: var(--pending); }
.saved { color: var(--muted); font-size: 0.85rem; min-height: 1.2em; }
.editbox { background: var(--surface); border: 1px solid var(--hairline);
border-radius: var(--radius); padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-5); }
.editbox summary { cursor: pointer; font-family: var(--font-display);
text-transform: uppercase; color: var(--primary); }
.editbox form { margin: var(--space-4) 0; }
.actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: flex-start; }
.actions form { display: flex; gap: var(--space-2); }
.actions input { width: 12rem; }
/* --- tables and admin --- */
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: var(--space-2); border-bottom: 1px solid var(--hairline); }
th { font-family: var(--font-display); text-transform: uppercase; font-size: 0.8rem;
font-weight: 600; color: var(--muted); }
tr.banned { opacity: 0.55; }
td.break { word-break: break-all; font-size: 0.8rem; }
.adminsection { background: var(--surface); border: 1px solid var(--hairline);
border-radius: var(--radius); margin-bottom: var(--space-6); }
.adminsection > header { background: var(--surface-header); padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--hairline); display: flex;
align-items: center; justify-content: space-between; gap: var(--space-4); }
.adminsection > header h2 { margin: 0; }
.adminsection .body { padding: var(--space-4) var(--space-5); }
.dot { display: inline-block; width: 0.9rem; height: 0.9rem; border-radius: 50%; }
.dot.on { background: #5a8f5a; }
.dot.off { background: #8f4a44; }
code { background: var(--surface-raised); padding: 0.1rem var(--space-1);
border-radius: var(--radius); font-size: 0.85rem; }
/* --- toasts --- */
.toasts { position: fixed; right: var(--space-4); bottom: var(--space-4); z-index: 1000;
display: flex; flex-direction: column; gap: var(--space-2);
max-width: min(360px, 100vw - 24px); pointer-events: none; }
.toast {
pointer-events: auto;
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-toast);
border-left: 4px solid var(--success);
background: var(--surface);
box-shadow: var(--shadow-card);
animation: toast-in var(--duration-fast) var(--ease-out);
}
.toast p { margin: 0; }
.toast .dismiss { background: none; border: 0; color: var(--text); font-size: 1.5rem;
line-height: 1; padding: 0; opacity: 0.7; cursor: pointer; }
.toast .dismiss:hover { opacity: 1; background: none; }
@keyframes toast-in {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: none; }
}
/* --- mobile --- */
.mobilenav { display: none; }
@media (max-width: 768px) {
.topbar-inner { grid-template-columns: 1fr auto; }
.navlinks, .userblock .lines { display: none; }
.mobilenav { display: block; justify-self: end; }
.mobilenav > summary {
list-style: none;
cursor: pointer;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--input-border);
border-radius: var(--radius);
font-size: 1.4rem;
color: var(--text);
transition: border-color var(--duration-fast) var(--ease-out),
background var(--duration-fast) var(--ease-out);
}
.mobilenav > summary::-webkit-details-marker { display: none; }
.mobilenav[open] > summary, .mobilenav > summary:hover {
border-color: var(--primary);
background: rgba(180, 120, 72, 0.05);
}
.mobilenav .panel {
position: absolute;
left: 0;
right: 0;
top: 100%;
background: var(--surface);
border-bottom: 2px solid var(--primary);
box-shadow: var(--shadow-card-hover);
padding: var(--space-5) var(--space-4) var(--space-4);
display: flex;
flex-direction: column;
z-index: 95;
animation: panel-down var(--duration-fast) var(--ease-out);
}
.mobilenav .panel a, .mobilenav .panel button {
font-family: var(--font-display);
text-transform: uppercase;
font-size: 1.15rem;
font-weight: 600;
letter-spacing: 0.02em;
padding: var(--space-4);
text-align: left;
background: none;
border: 0;
color: var(--text);
text-decoration: none;
}
main { padding: var(--space-5) var(--space-4); }
.toasts { left: var(--space-4); max-width: none; }
/* A 200px fader on a phone is worse than a horizontal one. */
.strip { grid-template-columns: 1fr; gap: var(--space-3); padding: var(--space-4); }
/* Side by side needs width it does not have here, so reading stacks above writing. */
.panes { grid-template-columns: 1fr; }
.lyricsbox { max-height: 14rem; }
.fader { grid-template-columns: 1fr auto; grid-template-rows: auto; align-items: center; }
.fader input[type="range"] { writing-mode: horizontal-tb; direction: ltr;
width: 100%; height: auto; min-height: 0; }
/* row-reverse: the markup is top-to-bottom (100 first), a horizontal fader runs 1 → 100. */
.ticks { flex-direction: row-reverse; justify-content: space-between; order: 2;
grid-column: 1 / -1; }
.readout { grid-column: auto; font-size: 1.5rem; }
}
@keyframes panel-down {
from { transform: translateY(-100%); opacity: 0; }
to { transform: none; opacity: 1; }
}
/* Keep the fades, drop the movement. */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; }
.songcard:hover { transform: none; }
}
/* --- stats --- */
.boards { display: grid; grid-template-columns: 1fr; gap: var(--space-5); }
@media (min-width: 700px) { .boards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .boards { grid-template-columns: repeat(3, 1fr); } }
.board { background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--radius);
padding: var(--space-4); margin: 0; box-shadow: var(--shadow-card); }
.board h2 { font-size: 1.1rem; margin-bottom: var(--space-3); }
/* A counter rather than a list marker: `display: grid` on the <li> suppresses markers. */
.board-list { list-style: none; counter-reset: rank; margin: 0; padding: 0; }
.board-list li { display: grid; grid-template-columns: 1.6rem 1fr auto;
column-gap: var(--space-2); align-items: baseline;
padding: var(--space-2) 0; border-bottom: 1px solid var(--hairline); }
.board-list li:last-child { border-bottom: 0; }
.board-list li::before { counter-increment: rank; content: counter(rank) "."; color: var(--muted);
font-family: var(--font-display); }
.board-list li a { grid-column: 2; }
.board-list li .value { grid-column: 3; grid-row: 1; font-family: var(--font-display);
font-size: 1.1rem; color: var(--gold-1); }
.board-list li .small { grid-column: 2; }
.board-list li .small:last-child { grid-column: 3; text-align: right; }
/* --- profile --- */
.profilehead { display: flex; align-items: center; gap: var(--space-4); margin-bottom: var(--space-5); }
.profilehead h1 { margin: 0; }
.avatar.big { width: 4.5rem; height: 4.5rem; font-size: 1.6rem; object-fit: cover; }
img.avatar { object-fit: cover; }
.statgrid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-4);
margin-bottom: var(--space-6); }
@media (min-width: 700px) { .statgrid { grid-template-columns: repeat(4, 1fr); } }
.statcard { background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--radius);
padding: var(--space-5); text-align: center; display: flex; flex-direction: column;
gap: var(--space-1); box-shadow: var(--shadow-card); }
.statvalue { font-family: var(--font-display); font-size: 2rem; font-weight: 700; color: var(--gold-1); }
/* --- leaderboard bars --- */
.bar { grid-column: 2 / -1; position: relative; height: 4px; margin-top: var(--space-1);
background: var(--track); border-radius: var(--radius-pill); overflow: hidden; }
.bar .fill { position: absolute; top: 0; bottom: 0; left: 0; background: var(--primary);
border-radius: var(--radius-pill); }
.bar.range .fill { background: linear-gradient(90deg, var(--primary), var(--gold-1)); }
/* --- nav queue count --- */
.count { display: inline-block; min-width: 1.4em; padding: 0 0.35em; border-radius: var(--radius-pill);
background: var(--primary); color: var(--primary-inverse); font-size: 0.75rem;
font-weight: 700; text-align: center; }
/* --- profile comparison --- */
.statcard.wide { grid-column: span 2; }
.channels.compare { justify-content: center; gap: var(--space-5); margin: 0; }
.channels.compare .chan-track { height: 5rem; }
.channels.compare .chan-cap::after { height: 5rem; }
.channels.compare .chan { width: 4rem; }
/* --- submission progress --- */
.segments { display: flex; gap: var(--space-1); list-style: none; margin: 0 0 var(--space-3);
padding: 0; font-family: var(--font-display); text-transform: uppercase;
font-size: 0.75rem; letter-spacing: 0.04em; }
.segments li { flex: 1; padding: var(--space-1) var(--space-2); text-align: center;
color: var(--muted); background: var(--bar); border: 1px solid var(--hairline);
border-radius: var(--radius); }
.segments li.now { color: var(--primary-inverse); background: var(--primary); border-color: var(--primary); }
.segments li.done { color: var(--gold-1); border-color: var(--input-border); }
.segments li.failed { color: var(--pending); border-color: var(--pending); background: var(--pending-bg); }
/* The score chip on a card, sealed: the score exists, you just haven't earned it yet.
The specificity has to beat .songcard .scorebadge, which sets its own background. */
.songcard .scorebadge.sealed {
display: block;
width: 2.4rem;
height: 1.4rem;
padding: 0;
background: repeating-linear-gradient(-45deg, rgba(224, 224, 224, 0.22) 0 5px, transparent 5px 10px),
var(--bar);
border: 1px solid var(--input-border);
}
/* --- transport --- */
.playerwrap { margin: 0 0 var(--space-3); }
.playerwrap .player { width: 100%; }
/* Once enhanced the native element is only a source of sound. */
.playerwrap:has(.transport) .player { display: none; }
.transport {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--space-4);
background: var(--bar);
border: 1px solid var(--divider);
border-radius: var(--radius);
padding: var(--space-3) var(--space-4);
}
.tp-play {
width: 3rem;
height: 3rem;
flex: none;
padding: 0;
border-radius: 50%;
background: var(--primary);
border: 0;
display: grid;
place-items: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
}
.tp-play:hover { background: var(--primary-hover); }
/* Drawn shapes, not glyphs: ▶ and ❚❚ render differently on every platform. */
.tp-icon {
width: 0;
height: 0;
border-style: solid;
border-width: 9px 0 9px 14px;
border-color: transparent transparent transparent var(--primary-inverse);
margin-left: 3px;
}
.playing .tp-icon {
width: 14px;
height: 16px;
margin-left: 0;
border: 0;
background: linear-gradient(90deg, var(--primary-inverse) 0 5px, transparent 5px 9px,
var(--primary-inverse) 9px 14px);
}
.tp-mid { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
/* A range input for seeking, so arrow keys and the focus ring come free. */
.tp-seek {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 14px;
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
}
.tp-seek::-webkit-slider-runnable-track {
height: 6px;
border-radius: var(--radius-pill);
background: linear-gradient(90deg, var(--primary) 0 var(--pct, 0%), var(--track) var(--pct, 0%));
border: 1px solid var(--input-border);
}
.tp-seek::-moz-range-track {
height: 6px;
border-radius: var(--radius-pill);
background: var(--track);
border: 1px solid var(--input-border);
}
.tp-seek::-moz-range-progress { height: 6px; border-radius: var(--radius-pill); background: var(--primary); }
.tp-seek::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
margin-top: -4px;
border-radius: 50%;
background: var(--gold-1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}
.tp-seek::-moz-range-thumb {
width: 12px;
height: 12px;
border: 0;
border-radius: 50%;
background: var(--gold-1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}
.tp-time { font-family: var(--font-display); font-size: 0.95rem; color: var(--muted); white-space: nowrap; }
.tp-time b { color: var(--text); font-weight: 700; }
/* --- level meter --- */
.meter { display: flex; flex-direction: column; gap: 3px; }
.meter-row { display: flex; gap: var(--space-2); align-items: center; }
.meter-row .lbl { font-family: var(--font-display); font-size: 0.65rem; color: var(--muted); width: 0.8rem; }
.meter .segs { display: flex; gap: 2px; }
.seg { width: 6px; height: 11px; border-radius: 1px; background: var(--track);
border: 1px solid rgba(0, 0, 0, 0.4); }
.seg.on { background: var(--gold-2); }
.seg.on.hot { background: var(--primary); }
.seg.on.peak { background: #c0392b; }
@media (max-width: 768px) {
.transport { gap: var(--space-3); padding: var(--space-3); }
.tp-play { width: 2.6rem; height: 2.6rem; }
.seg { width: 4px; }
}
.pager { display: flex; gap: var(--space-5); margin-top: var(--space-5); }
/* The slogan is a mark, so it is set in the display face rather than the body face. */
.slogan { font-family: var(--font-display); letter-spacing: 0.02em; }
.slogan.hero { color: var(--gold-2); font-size: 1.1rem; text-transform: uppercase;
margin: calc(-1 * var(--space-2)) 0 var(--space-5); }
.copyright { color: var(--muted); }
.copyright::before { content: "·"; margin: 0 var(--space-2); }
.version { color: var(--muted); font-family: var(--font-display); }
.version::before { content: "·"; margin: 0 var(--space-2); }
.lyricsbar { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
margin-top: var(--space-2); }