Files
Levyraati26_go/static/style.css
T
Esa Kataja f1e907bac3 Add stats, profiles, avatars and palaute
Step 6. The surfaces around the review loop.

- Nine leaderboards, ordered and limited in SQL, each with a deterministic
  tie-break so a tied board doesn't reshuffle between reloads. Min 3 reviews
  to qualify, for reviewer boards too
- Profiles show counts and history-wide averages and the member's songs,
  never a list of their reviews — per-song opinion stays gated
- Avatars: 5MB in, 256px JPEG out, ffmpeg's re-encode being the validation.
  No upload still means initials, and avatars are public
- Changing your own password requires the current one and drops your other
  sessions
- Palaute: free text plus the page you were on, carried in a footer link, and
  the user agent from the header. Reporters see their own; the admin resolves
  them with a timestamp rather than a status enum
- Admin gained the song list with delete, the reports page, and an open-report
  count on the dashboard

Two theme fixes the screenshots caught: leaderboard ranks need a CSS counter
because display:grid suppresses list markers, and count-based boards were
printing 3.0 where they mean 3.
2026-07-31 22:34:04 +03:00

593 lines
21 KiB
CSS
Raw 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;
/* 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 --- */
.songmeta { color: var(--muted); display: flex; flex-wrap: wrap; gap: var(--space-2);
align-items: center; margin: calc(-1 * var(--space-2)) 0 var(--space-4); }
.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); }
.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); }
.scorerow { display: flex; align-items: center; gap: var(--space-4); }
.scorerow input[type="range"] { flex: 1; accent-color: var(--primary); }
.scorerow output { font-family: var(--font-display); font-size: 2rem; font-weight: 700;
color: var(--gold-1); min-width: 3ch; text-align: right; }
/* Pulled up so it hugs the control. */
.scorescale { display: flex; justify-content: space-between; font-size: 0.9rem;
color: var(--muted); opacity: 0.6; margin-top: calc(-1 * var(--space-2)); }
/* --- 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; }
}
@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); }