Files
muzick/frontend/src/index.css
T
kami 93619824d8
Typecheck / typecheck (backend) (push) Has been cancelled
Typecheck / typecheck (workers) (push) Has been cancelled
feat(ui): rework every page for comfort, and make a seeded Vibe play its seed
A pass over the whole app against the Ethos laws, then a focused pass on
Vibe with the operator reviewing each change.

Across the app:
- The player bar restores the last track it played, paused at zero, so a
  fresh tab opens on where the listener was instead of "nothing playing".
- Track titles link to their album, matching the artist links beside them.
  Playback stays on the artwork tile; a title that played was the surprise.
- The search field is bg-bg2. Tailwind cannot alpha-modify these var()
  colors, so bg-surface0/70 emitted no rule at all and the input fell back
  to the UA's white.
- Row hover is light falling off to the right, not a flat slab.
- The artwork placeholder can drop its note glyph, so TrackRow no longer
  layers a play icon on top of one.

Vibe:
- A seeded Vibe plays its seed first. The seed sits in front of the durable
  plan without being part of it, so the first advance consumes it locally
  and reports no plan feedback.
- The queue drops a second recording of a song it already holds — same
  title, different track id, which id-based dedup let through.
- Up next is read from the queue rather than the plan preview, since the
  seed is not a plan item.
- The header carries the live profile (energy, discovery, goal) and both
  verbs. Keep is gone: letting a track finish already reports `completed`,
  which the director weighs the same.
- The aura is one warm diffuse blob in the page background, warm-hued only
  and quieter on mobile.
- Compact artwork is 32px. It was h-8 w-8, which this remapped spacing
  scale renders as 64px inside a 44px row, and that overflow was the
  "stacked" look.

Verified by render at 1440x900 and 390x844, no horizontal overflow at
either. 26 frontend and 122 backend tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:53:46 +04:00

440 lines
14 KiB
CSS

