Files
pl8list-rewind/static/style.css
T
nvrl 27225332fb Add opt-in pl8list connection with library sync, follows and resync
At sign-in users can choose to stay connected to pl8list on that device.
The pl8list session is then stored in the device's session row, encrypted
with XChaCha20-Poly1305 under a key derived from the device's local token
(only the browser has it) and a server key kept outside the database. It
is only decrypted during that device's own requests, re-encrypted when the
token rotates or pl8list sends a new cookie, and deleted when pl8list
rejects it.

Every sign-in now syncs the personal feed, saved tracks (imported as likes)
and saved artists (imported as follows). Connected devices resync on use
every six hours and push new likes and follows to the pl8list library with
addOnly, so nothing is ever removed there. Without the connection, the
resync button runs the email-code sign-in again.

Artists can be followed from each card (f); follows feed the ranker. The
account menu shows the sync state, resync and disconnect. Privacy policy,
export and account deletion cover the new data.
2026-11-01 12:36:00 +01:00

489 lines
14 KiB
CSS

:root {
--fg: #f2f2f2;
--muted: #9a9a9a;
--accent: #ff4d6d;
--panel: rgba(18, 18, 20, 0.94);
color-scheme: dark;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
height: 100%;
background: #000;
color: var(--fg);
font: 15px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
a { color: inherit; }
button, input {
font: inherit;
color: inherit;
}
button {
cursor: pointer;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 999px;
padding: 0.35em 0.9em;
}
[hidden] { display: none !important; }
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
/* Header and panels ------------------------------------------------------ */
#bar {
position: fixed;
inset: 0 0 auto 0;
z-index: 10;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 1rem;
padding-top: max(0.6rem, env(safe-area-inset-top));
background: linear-gradient(#000c, #0000);
}
.logo { font-weight: 600; margin-right: auto; }
.logo b { color: var(--accent); }
#stats { color: var(--muted); font-size: 0.8rem; }
#help-btn { width: 2.2em; padding: 0.35em 0; }
#account-btn { max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#filters-btn.active::after {
content: "";
display: inline-block;
width: 0.45em;
height: 0.45em;
margin-left: 0.4em;
border-radius: 50%;
background: var(--accent);
vertical-align: middle;
}
#bar button[aria-expanded="true"] { background: rgba(255, 255, 255, 0.22); }
.panel {
position: fixed;
z-index: 11;
top: calc(3rem + env(safe-area-inset-top));
right: 1rem;
background: var(--panel);
border: 1px solid #333;
border-radius: 12px;
padding: 0.8rem 1rem;
box-shadow: 0 16px 40px #000a;
}
#filters { display: flex; gap: 0.8rem; align-items: center; flex-wrap: wrap; }
#filters input[type=number] {
width: 4.5em;
background: #222;
border: 1px solid #444;
border-radius: 6px;
padding: 0.2em 0.4em;
}
#likes { width: min(26rem, calc(100vw - 2rem)); max-height: 70vh; overflow: auto; }
#likes h2 { margin: 0 0 0.5rem; font-size: 1rem; }
#likes ol { margin: 0; padding: 0; list-style: none; }
#likes li {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.35rem 0;
border-bottom: 1px solid #ffffff10;
}
#likes li:last-child { border-bottom: none; }
#likes li .meta { min-width: 0; flex: 1; }
#likes li .meta a { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-decoration: none; }
#likes li .meta a:hover { text-decoration: underline; }
#likes li .meta span { color: var(--muted); font-size: 0.8rem; }
#likes li .play-now {
flex: none;
width: 2rem;
height: 2rem;
padding: 0;
display: grid;
place-items: center;
}
#likes li .play-now svg { width: 0.9rem; height: 0.9rem; }
#account { width: min(20rem, calc(100vw - 2rem)); display: flex; flex-direction: column; align-items: flex-start; gap: 0.7rem; }
#account h2 { margin: 0; font-size: 1rem; }
#account a { font-size: 0.85rem; color: var(--muted); }
#sync { width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem;
padding-bottom: 0.7rem; border-bottom: 1px solid #ffffff14; }
#sync h3 { margin: 0; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
#sync-status { margin: 0; font-size: 0.85rem; color: var(--muted); }
#sync-status.on::before { content: "● "; color: #3ddc84; }
#delete-form { display: flex; flex-direction: column; gap: 0.5rem; width: 100%; }
#delete-form label { color: var(--muted); font-size: 0.85rem; }
#delete-form input { padding: 0.4em 0.6em; border-radius: 8px; border: 1px solid #444; background: #111; }
.link.danger { color: var(--accent); }
button.danger:not(.link) { background: var(--accent); border: none; color: #fff; font-weight: 600; }
.legal-links a { color: inherit; }
/* Feed ------------------------------------------------------------------- */
#feed {
height: 100dvh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
scrollbar-width: none;
}
#feed::-webkit-scrollbar { display: none; }
.card {
position: relative;
height: 100dvh;
scroll-snap-align: start;
scroll-snap-stop: always;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
cursor: pointer;
}
.bg {
position: absolute;
inset: -40px;
background: #111 center / cover no-repeat;
filter: blur(40px) brightness(0.45) saturate(1.3);
}
.content {
position: relative;
width: min(34rem, calc(100vw - 5.5rem));
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.5rem;
}
.art {
position: relative;
width: min(16rem, 42vh, 70vw);
aspect-ratio: 1;
border-radius: 14px;
background: #222 center / cover no-repeat;
box-shadow: 0 20px 60px #000a;
display: grid;
place-items: center;
margin-bottom: 0.6rem;
transition: transform 0.25s ease, filter 0.25s ease;
}
.initial { font-size: 4rem; font-weight: 700; color: #fff6; }
.card.paused .art { transform: scale(0.96); filter: brightness(0.7); }
.paused-badge {
position: absolute;
inset: 0;
margin: auto;
width: 4.5rem;
height: 4.5rem;
border-radius: 50%;
background: #000a;
display: grid;
place-items: center;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.paused-badge svg { width: 2rem; height: 2rem; margin-left: 0.2rem; }
.card.paused .paused-badge { opacity: 1; }
.spinner {
position: absolute;
inset: 0;
margin: auto;
width: 3rem;
height: 3rem;
border-radius: 50%;
border: 3px solid #ffffff33;
border-top-color: #fff;
opacity: 0;
pointer-events: none;
animation: spin 0.8s linear infinite;
}
.card.buffering .spinner { opacity: 1; }
.card.buffering .paused-badge { opacity: 0; }
@keyframes spin { to { transform: rotate(360deg); } }
.title { font-size: 1.25rem; margin: 0; overflow-wrap: anywhere; }
.byline { display: flex; align-items: center; gap: 0.6rem; }
.artist { color: var(--muted); text-decoration: none; }
.artist:hover { text-decoration: underline; }
.follow {
font-size: 0.75rem;
padding: 0.15em 0.75em;
border-radius: 999px;
border: 1px solid #ffffff40;
background: transparent;
}
.follow.on { background: #ffffff1f; border-color: transparent; color: var(--muted); }
.chips { display: flex; gap: 0.4rem; flex-wrap: wrap; justify-content: center; }
.chips span {
font-size: 0.75rem;
padding: 0.1em 0.6em;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
}
.chips .free { background: #1f7a4d; }
.chips .preview { background: #6a4b16; }
.desc {
margin: 0;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
overflow-wrap: anywhere;
color: var(--muted);
font-size: 0.85rem;
}
.wave-wrap { position: relative; width: 100%; margin-top: 0.6rem; }
.wave { display: block; width: 100%; height: 64px; cursor: pointer; touch-action: none; }
.wave-tip {
position: absolute;
bottom: calc(100% + 4px);
transform: translateX(-50%);
padding: 0.1em 0.45em;
border-radius: 6px;
background: #000c;
font-size: 0.75rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.1s;
}
.wave-wrap.hover .wave-tip { opacity: 1; }
.time { width: 100%; display: flex; justify-content: space-between; color: var(--muted); font-size: 0.75rem; }
.counts { color: var(--muted); font-size: 0.8rem; }
.actions {
position: absolute;
right: 0.8rem;
bottom: 18vh;
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.actions > * {
width: 3rem;
height: 3rem;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
border: none;
text-decoration: none;
padding: 0;
transition: background 0.15s, transform 0.15s;
}
.actions > *:hover { background: rgba(255, 255, 255, 0.2); }
.actions > *:active { transform: scale(0.9); }
.actions svg { width: 1.3rem; height: 1.3rem; }
.actions .like.on { color: var(--accent); background: rgba(255, 77, 109, 0.2); }
.actions .like.pop svg { animation: pop 0.35s ease; }
@keyframes pop { 50% { transform: scale(1.4); } }
/* Feedback overlays ------------------------------------------------------ */
.flash {
position: absolute;
z-index: 2;
top: 50%;
left: 50%;
width: 5.5rem;
height: 5.5rem;
margin: -2.75rem 0 0 -2.75rem;
border-radius: 50%;
background: #000a;
display: grid;
place-items: center;
pointer-events: none;
animation: flash 0.6s ease forwards;
}
.flash svg { width: 2.4rem; height: 2.4rem; }
@keyframes flash {
from { opacity: 1; transform: scale(0.7); }
to { opacity: 0; transform: scale(1.3); }
}
.burst {
position: absolute;
z-index: 3;
width: 6rem;
height: 6rem;
margin: -3rem 0 0 -3rem;
color: var(--accent);
pointer-events: none;
filter: drop-shadow(0 6px 20px #ff4d6d88);
animation: burst 0.8s cubic-bezier(.2, .9, .3, 1.3) forwards;
}
@keyframes burst {
0% { opacity: 0; transform: scale(0.3) rotate(var(--tilt)); }
25% { opacity: 1; transform: scale(1.15) rotate(var(--tilt)); }
60% { opacity: 1; transform: scale(1) rotate(var(--tilt)); }
100% { opacity: 0; transform: translateY(-3rem) scale(0.9) rotate(var(--tilt)); }
}
#toast {
position: fixed;
z-index: 40;
left: 50%;
bottom: max(2rem, env(safe-area-inset-bottom));
transform: translate(-50%, 1rem);
padding: 0.6em 1.1em;
border-radius: 999px;
background: #222e;
border: 1px solid #ffffff22;
font-size: 0.9rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
white-space: nowrap;
}
#toast.show { opacity: 1; transform: translate(-50%, 0); }
.status {
height: 100dvh;
display: grid;
place-items: center;
color: var(--muted);
scroll-snap-align: start;
padding: 1rem;
text-align: center;
}
#start {
position: fixed;
inset: 0;
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
background: #000b;
cursor: pointer;
}
#start button { font-size: 1.3rem; padding: 0.7em 1.6em; background: var(--accent); border: none; color: #fff; }
#help {
position: fixed;
inset: 0;
z-index: 25;
display: grid;
place-items: center;
padding: 1rem;
background: #000b;
animation: fade 0.2s ease;
}
@keyframes fade { from { opacity: 0; } }
.help-box {
width: min(22rem, 100%);
background: var(--panel);
border: 1px solid #333;
border-radius: 16px;
padding: 1.2rem 1.4rem;
}
.help-box h2 { margin: 0 0 0.8rem; font-size: 1.1rem; }
.help-box dl { display: grid; grid-template-columns: auto 1fr; gap: 0.45rem 1rem; margin: 0; }
.help-box dt { font-family: ui-monospace, monospace; font-size: 0.85rem; color: var(--accent); }
.help-box dd { margin: 0; }
.help-box p { color: var(--muted); font-size: 0.85rem; margin: 1rem 0; }
.help-box button { width: 100%; background: var(--accent); border: none; color: #fff; padding: 0.6em; font-weight: 600; }
@media (hover: hover) and (pointer: fine) { .touch-only { display: none !important; } }
@media not ((hover: hover) and (pointer: fine)) { .pointer-only { display: none !important; } }
/* Sign-in ---------------------------------------------------------------- */
#login {
position: fixed;
inset: 0;
z-index: 30;
display: grid;
place-items: center;
padding: 1rem;
background:
radial-gradient(60rem 40rem at 20% 10%, #ff4d6d33, transparent 60%),
radial-gradient(50rem 40rem at 90% 90%, #4d6dff2e, transparent 60%),
#050507;
}
.login-box {
width: min(24rem, 100%);
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.login-box h1 { margin: 0; font-size: 2.4rem; }
.login-box h1 b { color: var(--accent); }
.tagline { margin: 0 0 0.8rem; color: var(--muted); }
.login-box form { display: flex; flex-direction: column; gap: 0.6rem; }
.login-box label { color: var(--muted); font-size: 0.9rem; }
.login-box input {
padding: 0.7em 0.9em;
border-radius: 10px;
border: 1px solid #333;
background: #111;
}
#code { font-family: ui-monospace, monospace; font-size: 1.4rem; letter-spacing: 0.5em; text-align: center; }
.login-box button[type=submit] { background: var(--accent); border: none; color: #fff; padding: 0.7em; font-weight: 600; }
.login-box button[disabled] { opacity: 0.6; cursor: wait; }
.login-links { display: flex; justify-content: space-between; }
.login-box label.check { display: flex; gap: 0.6rem; align-items: flex-start; color: var(--fg); cursor: pointer; }
.check input { margin-top: 0.2rem; accent-color: var(--accent); }
.check small { display: block; margin-top: 0.2rem; color: #777; font-size: 0.75rem; line-height: 1.4; }
#login-cancel { align-self: center; }
.link { background: none; border: none; padding: 0; color: var(--muted); text-decoration: underline; font-size: 0.85rem; }
#login-error { margin: 0; min-height: 1.2em; color: var(--accent); font-size: 0.9rem; }
.fineprint { margin: 0; color: #666; font-size: 0.75rem; }
/* Impressum and privacy pages -------------------------------------------- */
html:has(body.legal), body.legal { height: auto; overflow: auto; }
body.legal main { max-width: 42rem; margin: 0 auto; padding: 2rem 1.2rem 4rem; line-height: 1.6; }
body.legal h1 { font-size: 1.8rem; }
body.legal h2 { font-size: 1.1rem; margin-top: 2rem; }
body.legal a { color: var(--accent); }
/* Phones ----------------------------------------------------------------- */
@media (max-width: 640px) {
#stats { display: none; }
#bar { gap: 0.4rem; padding-left: 0.7rem; padding-right: 0.7rem; }
#bar button { padding: 0.3em 0.7em; font-size: 0.85rem; }
#help-btn { display: none; }
#account-btn { max-width: 6.5rem; }
.panel { left: 0.7rem; right: 0.7rem; }
#likes { width: auto; }
.content { width: calc(100vw - 2rem); margin-bottom: 4.5rem; }
.actions {
right: auto;
left: 50%;
bottom: max(1.2rem, env(safe-area-inset-bottom));
transform: translateX(-50%);
flex-direction: row;
gap: 0.9rem;
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}