Files
musicshare/apps/web/src/app/globals.css
T
chrisandClaude Opus 5.5 be0a00a2c1
CI / check (push) Canceled after 0s
Song progress bar, and poll right after a track ends
- Feed and profile cards show a progress bar with elapsed/total time, only
  when the source reports a real position (Plex, Jellyfin, the Android
  reader). It ticks locally between updates. Apple Music's guessed
  position is no longer reported.
- The Android reader advances MediaSession's stale position to now, stamps
  when it was read (the server corrects for upload delay), and reports
  seeks. The server pushes an update to watchers when a position jumps.
- When a polled source says where in the song someone is, the next poll is
  scheduled just after the song ends (never sooner than 5s), instead of
  waiting out the full interval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
2026-10-06 01:24:20 -04:00

267 lines
16 KiB
CSS

:root {
--bg: #07070b;
--surface: rgba(255, 255, 255, 0.045);
--surface-solid: #12121a;
--surface-2: rgba(255, 255, 255, 0.08);
--border: rgba(255, 255, 255, 0.09);
--border-strong: rgba(255, 255, 255, 0.16);
--text: #f4f3fa;
--muted: #9b99ae;
/* Brand: the logo's purple-to-blue. */
--accent: #7c4dff;
--accent-2: #2f8cff;
--grad: linear-gradient(135deg, #7b2ff7 0%, #5b4cf0 50%, #2f8cff 100%);
--accent-text: #fff;
--live: #34d399;
--pin: #fbbf24;
--danger: #fb7185;
--radius: 20px;
--font-display: var(--font-space-grotesk), ui-sans-serif, system-ui, sans-serif;
--font-body: var(--font-inter), ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--spring: cubic-bezier(0.34, 1.56, 0.64, 1);
color-scheme: dark;
}
* { box-sizing: border-box; }
html { background: var(--bg); }
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font: 15px/1.55 var(--font-body);
-webkit-font-smoothing: antialiased;
}
/* Slowly drifting colour behind everything, so the app never feels static. */
.ambient { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }
.ambient::before, .ambient::after {
content: ''; position: absolute; width: 60vmax; height: 60vmax; border-radius: 50%;
filter: blur(90px); opacity: 0.34;
}
.ambient::before { background: #6d28d9; top: -25vmax; left: -15vmax; animation: drift-a 28s ease-in-out infinite alternate; }
.ambient::after { background: #1d6fe0; bottom: -30vmax; right: -20vmax; animation: drift-b 34s ease-in-out infinite alternate; }
@keyframes drift-a { to { transform: translate(18vmax, 12vmax) scale(1.15); } }
@keyframes drift-b { to { transform: translate(-16vmax, -10vmax) scale(0.9); } }
a { color: inherit; }
h1, h2, .brand, .display { font-family: var(--font-display); letter-spacing: -0.02em; }
h1 { font-size: 1.85rem; margin: 0; line-height: 1.15; }
h2 { font-size: 1.1rem; margin: 0 0 0.5rem; }
input, textarea, select {
width: 100%;
padding: 0.7rem 0.85rem;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--surface-2);
color: var(--text);
font: inherit;
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
}
input:focus, textarea:focus, select:focus {
outline: none; border-color: var(--accent); background: rgba(124, 77, 255, 0.08);
box-shadow: 0 0 0 4px rgba(124, 77, 255, 0.2);
}
.container { max-width: 680px; margin: 0 auto; padding: 1.75rem 1rem 5rem; }
.stack { display: flex; flex-direction: column; gap: 1rem; }
.row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.service-name { display: flex; align-items: center; gap: 0.6rem; font-family: var(--font-display); font-size: 1.12rem; font-weight: 700; margin-bottom: 0.2rem; }
.service-badge { width: 1.7rem; height: 1.7rem; border-radius: 0.5rem; display: grid; place-items: center; color: #fff; font-size: 0.95rem; border: 1px solid rgba(255, 255, 255, 0.25); flex-shrink: 0; }
.avatar-picker { display: flex; gap: 1rem; align-items: center; }
.avatar-picker-actions { display: flex; flex-direction: column; gap: 0.4rem; }
.grow { flex: 1; min-width: 0; }
.muted { color: var(--muted); }
.error { color: var(--danger); margin: 0; }
.notice {
background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 0.85rem 1rem; margin: 0;
backdrop-filter: blur(16px);
}
.section-label { margin: 0; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted); font-weight: 700; }
.narrow { max-width: 420px; margin: 2.5rem auto; }
.nav {
display: flex; justify-content: space-between; align-items: center;
padding: 0.9rem 1.25rem; border-bottom: 1px solid var(--border);
position: sticky; top: 0; background: rgba(7, 7, 11, 0.65); backdrop-filter: blur(18px) saturate(1.4); z-index: 10;
}
.brand { font-weight: 700; text-decoration: none; font-size: 1.2rem; display: inline-flex; align-items: center; gap: 0.5rem; }
.brand-logo { width: 30px; height: 30px; border-radius: 8px; box-shadow: 0 4px 14px -4px rgba(47, 140, 255, 0.6); }
.nav-links { display: flex; gap: 1.1rem; align-items: center; }
.nav-links a { text-decoration: none; color: var(--muted); transition: color 0.2s; }
.nav-links a:hover { color: var(--text); }
.nav-links a.btn-primary { color: var(--accent-text); }
.nav-links a.support-link { color: #f9a8d4; }
@media (max-width: 480px) { .nav-links a.support-link { display: none; } }
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
padding: 0.55rem 1.05rem; border-radius: 999px; border: 1px solid var(--border-strong);
background: var(--surface-2); color: var(--text); font: inherit; font-weight: 600;
text-decoration: none; cursor: pointer; white-space: nowrap;
transition: transform 0.25s var(--spring), border-color 0.2s, background 0.2s, box-shadow 0.25s, filter 0.2s;
}
.btn:hover { border-color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.12); transform: translateY(-1px); }
.btn:active { transform: scale(0.96); }
.btn:disabled { opacity: 0.55; cursor: default; transform: none; }
.btn-primary {
background: var(--grad); border-color: transparent; color: var(--accent-text);
box-shadow: 0 6px 22px -6px rgba(91, 76, 240, 0.6);
}
.btn-primary:hover { background: var(--grad); filter: brightness(1.08); box-shadow: 0 10px 28px -6px rgba(47, 140, 255, 0.65); }
.btn-danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn-small { padding: 0.3rem 0.75rem; font-size: 0.82rem; }
.link-button { background: none; border: 0; color: var(--muted); font: inherit; cursor: pointer; padding: 0; transition: color 0.2s; }
.link-button:hover { color: var(--text); }
.card {
position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
padding: 1.1rem; backdrop-filter: blur(20px) saturate(1.3); overflow: hidden;
}
.card-header { position: relative; display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; margin-bottom: 0.85rem; }
.card-header .user-chip { flex-shrink: 0; }
.card-header > .row { flex-wrap: nowrap; min-width: 0; justify-content: flex-end; margin-left: auto; }
.card-header .status { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; display: inline-block; }
.card-header .status .eq { vertical-align: -1px; margin-right: 0.4rem; }
.card-header .pin { flex-shrink: 0; }
.form { display: flex; flex-direction: column; gap: 0.8rem; }
.form label { display: flex; flex-direction: column; gap: 0.35rem; font-weight: 600; font-size: 0.9rem; }
.form label.checkbox { flex-direction: row; align-items: center; gap: 0.5rem; font-weight: 500; }
/* Now-playing cards take their colour from the album art. */
.np-card { border-color: rgba(255, 255, 255, 0.11); }
.np-card.pinned { border-color: rgba(251, 191, 36, 0.45); box-shadow: 0 0 0 1px rgba(251, 191, 36, 0.15), 0 18px 40px -20px rgba(251, 191, 36, 0.35); }
.np-backdrop {
position: absolute; inset: -40%; z-index: 0; background-size: cover; background-position: center;
filter: blur(40px) saturate(2.2) brightness(1.1); opacity: 0.6; transform: scale(1.1); transition: opacity 0.6s;
}
.np-card.playing .np-backdrop { animation: breathe 9s ease-in-out infinite alternate; }
@keyframes breathe { to { transform: scale(1.3) rotate(8deg); opacity: 0.75; } }
.np-card::after {
content: ''; position: absolute; inset: 0; z-index: 0;
background: linear-gradient(180deg, rgba(7, 7, 11, 0.05), rgba(7, 7, 11, 0.45));
}
.np-card > *:not(.np-backdrop) { position: relative; z-index: 1; }
.pin {
background: none; border: 0; cursor: pointer; font-size: 1.25rem; line-height: 1; color: var(--muted);
padding: 0.15rem 0.25rem; transition: color 0.2s;
}
.pin:hover, .pin.pinned { color: var(--pin); }
.pin.pinned { text-shadow: 0 0 14px rgba(251, 191, 36, 0.6); }
.user-chip { display: inline-flex; align-items: center; gap: 0.55rem; text-decoration: none; font-weight: 600; }
.user-chip:hover .user-name { text-decoration: underline; }
.avatar { border-radius: 50%; object-fit: cover; flex-shrink: 0; display: block; }
.avatar-empty { display: grid; place-items: center; background: linear-gradient(135deg, #4a1278, #1872cf); color: #fff; font-weight: 700; font-family: var(--font-display); }
/* A spinning gradient ring while someone is listening. */
.avatar-ring { position: relative; display: inline-grid; place-items: center; border-radius: 50%; padding: 2px; flex-shrink: 0; }
.avatar-ring.live::before {
content: ''; position: absolute; inset: 0; border-radius: 50%;
background: conic-gradient(from 0deg, #7b2ff7, #2f8cff, #34d399, #7b2ff7); animation: spin 3.5s linear infinite;
}
.avatar-ring > * { position: relative; border: 2px solid var(--bg); border-radius: 50%; }
@keyframes spin { to { transform: rotate(360deg); } }
.status { font-size: 0.85rem; color: var(--muted); display: inline-flex; align-items: center; gap: 0.45rem; }
.status-live { color: var(--live); font-weight: 600; }
/* Equalizer bars: dancing while playing, resting when paused. */
.eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 14px; }
.eq span { width: 3px; height: 100%; border-radius: 2px; background: currentColor; transform-origin: bottom; transform: scaleY(0.3); }
.eq.playing span { animation: eq 1s ease-in-out infinite alternate; }
.eq.playing span:nth-child(2) { animation-duration: 0.7s; animation-delay: -0.3s; }
.eq.playing span:nth-child(3) { animation-duration: 1.2s; animation-delay: -0.6s; }
.eq.playing span:nth-child(4) { animation-duration: 0.85s; animation-delay: -0.15s; }
@keyframes eq { 0% { transform: scaleY(0.25); } 50% { transform: scaleY(1); } 100% { transform: scaleY(0.45); } }
.track { display: flex; gap: 1rem; align-items: flex-start; }
.artwork {
width: 112px; height: 112px; border-radius: 14px; object-fit: cover; flex-shrink: 0; background: var(--surface-2);
box-shadow: 0 14px 30px -12px rgba(0, 0, 0, 0.75), 0 0 0 1px rgba(255, 255, 255, 0.08);
transition: transform 0.4s var(--spring);
}
.track:hover .artwork { transform: scale(1.04) rotate(-1deg); }
.artwork-empty { display: grid; place-items: center; font-size: 2rem; color: rgba(255, 255, 255, 0.85); background: linear-gradient(135deg, #4a1278, #1872cf); }
.artwork-sm { width: 44px; height: 44px; border-radius: 9px; object-fit: cover; flex-shrink: 0; box-shadow: 0 6px 14px -8px rgba(0, 0, 0, 0.8); }
.track-body { min-width: 0; flex: 1; }
.track-title { font-family: var(--font-display); font-weight: 700; font-size: 1.15rem; letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.track-artist { color: var(--text); opacity: 0.82; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.progress { margin-top: 0.85rem; }
.progress-track { height: 4px; border-radius: 999px; background: rgba(255, 255, 255, 0.14); overflow: hidden; }
.progress-fill { height: 100%; border-radius: inherit; background: var(--grad); transition: width 1s linear; }
.progress-times { display: flex; justify-content: space-between; margin-top: 0.3rem; font-size: 0.75rem; color: var(--muted); font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) { .progress-fill { transition: none; } }
.track-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-top: 0.8rem; }
.service-link {
font-size: 0.76rem; color: var(--text); text-decoration: none; padding: 0.2rem 0.55rem; border-radius: 999px;
background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.08); transition: background 0.2s, transform 0.25s var(--spring);
}
.service-link:hover { background: rgba(255, 255, 255, 0.16); transform: translateY(-1px); }
.service-link.inexact { opacity: 0.55; }
.profile-header { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.75rem; padding: 0.5rem 0 0.75rem; }
.profile-header h1 { font-size: 2.1rem; }
.profile-header .bio { margin: 0; max-width: 30rem; }
.stats { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }
.stats span { padding: 0.3rem 0.8rem; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--border); color: var(--muted); font-size: 0.88rem; }
.stats strong { color: var(--text); font-family: var(--font-display); }
.featured blockquote {
margin: 0.9rem 0 0; padding: 0.6rem 0.9rem; border-left: 3px solid transparent; font-style: italic;
border-image: var(--grad) 1; background: rgba(255, 255, 255, 0.04);
}
.search { position: relative; }
.search-results { list-style: none; margin: 0.5rem 0 0; padding: 0.4rem; background: var(--surface-solid); border: 1px solid var(--border); border-radius: 14px; }
.search-results li { padding: 0.4rem 0.45rem; border-radius: 10px; transition: background 0.15s; }
.search-results li:hover { background: var(--surface-2); }
.result { display: flex; gap: 0.6rem; align-items: center; background: none; border: 0; color: inherit; font: inherit; cursor: pointer; width: 100%; text-align: left; padding: 0; }
.recent { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.recent-row { display: flex; align-items: center; gap: 0.8rem; padding: 0.5rem 0.4rem; text-decoration: none; border-radius: 12px; transition: background 0.15s, transform 0.25s var(--spring); }
a.recent-row:hover { background: var(--surface-2); transform: translateX(3px); }
.recent li + li { border-top: 1px solid var(--border); }
.recent-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.recent-text > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-title { font-weight: 600; }
.recent-time { font-size: 0.8rem; white-space: nowrap; }
.artwork-sm.artwork-empty { display: grid; place-items: center; background: var(--surface-2); color: var(--muted); }
.qr-card { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; text-align: center; }
.qr { border-radius: 14px; box-shadow: 0 20px 50px -20px rgba(47, 140, 255, 0.6); }
.credit { margin: 0.6rem 0 0; font-size: 0.8rem; }
.picked { display: flex; gap: 0.75rem; align-items: center; }
.connections { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem; }
.connection { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.85rem 0; border-top: 1px solid var(--border); flex-wrap: wrap; }
.connection form input { width: 160px; }
.connection.disabled { opacity: 0.55; }
.steps { margin: 0.5rem 0; padding-left: 1.2rem; display: flex; flex-direction: column; gap: 0.35rem; }
.jellyfin-form { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-top: 0.6rem; }
.jellyfin-form input[name='serverUrl'] { grid-column: 1 / -1; }
.jellyfin-form .btn { justify-self: start; }
.webhook { margin-top: 0.4rem; font-size: 0.85rem; }
.webhook summary { cursor: pointer; color: var(--muted); }
.webhook p { margin: 0.35rem 0; }
.webhook code { font-size: 0.75rem; background: var(--surface-2); padding: 0.3rem 0.5rem; border-radius: 6px; word-break: break-all; flex: 1; min-width: 0; }
@media (max-width: 480px) { .jellyfin-form { grid-template-columns: 1fr; } }
.hero { padding: 4.5rem 0 3rem; display: flex; flex-direction: column; gap: 1.25rem; }
.hero h1 { font-size: 3.2rem; letter-spacing: -0.04em; line-height: 1.02; }
.gradient-text {
background: linear-gradient(90deg, #a78bfa, #7dd3fc, #60a5fa, #a78bfa); background-size: 300% 100%;
-webkit-background-clip: text; background-clip: text; color: transparent; animation: shimmer 8s linear infinite;
}
@keyframes shimmer { to { background-position: 300% 0; } }
.hero p { color: var(--muted); font-size: 1.15rem; margin: 0; max-width: 34rem; }
.hero .eq { height: 42px; gap: 5px; color: #8b9dff; }
.hero .eq span { width: 8px; border-radius: 4px; }
@media (max-width: 480px) {
.artwork { width: 84px; height: 84px; }
.nav-links { gap: 0.75rem; font-size: 0.9rem; }
.hero h1 { font-size: 2.3rem; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}