Files
Esa Kataja 0f15ae0bfc Release 2026.08.02-1
SQLite replaces Postgres, and two fixes from using the thing.

- The database is a file under ./storage instead of a second container. Ten
  members never needed a database server, and the driver is pure Go, so the
  build stays CGO_ENABLED=0 and the dependency count is unchanged. One bind
  mount is now the whole backup: no pgdata, no healthcheck-gated depends_on,
  no startup retry loop. Timestamps are UTC text, idle_ttl is seconds, the
  divisive/unified boards carry their own stddev, and foreign keys are on by
  pragma. Tests get a database file each and run without any setup
- Invites are copied, not clicked. An invite is something to send, and the
  anchor opened the join form in the admin's own browser
- Feedback asks for more than faults: the footer reads "Ongelmia? Ideoita?
  Palautetta?" and the page behind it invites ideas rather than only bugs
- Kuuntele YouTubessa opens in a new tab, so a half-typed review survives it
2026-08-02 20:58:52 +03:00

1091 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; }
/* The link is long and the button must stay reachable next to it on a narrow admin window. */
.invitecell { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.invitecell code { word-break: break-all; }
/* --- 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); }