@tailwind base;
@tailwind components;
@tailwind utilities;
/* ── Self-hosted fonts (no CDN — see Ethos threat model) ─────────────────── */
@font-face {
font-family: 'Geist';
src: url('/fonts/Geist-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('/fonts/Geist-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('/fonts/Geist-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('/fonts/Geist-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('/fonts/Geist-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('/fonts/GeistMono-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('/fonts/GeistMono-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
/* ── Ethos Design Tokens (default dark) ────────────────────────────────────── */
:root {
/* warm brown-tinted neutral ramp — Ethos law, not cool grey */
--ethos-bg0: #14110D;
--ethos-bg1: #1B1712;
--ethos-bg2: #221D17;
--ethos-surface0: #2C261D;
--ethos-surface1: #372F24;
--ethos-surface2: #423927;
--ethos-border: rgba(244,234,220,0.09);
--ethos-border-hi: rgba(244,234,220,0.16);
--ethos-text: #F4EEE4;
--ethos-secondary: #B4AA98;
--ethos-muted: #756C5C;
--ethos-disabled: #5a5347;
/* Mono default (Ethos law 1) — machine values sit a step above `muted` so a
count or a duration stays legible without competing with the human label. */
--ethos-machine: #9C917D;
/* muzick fingerprint: honey amber */
--ethos-accent: #EDA24E;
--ethos-accent-hover: #E08F32;
--ethos-on-accent: #14110D;
/* semantic colors */
--ethos-green: #22c55e;
--ethos-amber: #eab308;
--ethos-red: #ef4444;
--ethos-purple: #a855f7;
--ethos-cyan: #22d3ee;
--ethos-orange: #f97316;
}
/* ── Base reset ───────────────────────────────────────────────────────────── */
body {
margin: 0;
padding: 0;
background-color: var(--ethos-bg0);
color: var(--ethos-text);
font-family: 'Geist', -apple-system, BlinkMacSystemFont,
'Segoe UI', 'Roboto', sans-serif;
font-size: 14px;
line-height: 20px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
position: relative;
z-index: 1;
min-height: 100dvh;
}
button,
a,
input[type='range'] {
-webkit-tap-highlight-color: transparent;
}
@media (pointer: coarse) {
input[type='range']::-webkit-slider-thumb {
width: 18px;
height: 18px;
margin-top: -7px;
}
input[type='range']::-moz-range-thumb {
width: 18px;
height: 18px;
}
}
/* ── Focus ring: 2px accent, for keyboard users only ──────────────────────── */
:focus-visible {
outline: 2px solid var(--ethos-accent);
outline-offset: 2px;
border-radius: 4px;
}
/* ── Selection ────────────────────────────────────────────────────────────── */
::selection {
background: color-mix(in srgb, var(--ethos-accent) 35%, transparent);
color: var(--ethos-text);
}
/* ── Scrollbars (thin, themed) ────────────────────────────────────────────── */
* {
scrollbar-width: thin;
scrollbar-color: var(--ethos-border) transparent;
}
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--ethos-border) 80%, transparent);
border-radius: 9999px;
border: 2px solid transparent;
background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--ethos-muted) 60%, transparent);
background-clip: padding-box;
}
/* ── Smooth scroll for main content ───────────────────────────────────────── */
html { scroll-behavior: smooth; }
/* ── Utility classes ──────────────────────────────────────────────────────── */
/* Surfaced bar (top bar / playback bar) — depth from light, not blur */
.glass {
background: var(--ethos-bg1);
border-top: 1px solid var(--ethos-border);
box-shadow: 0 2px 8px rgba(0,0,0,.35), 0 12px 32px rgba(0,0,0,.28);
}
/* Hide scrollbars but keep scroll behavior */
.scrollbar-hide {
scrollbar-width: none;
-ms-overflow-style: none;
}
.scrollbar-hide::-webkit-scrollbar { display: none; }
/* Skeleton shimmer */
@keyframes shimmer {
0% { opacity: 0.4; }
50% { opacity: 0.8; }
100% { opacity: 0.4; }
}
.skeleton {
animation: shimmer 1.5s ease-in-out infinite;
background: var(--ethos-surface1);
border-radius: 4px;
}
/* Vibe's profile aura: a small, old-player-inspired creature whose color and
movement come from the active recommendation state. */
@keyframes vibe-aura-pulse {
0%, 100% { transform: scale(var(--vibe-scale)) rotate(-7deg); filter: brightness(.9); }
50% { transform: scale(calc(var(--vibe-scale) * 1.12)) rotate(8deg); filter: brightness(1.28); }
}
@keyframes vibe-aura-orbit {
to { transform: rotate(360deg); }
}
@keyframes vibe-aura-spark {
0%, 100% { transform: translateY(0) scale(.75); opacity: .38; }
50% { transform: translateY(-5px) scale(1.15); opacity: 1; }
}
.vibe-aura-halo {
position: absolute;
inset: 7px;
border-radius: 9999px;
background: hsl(var(--vibe-hue) 88% 62% / .22);
filter: blur(12px);
animation: vibe-aura-pulse var(--vibe-pulse) ease-in-out infinite;
}
.vibe-aura-core {
position: relative;
width: 30px;
height: 36px;
border: 1px solid hsl(var(--vibe-hue) 95% 86% / .72);
border-radius: 46% 54% 58% 42% / 43% 45% 55% 57%;
background: radial-gradient(circle at 36% 28%, hsl(var(--vibe-hue) 100% 93%), hsl(var(--vibe-hue) 88% 61%) 38%, hsl(calc(var(--vibe-hue) + 35) 68% 31%) 100%);
box-shadow: inset 3px 2px 8px hsl(0 0% 100% / .34), 0 0 17px hsl(var(--vibe-hue) 92% 59% / .78);
animation: vibe-aura-pulse var(--vibe-pulse) ease-in-out infinite;
}
.vibe-aura-orbit {
position: absolute;
width: 47px;
height: 47px;
border: 1px solid hsl(var(--vibe-hue) 90% 76% / .36);
border-radius: 47% 53% 43% 57%;
animation: vibe-aura-orbit var(--vibe-orbit) linear infinite;
}
.vibe-aura-orbit-two {
width: 56px;
height: 37px;
border-color: hsl(calc(var(--vibe-hue) + 55) 90% 76% / .24);
animation-direction: reverse;
animation-duration: calc(var(--vibe-orbit) * 1.45);
}
.vibe-aura-heart,
.vibe-aura-spark {
position: absolute;
display: block;
border-radius: 9999px;
background: hsl(0 0% 100% / .9);
box-shadow: 0 0 8px hsl(0 0% 100% / .92);
}
.vibe-aura-heart { width: 7px; height: 7px; left: 11px; top: 15px; }
.vibe-aura-spark { width: 4px; height: 4px; animation: vibe-aura-spark 2.1s ease-in-out infinite; }
.vibe-aura-spark-one { left: -4px; top: 5px; }
.vibe-aura-spark-two { right: -3px; bottom: 7px; animation-delay: -1s; }
/* Ambient variant: one soft organic blob of warm light behind the page. The
scaled-up creature read as concentric rings at that size, which fought the
content instead of sitting behind it. Gradient goes on the sized element —
no inset-0 fill layer. */
@keyframes vibe-aura-drift {
from { transform: scale(var(--vibe-scale)) translate3d(-2%, -1%, 0) rotate(-4deg); }
to { transform: scale(calc(var(--vibe-scale) * 1.12)) translate3d(3%, 2%, 0) rotate(5deg); }
}
.vibe-aura-blob {
border-radius: 46% 54% 62% 38% / 55% 43% 57% 45%;
background:
radial-gradient(closest-side at 40% 36%, hsl(var(--vibe-hue) 92% 60% / .55), transparent 72%),
radial-gradient(closest-side at 66% 64%, hsl(calc(var(--vibe-hue) + 28) 86% 52% / .4), transparent 74%);
filter: blur(44px);
animation: vibe-aura-drift var(--vibe-orbit) ease-in-out infinite alternate;
}
@media (min-width: 640px) {
.vibe-aura-blob { filter: blur(72px); }
}
@media (prefers-reduced-motion: reduce) {
.vibe-aura-halo, .vibe-aura-core, .vibe-aura-orbit, .vibe-aura-spark, .vibe-aura-blob { animation: none; }
}
/* ── Component base classes ────────────────────────────────────────────────── */
/* Card surface — standard container */
.card-surface {
display: flex;
flex-direction: column;
gap: 12px;
border-radius: 8px;
border: 1px solid var(--ethos-border);
background: var(--ethos-surface0);
padding: 12px;
transition: background 150ms ease, border-color 150ms ease;
}
.card-surface:hover {
background: var(--ethos-surface1);
border-color: color-mix(in srgb, var(--ethos-accent) 40%, transparent);
}
/* Track list — one dense, hairline-separated column everywhere a list of tracks
appears. Replaces the per-page `space-y-1` around bordered row cards, which
cost 90px per track and fit only 7 rows on a 900px screen. */
.track-list > * + * {
border-top: 1px solid color-mix(in srgb, var(--ethos-border) 60%, transparent);
}
/* Row hover: light falling off to the right, not a filled slab. A flat
770px-wide rectangle with a hard right edge is the ugly version, and it also
squares off the Vibe timeline's stacked artwork. */
.track-row:hover {
background: linear-gradient(
90deg,
color-mix(in srgb, var(--ethos-surface1) 85%, transparent) 0%,
color-mix(in srgb, var(--ethos-surface1) 40%, transparent) 38%,
transparent 78%
);
}
/* Artwork frame */
.artwork-frame {
aspect-ratio: 1 / 1;
border-radius: 8px;
overflow: hidden;
}
/* Play button overlay */
.play-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
justify-content: flex-end;
padding: 8px;
background: linear-gradient(to top, rgba(0,0,0,0.5), transparent 60%);
opacity: 0;
transition: opacity 150ms ease;
}
.group:hover .play-overlay { opacity: 1; }
.play-overlay-btn {
width: 44px;
height: 44px;
border-radius: 9999px;
background: var(--ethos-accent);
display: flex;
align-items: center;
justify-content: center;
color: var(--ethos-on-accent);
box-shadow: 0 8px 24px -4px color-mix(in srgb, var(--ethos-accent) 40%, transparent);
transform: translateY(8px);
transition: transform 150ms ease;
}
.group:hover .play-overlay-btn { transform: translateY(0); }
/* Settings library actions use a compact, stateful button rather than an
unstyled native control. */
.admin-btn {
display: inline-flex;
min-height: 36px;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid var(--ethos-border-hi);
border-radius: 8px;
padding: 8px 12px;
background: var(--ethos-surface1);
color: var(--ethos-text);
font-size: 13px;
font-weight: 500;
transition: background 150ms ease, border-color 150ms ease, color 150ms ease;
}
.admin-btn:hover:not(:disabled) {
background: var(--ethos-surface2);
border-color: color-mix(in srgb, var(--ethos-accent) 50%, transparent);
}
.admin-btn:disabled { cursor: wait; opacity: 0.65; }
.admin-btn--done { border-color: color-mix(in srgb, var(--ethos-green) 60%, transparent); color: var(--ethos-green); }
.admin-btn--error { border-color: color-mix(in srgb, var(--ethos-red) 60%, transparent); color: var(--ethos-red); }
/* Keep row actions available on touch. On pointer-and-hover devices, reveal
them when the row is hovered or any of its controls receives focus. */
.track-row-actions { opacity: 1; transition: opacity 150ms ease; }
@media (hover: hover) and (pointer: fine) {
.track-row-actions { opacity: 0; }
.group:hover .track-row-actions,
.group:focus-within .track-row-actions { opacity: 1; }
}
/* Transport button */
.transport-btn {
width: 40px;
height: 40px;
border-radius: 9999px;
background: var(--ethos-accent);
display: flex;
align-items: center;
justify-content: center;
color: var(--ethos-on-accent);
transition: background 150ms ease, transform 150ms ease;
}
.transport-btn:hover { background: var(--ethos-accent-hover); transform: scale(1.06); }
.transport-btn:active { transform: scale(0.94); }
.transport-btn:disabled { opacity: 0.4; }
/* Gradient text — used sparingly for page headings */
.text-gradient {
background: linear-gradient(120deg, var(--ethos-text), color-mix(in srgb, var(--ethos-accent) 75%, var(--ethos-text)));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* Range slider styling */
input[type='range'] {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
input[type='range']::-webkit-slider-runnable-track {
height: 4px;
border-radius: 9999px;
background: color-mix(in srgb, var(--ethos-border) 90%, transparent);
}
input[type='range']::-moz-range-track {
height: 4px;
border-radius: 9999px;
background: color-mix(in srgb, var(--ethos-border) 90%, transparent);
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 9999px;
background: var(--ethos-accent);
margin-top: -4px;
transition: transform 150ms ease;
}
input[type='range']:hover::-webkit-slider-thumb { transform: scale(1.15); }
input[type='range']::-moz-range-thumb {
width: 12px; height: 12px; border: none;
border-radius: 9999px; background: var(--ethos-accent);
}
input[type='range']:focus-visible {
outline: none;
}
input[type='range']:focus-visible::-webkit-slider-thumb {
box-shadow: 0 0 0 6px color-mix(in srgb, var(--ethos-accent) 30%, transparent);
}
input[type='range']:focus-visible::-moz-range-thumb {
box-shadow: 0 0 0 6px color-mix(in srgb, var(--ethos-accent) 30%, transparent);
}