Files
SoulStream/src/mobile/shell.css
T
KeelrLeek f2cde4ec7c android: the whole discography, on a page of its own
The artist page already filed every release the desktop's way, but it only
ever showed one category at a time and it showed all of it: no slice anywhere
in that grid, so an artist with 140 releases drew 140 cards. There is a page
now — All in front of the categories, a sort, a grouping by year, and the
editions opened in place under their own record, which is the part a flat
list loses — and the grid behind it stops at twelve with the way down under
it. It is the same classifier twice over the same cached albums, which the
desktop cannot do because its filing waits on a background description
harvest; the three things its own page has and this one does not — a
tracklist per record, an accent sampled per cover, the scroll-led banner —
are 140 calls and 140 decodes over mobile data for rows that are one tap
away, and ANDROID.md says so rather than leaving them looking forgotten.

The list caps at sixty with the rest a button away, before the grouping, so
the last year on screen can be a part of itself; under a name order By year
is not offered, because the years would repeat out of sequence. The mock
gains a 140-record artist so both caps are walked rather than argued about.
2026-09-14 03:41:16 -05:00

1664 lines
89 KiB
CSS

/* ═══════════════════════════════════════════════════════════════════════════
SoulStream on Android — the shell and the shared vocabulary.
research/mobile-mockup's app.css / player.css / layouts.css, the phone
layout, with the workbench and the other layouts left out. Every name has
a desktop ancestor, noted where it matters. Global on purpose: this is
the phone's own document, and a vocabulary shared across screens is not
a component's private style.
═══════════════════════════════════════════════════════════════════════════ */
.app {
position: relative;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--bg-base);
}
/* ── the page ground ──────────────────────────────────────────────────────
The desktop's dark-glass canvas, restated for a device with no compositor
blur to borrow: the artwork in play, blown up and blurred, is the light
source. --ambience is how much of it the ground admits. Two layers, so a
change of record is a crossfade rather than a cut (background-image does
not transition). */
.app-bg {
position: absolute;
inset: 0;
z-index: 0;
background: var(--bg-base);
overflow: hidden;
pointer-events: none;
}
.amb {
position: absolute;
inset: -22%;
background-size: cover;
background-position: center;
filter: blur(56px) saturate(1.5);
opacity: 0;
transform: scale(1.25);
transition: opacity 620ms ease;
will-change: opacity;
}
.amb.on { opacity: var(--ambience); }
.amb-tint {
position: absolute;
inset: 0;
background: linear-gradient(180deg,
rgba(10, 10, 12, 0.34) 0%,
rgba(10, 10, 12, 0.72) 46%,
rgba(10, 10, 12, 0.93) 100%);
}
/* the desktop's .feat-grain: a fixed noise tile at low alpha so a huge flat
gradient does not band on an OLED */
.grain {
position: absolute;
inset: 0;
pointer-events: none;
opacity: var(--grain);
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/></filter><rect width='120' height='120' filter='url(%23n)' opacity='0.5'/></svg>");
mix-blend-mode: overlay;
}
/* ── the scroll pane ───────────────────────────────────────────────────── */
.pane {
position: relative;
z-index: 1;
flex: 1;
min-height: 0;
}
.viewport {
position: absolute;
inset: 0;
overflow: hidden;
}
.screen {
position: absolute;
inset: 0;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
padding-top: var(--inset-top);
}
.screen::-webkit-scrollbar { width: 0; display: none; }
/* a pushed screen whose content starts at the very top must clear the bar;
heroes bleed behind it and the record head has its own clearance */
.screen.under-bar { padding-top: var(--top-h); }
/* the pad that clears the mini player + nav bar */
.endpad { height: calc(var(--nav-h) + var(--inset-bottom) + 24px); }
.has-mini .endpad { height: calc(var(--mini-h) + var(--nav-h) + var(--inset-bottom) + 16px); }
/* ── the glass bar ────────────────────────────────────────────────────────
Direct descendant of GlassBar.svelte: same tint, same 12px blur at
saturate(1.7), same top-light hairline. A phone has a system back gesture
and no room for a hierarchy trail, so the trail collapses to a KICKER
under the title, and the arrows to one chevron that only appears when
there is somewhere to go back to. It runs up under the status bar. */
.gbar {
position: absolute;
top: 0; left: 0; right: 0;
z-index: 40;
height: var(--top-h);
padding: var(--inset-top) 8px 0 6px;
display: flex;
align-items: center;
gap: 10px;
/* SOLID by default on the phone. The desktop's glass — the 28% tint over
a 12px backdrop blur — was measured on the Pixel 6a (tests/mobile/
perf.mjs): every backdrop blur re-blurs its band on every frame that
scrolls or animates under it, and the three of them (this bar, the mini
bar, the nav) turned a 60 Hz scroll into 30 Hz and dropped every other
frame of the player's opening. The blur is a switch (You → Appearance →
Glass) so the two can be compared; the design was built to stand on
the solid floor first. */
background: rgba(18, 18, 22, 0.92);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05);
opacity: 0;
transition: opacity 180ms ease;
pointer-events: none;
}
.gbar.on { opacity: 1; pointer-events: auto; }
html[data-glass="blur"] .gbar { background: rgba(20, 20, 24, 0.28); backdrop-filter: blur(12px) saturate(1.7); -webkit-backdrop-filter: blur(12px) saturate(1.7); }
html[data-material="solid"] .gbar { background: rgba(24, 24, 24, 0.94); backdrop-filter: none; -webkit-backdrop-filter: none; }
.gbar .gb-t {
font-size: 15px;
font-weight: 800;
letter-spacing: -0.2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
flex: 1;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
/* the bar arrives before its title does */
opacity: 0;
transform: translateY(5px);
transition: opacity 200ms ease, transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
.gbar.titled .gb-t { opacity: 1; transform: none; }
.gbar .gb-t small {
display: block;
font-size: 10.5px;
font-weight: 700;
color: rgba(255, 255, 255, 0.55);
letter-spacing: 0.2px;
margin-top: -1px;
}
/* the round chrome button — the desktop bar's .arrow at touch size */
.iconbtn {
display: grid;
place-items: center;
width: var(--tap);
height: var(--tap);
border-radius: 50%;
color: #fff;
flex: none;
transition: background var(--m-fast), transform var(--m-fast);
}
.iconbtn:active { background: rgba(255, 255, 255, 0.12); transform: scale(0.92); }
.iconbtn.sm { width: 38px; height: 38px; }
.iconbtn.muted { color: var(--text-secondary); }
.iconbtn.on { color: var(--accent-hover); }
/* ── large page header (the un-scrolled state) ────────────────────────── */
.phead { padding: 14px var(--pad) 6px; position: relative; z-index: 2; }
.phead .kick {
font-size: 11.5px;
font-weight: 800;
letter-spacing: 0.8px;
text-transform: uppercase;
color: var(--accent-hover);
margin-bottom: 3px;
}
.phead h1 { margin: 0; font-size: 27px; font-weight: 800; letter-spacing: -0.7px; line-height: 1.1; }
.phead .row { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 0; padding: 0; }
.phead .row:active { background: none; }
.phead .acts { display: flex; }
/* ── section heads — the desktop's ax-head / ax-title / ax-note ────────── */
.sec {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
padding: 22px var(--pad) 11px;
}
.sec h2 { margin: 0; font-size: 17px; font-weight: 800; letter-spacing: -0.25px; min-width: 0; }
.sec h2 em { font-style: normal; font-size: 12.5px; font-weight: 650; color: var(--text-muted); letter-spacing: 0; }
.sec .all {
font-size: 12px;
font-weight: 750;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 1px;
flex: none;
padding: 6px 2px 6px 8px;
}
.sec .all:active { color: #fff; }
.sec-note { padding: 0 var(--pad) 4px; font-size: 12.5px; color: var(--text-muted); line-height: 1.5; }
/* ── horizontal rails ─────────────────────────────────────────────────── */
.rail {
display: flex;
gap: 13px;
overflow-x: auto;
scroll-snap-type: x proximity;
padding: 0 var(--pad) 2px;
scrollbar-width: none;
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { scroll-snap-align: start; flex: none; }
/* ── the release card — Home.svelte's .release, at phone scale ──────────── */
.release { width: 138px; text-align: left; display: block; }
.release.w2 { width: 100%; }
.cover-wrap {
position: relative;
display: block;
aspect-ratio: 1;
border-radius: var(--r-art);
overflow: hidden;
background: var(--surface);
box-shadow: var(--ring), 0 6px 18px -8px rgba(0, 0, 0, 0.7);
transition: transform var(--m-base);
}
html[data-material="solid"] .cover-wrap { background: var(--bg-elevated); }
.cover-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; animation: img-in 300ms ease both; }
@keyframes img-in {
from { opacity: 0; transform: scale(1.04); }
to { opacity: 1; transform: none; }
}
.cover-wrap .ph { position: absolute; inset: 0; display: grid; place-items: center; color: var(--text-muted); }
.release .title {
display: -webkit-box;
font-size: 14px;
font-weight: 700;
margin-top: 8px;
line-height: 1.28;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.release .sub {
display: block;
font-size: 12.5px;
color: var(--text-muted);
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.release:active .cover-wrap { transform: scale(0.965); transition: transform 90ms ease; }
/* the edition badge, straight from Home.svelte */
.ed-badge {
position: absolute;
top: 6px; right: 6px;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 4px;
line-height: 1;
padding: 4px 7px;
border-radius: 6px;
font-size: 10px;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: #fff;
background: color-mix(in srgb, var(--accent) 40%, rgba(10, 10, 12, 0.7));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
}
/* artist card — the desktop's ax-card, round on mobile */
.acard { width: 116px; text-align: center; display: block; }
.acard .por {
display: block;
width: 116px; height: 116px;
border-radius: 50%;
overflow: hidden;
background: var(--surface);
box-shadow: var(--ring);
margin: 0 auto;
}
.acard .por img { width: 100%; height: 100%; object-fit: cover; animation: img-in 300ms ease both; }
/* no picture: the app's own placeholder, centred, muted */
.acard .por { display: flex; align-items: center; justify-content: center; color: var(--text-muted); }
.acard .nm { display: block; font-size: 13.5px; font-weight: 700; margin-top: 9px; line-height: 1.25; }
.acard .kd { display: block; font-size: 12px; color: var(--text-muted); margin-top: 1px; }
/* ── grids: auto-fill, so one rule covers every width ─────────────────── */
.grid2 { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(148px, 46%), 1fr)); gap: 20px 14px; padding: 0 var(--pad); }
.grid2 > .release, .grid2 > .acard { width: 100%; }
.grid2 > .acard .por { width: 100%; height: auto; aspect-ratio: 1; }
/* ── list rows ────────────────────────────────────────────────────────── */
.row {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
min-height: var(--tap);
padding: 8px var(--pad);
text-align: left;
transition: background var(--m-fast);
}
.row:active { background: var(--hover); }
.row .art {
width: 46px; height: 46px;
border-radius: 7px;
overflow: hidden;
flex: none;
background: var(--surface);
box-shadow: var(--ring);
position: relative;
display: grid;
place-items: center;
color: var(--text-muted);
}
.row .art.round { border-radius: 50%; }
.row .art img { width: 100%; height: 100%; object-fit: cover; animation: img-in 260ms ease both; }
.row .b { min-width: 0; flex: 1; }
.row .t { display: block; font-size: 14.5px; font-weight: 650; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .m { display: block; font-size: 12.5px; color: var(--text-muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .r { flex: none; color: var(--text-muted); display: flex; align-items: center; gap: 4px; }
.row.playing .t { color: var(--accent-hover); }
.row .lead { width: 30px; display: grid; place-items: center; color: var(--text-secondary); flex: none; }
/* track rows carry a number instead of art */
.trk { display: flex; align-items: center; gap: 12px; width: 100%; min-height: 46px; padding: 5px var(--pad); text-align: left; transition: background var(--m-fast); }
.trk:active { background: var(--hover); }
.trk .n { width: 22px; flex: none; text-align: right; font-size: 13px; font-variant-numeric: tabular-nums; color: var(--text-muted); font-weight: 650; }
.trk .b { min-width: 0; flex: 1; }
.trk .t { display: block; font-size: 14.5px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.trk .m { display: block; font-size: 12px; color: var(--text-muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.trk .d { font-size: 12.5px; color: var(--text-muted); font-variant-numeric: tabular-nums; flex: none; }
.trk .r { flex: none; color: var(--text-muted); display: grid; place-items: center; width: 34px; height: 34px; margin-right: -8px; border-radius: 50%; }
.trk .r:active { background: var(--hover); }
.trk.playing .n { color: var(--accent); }
.trk.playing .t { color: var(--accent-hover); }
/* the playing marker: the desktop's little bars, at row size */
.trk.playing .n { font-size: 0; }
.trk.playing .n::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px var(--accent); }
/* ── buttons: the desktop's ax-primary / ax-ghost, at 48dp ─────────────── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
height: 40px;
padding: 0 17px;
border-radius: var(--r-pill);
font-size: 13.5px;
font-weight: 750;
white-space: nowrap;
transition: background var(--m-fast), transform var(--m-fast), color var(--m-fast);
}
.btn:active { transform: scale(0.955); }
.btn-fill { background: var(--accent); color: #04121a; }
.btn-fill:active { background: var(--accent-hover); }
.btn-ghost { background: rgba(255, 255, 255, 0.09); color: #fff; }
.btn-ghost:active { background: rgba(255, 255, 255, 0.16); }
.btn.wide { width: 100%; }
/* the play disc — the app's one play vocabulary: filled accent circle + play */
.disc {
display: grid;
place-items: center;
width: 46px; height: 46px;
border-radius: 50%;
background: var(--accent);
color: #04121a;
flex: none;
box-shadow: 0 6px 18px -6px color-mix(in srgb, var(--accent) 70%, transparent);
transition: transform var(--m-fast), background var(--m-fast);
}
.disc:active { transform: scale(0.92); background: var(--accent-hover); }
.disc.lg { width: 66px; height: 66px; }
.disc.sm { width: 36px; height: 36px; }
/* ── chips ────────────────────────────────────────────────────────────── */
.chips { display: flex; gap: 7px; overflow-x: auto; padding: 0 var(--pad); scrollbar-width: none; }
.chips::-webkit-scrollbar { display: none; }
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
height: 34px;
padding: 0 13px;
border-radius: var(--r-pill);
background: rgba(255, 255, 255, 0.08);
font-size: 12.5px;
font-weight: 700;
color: #fff;
white-space: nowrap;
flex: none;
transition: background var(--m-fast), color var(--m-fast), transform var(--spd-spatial-fast) var(--sp-spatial-fast);
}
.chip:active { transform: scale(0.94); }
.chip.on { background: var(--accent); color: #04121a; }
.chip.quiet { background: rgba(255, 255, 255, 0.055); color: var(--text-secondary); font-weight: 650; }
/* stat chips — Home's .stat-chips, glassier on mobile */
.stats { display: flex; flex-wrap: wrap; gap: 6px; }
.stats .s {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 9px;
border-radius: var(--r-pill);
background: rgba(0, 0, 0, 0.42);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
font-size: 11.5px;
font-weight: 750;
color: rgba(255, 255, 255, 0.92);
}
/* ── segmented tabs (Library's second tier) ───────────────────────────── */
.segtabs { display: flex; gap: 6px; overflow-x: auto; padding: 4px var(--pad) 10px; scrollbar-width: none; }
.segtabs::-webkit-scrollbar { display: none; }
.segtabs button {
padding: 7px 14px;
border-radius: var(--r-pill);
font-size: 13px;
font-weight: 750;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.055);
white-space: nowrap;
flex: none;
transition: background var(--m-fast), color var(--m-fast), transform var(--spd-spatial-fast) var(--sp-spatial-fast);
}
.segtabs button:active { transform: scale(0.94); }
.segtabs button.on { background: #fff; color: #101014; }
/* ── cards / groups ───────────────────────────────────────────────────── */
.card { background: var(--surface); box-shadow: var(--ring); border-radius: var(--r-card); overflow: hidden; }
.group { margin: 0 var(--pad) 14px; border-radius: var(--r-card); overflow: hidden; background: var(--surface); box-shadow: var(--ring); }
html[data-material="solid"] .card, html[data-material="solid"] .group { background: var(--bg-elevated); box-shadow: none; }
.group .row { padding: 11px 14px; }
.group .row + .row { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.055); }
.group .lead { width: 32px; display: grid; place-items: center; color: var(--accent-hover); flex: none; }
/* key / value rows — the shape a page wants when it is REPORTING */
.kv { display: flex; align-items: baseline; gap: 12px; padding: 9px 14px; min-height: 40px; }
.kv + .kv { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.055); }
.kv .k { font-size: 13px; color: var(--text-muted); font-weight: 600; flex: none; }
.kv .v { font-size: 13.5px; font-weight: 700; margin-left: auto; text-align: right; min-width: 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.kv .v.mono { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 12px; font-weight: 600; letter-spacing: -0.2px; }
.kv .v.dim { color: var(--text-secondary); font-weight: 600; }
/* the switch — M3 Expressive's, at the size the rest of this app sits at */
.sw {
width: 44px; height: 26px;
border-radius: var(--r-pill);
flex: none;
position: relative;
background: rgba(255, 255, 255, 0.16);
box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.14);
transition: background var(--m-base), box-shadow var(--m-base);
}
.sw > span {
position: absolute;
top: 4px; left: 4px;
width: 18px; height: 18px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.62);
transition: left var(--m-spring), width var(--m-spring), height var(--m-spring), top var(--m-spring), background var(--m-base);
}
.sw.on { background: var(--accent); box-shadow: none; }
.sw.on > span { left: 20px; top: 3px; width: 20px; height: 20px; background: #fff; }
/* tags — the format badge and its verdict colours */
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
min-height: 19px;
padding: 2px 7px;
line-height: 1.25;
border-radius: 5px;
font-size: 10.5px;
font-weight: 800;
letter-spacing: 0.3px;
background: rgba(255, 255, 255, 0.09);
color: var(--text-secondary);
}
.tag.hi { background: color-mix(in srgb, var(--accent) 26%, transparent); color: var(--accent-hover); }
.tag.good { background: rgba(61, 220, 132, 0.16); color: #3ddc84; }
/* ── the hero card — Home.svelte's .feat, restacked vertically ──────────── */
.feat {
position: relative;
margin: 6px var(--pad) 0;
border-radius: var(--r-card);
overflow: hidden;
min-height: 152px;
display: flex;
align-items: flex-end;
box-shadow: 0 12px 34px -14px rgba(0, 0, 0, 0.85);
}
/* the blow-up is BLURRED here, unlike the desktop's sharp banner: at 380px
wide a sharp cover behind text is noise; blurred it is the record's own
colour field, the trick the page ground uses */
.feat .bg { position: absolute; inset: -18%; background-size: cover; background-position: center; filter: blur(30px) saturate(1.25); transform: scale(1.2); }
.feat .tint {
position: absolute; inset: 0;
background:
linear-gradient(180deg, rgba(8, 8, 10, 0.42) 0%, rgba(8, 8, 10, 0.66) 55%, rgba(8, 8, 10, 0.84) 100%),
linear-gradient(90deg, rgba(8, 8, 10, 0.34) 0%, transparent 70%);
}
.feat .in { position: relative; z-index: 2; padding: 15px; width: 100%; }
.feat-top { display: flex; gap: 13px; align-items: flex-start; }
.feat-art {
display: block;
width: 78px; height: 78px;
flex: none;
border-radius: 9px;
overflow: hidden;
background: var(--bg-highlight);
box-shadow: 0 10px 22px -8px rgba(0, 0, 0, 0.85), inset 0 0 0 1px rgba(255, 255, 255, 0.14);
transition: transform var(--spd-spatial-fast) var(--sp-spatial-fast);
}
.feat-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.feat-art:active { transform: scale(var(--art-grow)); }
.feat .kick { font-size: 10.5px; font-weight: 800; letter-spacing: 0.9px; text-transform: uppercase; color: rgba(255, 255, 255, 0.7); }
.feat .nm { font-size: 21px; font-weight: 800; letter-spacing: -0.5px; margin: 2px 0 3px; line-height: 1.12; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.feat-sub { font-size: 12.5px; font-weight: 600; color: rgba(255, 255, 255, 0.68); }
.feat .stats { margin-top: 12px; }
.feat .acts { display: flex; gap: 8px; margin-top: 13px; }
/* ── the empty / stub markers ─────────────────────────────────────────── */
.stub {
margin: 10px var(--pad);
padding: 16px;
border-radius: var(--r-card);
background: rgba(255, 255, 255, 0.035);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
font-size: 12.5px;
line-height: 1.55;
color: var(--text-secondary);
}
.stub b { color: #fff; font-weight: 750; display: block; margin-bottom: 3px; font-size: 13px; }
/* ── the record head — album and playlist share it ────────────────────────
Stacked and centred, the cover as the page's own hero. The glass bar is
opaque from the first pixel on a pushed screen, so the cover starts below
it. */
.rec-head { padding: calc(var(--bar-h) + 14px) var(--pad) 0; display: flex; flex-direction: column; align-items: center; }
.rec-cover {
width: 66%;
max-width: 300px;
aspect-ratio: 1;
flex: none;
border-radius: var(--r-card);
overflow: hidden;
background: var(--surface);
box-shadow: 0 26px 54px -18px rgba(0, 0, 0, 0.9), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
display: grid;
place-items: center;
color: var(--text-muted);
}
.rec-cover img { width: 100%; height: 100%; object-fit: cover; display: block; animation: img-in 300ms ease both; }
.rec-meta { text-align: center; min-width: 0; width: 100%; }
.rec-title { font-size: 22px; font-weight: 800; letter-spacing: -0.4px; line-height: 1.2; margin-top: 18px; }
.rec-artist { font-size: 14.5px; font-weight: 700; color: var(--accent-hover); margin-top: 6px; display: inline-block; padding: 2px 6px; border-radius: 6px; }
.rec-artist:active { background: var(--hover); }
.rec-line { margin-top: 5px; font-size: 13px; font-weight: 500; color: var(--text-muted); }
.rec-chips { justify-content: center; margin-top: 9px; }
.rec-acts { display: flex; align-items: center; justify-content: center; gap: 16px; padding: 18px 0 6px; }
.disc-head { padding: 14px var(--pad) 4px; font-size: 13px; font-weight: 800; color: var(--text-muted); letter-spacing: 0.6px; text-transform: uppercase; }
/* ── the artist hero ──────────────────────────────────────────────────────
The desktop hero is a wide banner beside the identity block. A phone has
no beside, so the portrait goes full-bleed and the name sits ON it. */
.hero { position: relative; height: 300px; display: flex; align-items: flex-end; overflow: hidden; margin-bottom: -6px; }
.hero-img { position: absolute; inset: 0; background-size: cover; background-position: center 22%; transform: scale(1.04); }
/* the blurred SEED: the picture the library already has, soft and
oversized, until a sharp one exists */
.hero-img.seed { filter: blur(16px) saturate(1.15); transform: scale(1.12); }
/* the sharp one dissolves in over the seed when the catalog lookup lands */
.hero-img.dissolve { animation: hero-dissolve 720ms var(--sp-effects-default) both; }
@keyframes hero-dissolve {
from { opacity: 0; filter: blur(16px) saturate(1.15); transform: scale(1.12); }
to { opacity: 1; filter: blur(0) saturate(1); transform: scale(1.04); }
}
.hero-scrim {
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(10, 10, 12, 0.3) 0%, rgba(10, 10, 12, 0.1) 34%, rgba(10, 10, 12, 0.82) 82%, var(--bg-base) 100%),
linear-gradient(90deg, rgba(10, 10, 12, 0.42) 0%, transparent 58%);
}
.hero-in { position: relative; z-index: 2; padding: 0 var(--pad) 16px; width: 100%; }
.hero-name { font-size: 32px; font-weight: 800; letter-spacing: -0.9px; line-height: 1.04; text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55); }
/* ── mini player ──────────────────────────────────────────────────────────
The desktop PlayerBar is one 62px row; the phone keeps the height and the
single-row rule, floats it as a CARD above the nav, and gives the lane its
own strip along the bottom, inset past the corners. */
.mini {
position: absolute;
left: 8px; right: 8px;
bottom: calc(var(--nav-h) + var(--inset-bottom) + 6px);
z-index: 50;
height: var(--mini-h);
border-radius: 16px;
display: flex;
align-items: center;
gap: 11px;
padding: 0 10px 26px 8px; /* 4 above the lane, 16 of lane, 6 below */
background: rgba(28, 28, 33, 0.94);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.09), 0 10px 28px -10px rgba(0, 0, 0, 0.9);
overflow: hidden;
transition: transform var(--m-sheet), opacity 200ms ease;
touch-action: none;
}
html[data-glass="blur"] .mini { background: rgba(28, 28, 33, 0.72); backdrop-filter: blur(22px) saturate(1.6); -webkit-backdrop-filter: blur(22px) saturate(1.6); }
html[data-material="solid"] .mini { background: #1c1c20; backdrop-filter: none; -webkit-backdrop-filter: none; }
.mini .art { width: 40px; height: 40px; border-radius: 9px; overflow: hidden; flex: none; background: var(--bg-highlight); display: grid; place-items: center; color: var(--text-muted); }
.mini .art img { width: 100%; height: 100%; object-fit: cover; }
.mini .b { min-width: 0; flex: 1; }
/* chrome, like the notification shade: the two lines scale to 150% and stop */
.mini .t { display: block; font-size: min(13.5px, 18px); font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini .m { display: block; font-size: min(12px, 15px); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
.mini .m.live { color: var(--accent-hover); }
.mini .mini-lane { position: absolute; left: 16px; right: 16px; bottom: 6px; opacity: 0.95; pointer-events: none; }
.mini.hidden { transform: translateY(140%); opacity: 0; pointer-events: none; }
/* ── bottom nav ───────────────────────────────────────────────────────── */
.nav {
position: absolute;
left: 0; right: 0; bottom: 0;
z-index: 55;
height: calc(var(--nav-h) + var(--inset-bottom));
padding-bottom: var(--inset-bottom);
display: flex;
background: rgba(16, 16, 19, 0.92);
box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.06);
transition: transform var(--m-sheet);
}
html[data-glass="blur"] .nav { background: rgba(16, 16, 19, 0.82); backdrop-filter: blur(26px) saturate(1.5); -webkit-backdrop-filter: blur(26px) saturate(1.5); }
html[data-material="solid"] .nav { background: #101013; backdrop-filter: none; -webkit-backdrop-filter: none; }
.nav.gone { transform: translateY(100%); }
.nav button {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
color: var(--text-muted);
transition: color var(--m-fast);
position: relative;
padding-top: 2px;
}
.nav button .lb { font-size: 10.5px; font-weight: 750; letter-spacing: 0.1px; }
.nav button.on { color: #fff; }
/* the pill is the one element that should visibly overshoot: it is the app
telling you where you just landed */
.nav button .pill {
position: absolute;
top: 4px;
width: 46px; height: 26px;
border-radius: var(--r-pill);
background: var(--accent);
opacity: 0;
transform: scaleX(0.5);
transition: opacity var(--m-fast), transform var(--spd-spatial-fast) var(--sp-spatial-fast);
}
.nav button.on .pill { opacity: 0.22; transform: none; }
.nav button .gl { position: relative; z-index: 1; height: 26px; display: grid; place-items: center; transition: transform var(--spd-spatial-fast) var(--sp-spatial-fast); }
.nav button:active .gl { transform: scale(0.86); }
.nav button.on .gl { color: var(--accent-hover); }
/* ── the player sheet ─────────────────────────────────────────────────────
Two arrivals. Tapping the mini bar is a CONTAINER TRANSFORM: the artwork
flies from the bar to the sheet and the sheet fades in around it, which is
why .morphing suppresses the slide. Anything else slides up on the
spatial spring. */
.sheet {
position: absolute;
inset: 0;
z-index: 90;
display: flex;
flex-direction: column;
transform: translateY(100%);
transition: transform var(--spd-spatial-default) var(--sp-spatial-default);
overflow: hidden;
}
.sheet.up { transform: none; }
/* the drag handles own their touches; the faces underneath still scroll */
.np-head, .np-art { touch-action: none; }
.sheet.morphing {
transition: opacity var(--spd-effects-default) var(--sp-effects-default);
transform: none;
opacity: 0;
}
.sheet.morphing.up { opacity: 1; }
.sheet .sheet-bg { position: absolute; inset: 0; z-index: 0; background: var(--bg-base); overflow: hidden; }
/* the one screen entirely about the record gets the most of the artwork:
the blow-up runs brighter and the veil is lighter than any browsing page's;
the radial keeps the middle darker so the cover reads as lit, not fogged */
.sheet .sheet-bg .amb { opacity: calc(var(--ambience) * 1.05); filter: blur(64px) saturate(1.45); transition: none; }
.sheet .sheet-bg .amb-tint {
background:
radial-gradient(125% 72% at 50% 28%, rgba(10, 10, 12, 0.34) 0%, rgba(10, 10, 12, 0.64) 58%, rgba(10, 10, 12, 0.82) 100%),
linear-gradient(180deg, rgba(10, 10, 12, 0.3) 0%, rgba(10, 10, 12, 0.42) 48%, rgba(10, 10, 12, 0.72) 100%);
}
.np {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
height: 100%;
padding-top: var(--inset-top); /* the OS status bar stays on top of us */
padding-bottom: calc(var(--inset-bottom) + 6px);
}
.np-head { display: flex; align-items: center; gap: 8px; padding: 4px 6px; flex: none; }
.np-ctx { flex: 1; min-width: 0; text-align: center; border-radius: 10px; padding: 2px 6px; transition: background var(--m-fast); }
.np-ctx:active { background: rgba(255, 255, 255, 0.08); }
.np-ctx .k { font-size: 10.5px; font-weight: 800; letter-spacing: 0.9px; text-transform: uppercase; color: rgba(255, 255, 255, 0.55); }
.np-ctx .v { font-size: 13px; font-weight: 750; margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-art { flex: none; display: grid; place-items: center; padding: 12px var(--pad) 0; transition: padding var(--m-sheet); }
.np-frame {
width: 100%;
aspect-ratio: 1;
border-radius: 16px;
overflow: hidden;
background: var(--bg-highlight);
box-shadow: 0 34px 60px -22px rgba(0, 0, 0, 0.92), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
transition: width var(--m-sheet), border-radius var(--m-sheet);
display: grid;
place-items: center;
color: var(--text-muted);
}
.np-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* the artwork demotes to a thumbnail when a face takes the room */
.np-art.small { padding: 6px var(--pad) 2px; justify-items: start; }
.np-art.small .np-frame { width: 62px; border-radius: 10px; }
/* all the vertical slack goes BELOW the artwork, never above it: the record
hangs from the top and the controls land about a third up, where a thumb is */
.np-spacer { flex: 1; min-height: 0; display: flex; flex-direction: column; justify-content: flex-end; gap: 12px; padding: 14px var(--pad) 4px; }
.np-next {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
height: 42px;
padding: 0 12px;
border-radius: var(--r-pill);
background: rgba(255, 255, 255, 0.07);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
font-size: 13px;
color: rgba(255, 255, 255, 0.72);
transition: background var(--m-fast);
}
.np-next:active { background: rgba(255, 255, 255, 0.13); }
.np-next span { flex: 1; min-width: 0; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-next b { font-weight: 800; color: #fff; margin-right: 4px; }
.np-next i { font-style: normal; color: var(--accent-hover); font-weight: 700; }
.np-face {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
-webkit-mask-image: linear-gradient(180deg, transparent 0, #000 22px, #000 calc(100% - 26px), transparent 100%);
mask-image: linear-gradient(180deg, transparent 0, #000 22px, #000 calc(100% - 26px), transparent 100%);
}
.np-face::-webkit-scrollbar { display: none; }
.np-meta { display: flex; align-items: center; gap: 12px; padding: 12px var(--pad) 0; flex: none; }
.np-meta .b { min-width: 0; flex: 1; }
.np-t { font-size: 21px; font-weight: 800; letter-spacing: -0.4px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-a { font-size: 14.5px; font-weight: 650; color: rgba(255, 255, 255, 0.66); margin-top: 2px; display: block; text-align: left; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.np-a:active { color: var(--accent-hover); }
/* ── the scrubber: the lane, at player size ───────────────────────────────
There is no track and no knob: SoulStream's position control has always
been the waveform itself — played bars in the record's accent, the rest in
--text-muted. Geometry and shaping come from src/lib/waveshape.js. */
.np-scrub { padding: 14px var(--pad) 0; flex: none; }
.np-scrub .times { display: flex; justify-content: space-between; margin-top: 7px; font-size: 11.5px; font-weight: 700; font-variant-numeric: tabular-nums; color: rgba(255, 255, 255, 0.6); }
/* one <i> per bar, the way the desktop rail draws it (NowPlaying.svelte's
.wave): 2px on a 6px pitch, an inline height, --fill mixing the accent in
for the played part. The bars are updated in place, never rebuilt while a
track plays — a rebuilt node cannot transition. */
.lane {
display: flex;
align-items: center;
justify-content: center;
gap: 4px; /* BAR_PITCH - BAR_W */
width: 100%;
overflow: hidden;
touch-action: none;
}
.lane i {
flex: 0 0 2px;
width: 2px;
height: 0;
min-height: 2px; /* the resting dot */
border-radius: 1px;
background: color-mix(in srgb, var(--accent) calc(var(--fill, 0) * 100%), #7a7a7a);
}
/* LOADING SWELL — NowPlaying.svelte's rail-swell, verbatim: phase-staggered
per bar so one crest travels the lane left→right. 2000ms sweep + 500ms
rest is the 2500ms period in waveshape.js. */
.lane.loading i {
animation: lane-swell 2500ms linear infinite;
animation-delay: calc(-2500ms + 2000ms * var(--i) / var(--bars, 60));
}
@keyframes lane-swell {
0%, 26%, 100% { height: 2px; opacity: 0.45; }
13% { height: 30%; opacity: 0.85; }
}
/* STREAMING RISE — a released bar stands up over RISE_MS (2000ms) */
.lane.rising i { transition: height 2000ms cubic-bezier(0.22, 1, 0.36, 1), opacity 400ms linear; }
/* ARRIVAL MORPH — a complete waveform eases in as one motion, ARRIVE_MS */
.lane.arriving i { transition: height 1000ms cubic-bezier(0.22, 1, 0.36, 1), opacity 400ms linear; }
/* the transport lands about a third up the screen — reachable one-handed */
.np-transport { display: flex; align-items: center; justify-content: space-between; padding: 20px calc(var(--pad) + 2px) 0; flex: none; }
.np-transport .disc.lg { width: 68px; height: 68px; background: #fff; color: #101014; box-shadow: 0 10px 26px -8px rgba(0, 0, 0, 0.7); }
.np-transport .disc.lg:active { background: #e6e6e6; }
.np-foot { display: flex; align-items: center; justify-content: center; padding: 14px var(--pad) 0; flex: none; }
.np-faces { display: flex; gap: 3px; padding: 3px; border-radius: var(--r-pill); background: rgba(0, 0, 0, 0.28); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.09); }
.fbtn { display: flex; align-items: center; gap: 5px; height: 32px; padding: 0 12px; border-radius: var(--r-pill); font-size: 12px; font-weight: 750; color: rgba(255, 255, 255, 0.6); transition: background var(--m-fast), color var(--m-fast); }
.fbtn.on { background: rgba(255, 255, 255, 0.15); color: #fff; }
.fbtn span { display: none; }
.fbtn.on span { display: inline; }
/* ── lyrics ───────────────────────────────────────────────────────────────
Line highlighting from the app's own lyrics pipeline; a word sweep only
when the source really carries word times — carried, never invented. */
.lyr { padding: 16px var(--pad) 20px; }
.lyr .ln { display: block; width: 100%; text-align: left; font-size: 22px; font-weight: 800; letter-spacing: -0.4px; line-height: 1.34; padding: 5px 0; transition: color 300ms ease, opacity 300ms ease; color: rgba(255, 255, 255, 0.46); }
.lyr .ln.past { color: rgba(255, 255, 255, 0.34); }
.lyr .ln.now { color: #fff; text-shadow: 0 0 22px color-mix(in srgb, var(--accent) 55%, transparent); }
.lyr .ln.now.words { color: rgba(255, 255, 255, 0.34); text-shadow: none; }
.lyr .ln.now.words .fill { color: #fff; text-shadow: 0 0 22px color-mix(in srgb, var(--accent) 55%, transparent); }
.lyr .ln:active { color: var(--accent-hover); }
.lyr-gap { height: 14px; }
.lyr-plain { white-space: pre-wrap; font-size: 17px; font-weight: 650; line-height: 1.55; color: rgba(255, 255, 255, 0.82); }
.lyr-note { padding: 10px 0 0; font-size: 12px; color: var(--text-muted); font-weight: 650; }
/* the queue face */
.q-face { padding-top: 4px; }
.q-break {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.6px;
text-transform: uppercase;
color: var(--text-muted);
padding: 14px var(--pad) 6px;
display: flex;
align-items: center;
gap: 8px;
}
.q-break::after { content: ""; flex: 1; height: 1px; background: rgba(255, 255, 255, 0.1); }
/* ── half sheets ──────────────────────────────────────────────────────── */
.halfsheet {
position: absolute;
left: 0; right: 0; bottom: 0;
z-index: 92;
max-height: 76%;
display: flex;
flex-direction: column;
border-radius: var(--r-sheet) var(--r-sheet) 0 0;
background: rgba(26, 26, 30, 0.97);
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.1);
transform: translateY(102%);
transition: transform var(--spd-spatial-default) var(--sp-spatial-default);
overflow: hidden;
padding-bottom: var(--inset-bottom);
}
.halfsheet.up { transform: none; }
html[data-glass="blur"] .halfsheet { background: rgba(26, 26, 30, 0.94); backdrop-filter: blur(30px) saturate(1.5); -webkit-backdrop-filter: blur(30px) saturate(1.5); }
html[data-material="solid"] .halfsheet { background: #1c1c20; backdrop-filter: none; -webkit-backdrop-filter: none; }
.halfsheet .grab { width: 38px; height: 4px; border-radius: 3px; background: rgba(255, 255, 255, 0.26); margin: 10px auto 4px; flex: none; }
.halfsheet .hs-body { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: none; display: flex; flex-direction: column; }
.halfsheet .hs-body::-webkit-scrollbar { display: none; }
.halfsheet .hs-body > * { flex: none; }
.sheet-title { font-size: 17px; font-weight: 800; letter-spacing: -0.25px; padding: 10px var(--pad) 4px; }
.sheet-sec { font-size: 11px; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; color: var(--text-muted); padding: 12px var(--pad) 8px; }
.sheet-head { display: flex; align-items: center; gap: 12px; padding: 8px var(--pad) 14px; margin-bottom: 4px; box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.08); }
.sheet-head .art { width: 52px; height: 52px; border-radius: 8px; overflow: hidden; flex: none; background: var(--bg-highlight); display: grid; place-items: center; color: var(--text-muted); }
.sheet-head .art img { width: 100%; height: 100%; object-fit: cover; }
.sheet-head .b { min-width: 0; }
.sheet-head .t { display: block; font-size: 15px; font-weight: 750; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sheet-head .m { display: block; font-size: 12.5px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.sheet-rows { padding-bottom: 6px; }
.sheet-hair { height: 1px; background: rgba(255, 255, 255, 0.08); margin: 6px var(--pad); }
.sheet-info { margin: 4px var(--pad) 12px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--ring); }
.scrim {
position: absolute;
inset: 0;
z-index: 91;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity var(--spd-effects-default) var(--sp-effects-default);
}
.scrim.on { opacity: 1; pointer-events: auto; }
/* ── the toast ─────────────────────────────────────────────────────────── */
.toast {
position: absolute; left: 12px; right: 12px;
bottom: calc(var(--nav-h) + var(--mini-h) + var(--inset-bottom) + 20px);
z-index: 95;
padding: 10px 14px; border-radius: 12px;
background: #3a0f14; color: #ffb4b4;
font-size: 12.5px; line-height: 1.4;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
}
/* ── the network: sign-in, rooms, chat ─────────────────────────────────── */
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #3ddc84; flex: none; }
.dot.off { background: rgba(255, 255, 255, 0.25); }
.dot.warn { background: #f5a524; }
.online-line { display: flex; align-items: center; gap: 7px; margin-top: 6px; }
.online-line b { color: var(--text-secondary); }
.signin { margin: 8px var(--pad) 0; padding: 16px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--ring); display: flex; flex-direction: column; gap: 10px; }
.signin-t { font-size: 17px; font-weight: 800; letter-spacing: -0.25px; }
.signin-m { font-size: 12.5px; line-height: 1.5; color: var(--text-secondary); margin-top: -4px; }
.signin-err { font-size: 12.5px; color: #ffb4b4; line-height: 1.4; }
.field-row { display: flex; align-items: center; gap: 6px; }
.field-row .field { flex: 1; min-width: 0; }
/* the desktop's text field, at thumb size: a pill of the surface ladder */
.field {
width: 100%;
height: 44px;
padding: 0 14px;
border-radius: var(--r-pill);
background: rgba(255, 255, 255, 0.08);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
border: 0;
outline: 0;
font-size: 15px;
font-weight: 600;
color: #fff;
-webkit-appearance: none;
appearance: none;
}
.field::placeholder { color: var(--text-muted); font-weight: 600; }
.field:focus { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--accent) 70%, transparent); }
.btn:disabled { opacity: 0.45; }
.disc:disabled { opacity: 0.4; }
.segtabs .count, .unread {
display: inline-block;
min-width: 18px;
padding: 1px 6px;
border-radius: 999px;
background: var(--accent);
color: #04121a;
font-size: 11px;
font-weight: 800;
text-align: center;
vertical-align: 1px;
}
.row .art.hash { color: var(--accent-hover); }
.row .art.idc { background: none; box-shadow: none; }
.row .r .n { font-size: 12.5px; font-weight: 700; color: var(--text-secondary); }
.room-head { display: flex; align-items: center; gap: 10px; padding: 6px var(--pad) 8px; }
.room-head .b { min-width: 0; flex: 1; }
.room-head .art { width: 40px; height: 40px; border-radius: 50%; overflow: hidden; flex: none; }
/* the conversation shape: theirs left on the surface, ours right in the
accent; a room and a thread draw with the same classes */
.chat { display: flex; flex-direction: column; gap: 3px; padding: 6px var(--pad) 0; }
.msg { max-width: 84%; align-self: flex-start; }
.msg.me { align-self: flex-end; }
.msg .who { display: block; font-size: 11.5px; font-weight: 750; color: var(--text-muted); padding: 0 4px 3px; text-align: left; }
.msg .who:active { color: var(--accent-hover); }
.msg .bub {
padding: 9px 13px;
border-radius: 16px 16px 16px 5px;
background: var(--surface);
box-shadow: var(--ring);
font-size: 14px;
line-height: 1.45;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.msg.me .bub {
border-radius: 16px 16px 5px 16px;
background: color-mix(in srgb, var(--accent) 24%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 32%, transparent);
}
.msg + .msg { margin-top: 9px; }
.msg .at { font-size: 10.5px; color: var(--text-muted); padding: 3px 5px 0; font-weight: 650; }
.msg.me .at { text-align: right; }
/* the composer, pinned above the mini bar and the nav — a chat input that
scrolls away with the messages is not a chat input */
.composer {
display: flex;
align-items: center;
gap: 9px;
padding: 8px var(--pad) 10px;
position: sticky;
bottom: calc(var(--nav-h) + var(--inset-bottom) + 6px);
background: linear-gradient(180deg, transparent, rgba(12, 12, 14, 0.78) 34%);
z-index: 3;
}
.has-mini .composer { bottom: calc(var(--mini-h) + var(--nav-h) + var(--inset-bottom) + 14px); }
.composer .field { height: 42px; }
/* ── the artist page: the desktop's identity row and extras ───────────────
The wordmark (or the name) on the banner; under it Play popular beside the
stat chips — Follow leads the row; Popular's listener bars; the latest
release on the app's featured surface (the artwork blurred behind itself);
releases by year; About. */
.hero-logo { display: block; max-width: 78%; max-height: 96px; object-fit: contain; object-position: left bottom; filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.6)); }
.hero-acts { display: flex; align-items: center; gap: 12px; padding: 12px var(--pad) 0; }
.hero-acts .stats { flex: 1; min-width: 0; }
.stats button.s { cursor: pointer; }
.stats .s.fb { background: rgba(255, 255, 255, 0.11); color: var(--text); }
.stats .s.fb.on { background: var(--accent); color: #04121a; }
.stats .s.fb:disabled { opacity: 0.5; }
.stats .s.quiet { color: var(--text-secondary); }
.row.pop .rank { width: 16px; text-align: right; font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; flex: none; }
.lbar { display: flex; align-items: center; gap: 8px; margin-top: 5px; height: 10px; }
.lbar i { display: block; flex: 1; height: 3px; border-radius: 2px; background: var(--accent); transform-origin: left center; opacity: 0.85; }
.lbar em { font-style: normal; font-size: 10.5px; color: var(--text-muted); font-variant-numeric: tabular-nums; min-width: 32px; text-align: right; }
.more-link { display: block; padding: 8px var(--pad) 2px; font-size: 12.5px; font-weight: 750; color: var(--accent-hover); }
.feat { position: relative; border-radius: var(--r-card); overflow: hidden; background: var(--surface); box-shadow: var(--ring); }
.feat-bg { position: absolute; inset: -30px; background-size: cover; background-position: center; filter: blur(28px) saturate(1.2); opacity: 0.55; }
.feat::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(10, 10, 12, 0.55), rgba(10, 10, 12, 0.25)); }
.feat-in { position: relative; z-index: 1; display: flex; gap: 14px; padding: 14px; align-items: center; }
.feat-art { width: 96px; height: 96px; border-radius: 10px; overflow: hidden; flex: none; background: rgba(255, 255, 255, 0.06); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); }
.feat-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.feat-art .ph { display: flex; width: 100%; height: 100%; align-items: center; justify-content: center; color: var(--text-muted); }
.feat-copy { min-width: 0; flex: 1; }
.feat-copy .k, .top-text .k { font-size: 10.5px; letter-spacing: 0.8px; text-transform: uppercase; font-weight: 800; color: var(--text-muted); }
.feat-name { font-size: 17px; font-weight: 800; letter-spacing: -0.3px; line-height: 1.15; margin: 3px 0 2px; }
.feat-copy .m, .top-text .m { font-size: 12px; color: var(--text-secondary); font-weight: 600; }
.feat-acts, .top-acts { display: flex; gap: 8px; margin-top: 10px; }
.about { padding: 0 var(--pad); }
.about .stats { margin-bottom: 10px; }
.bio { font-size: 13.5px; line-height: 1.5; color: var(--text-secondary); display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden; margin: 0; }
.bio.open { display: block; }
.about .tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.tl { display: flex; align-items: flex-end; gap: 3px; padding: 6px var(--pad) 0; height: 66px; overflow-x: auto; scrollbar-width: none; }
.tl::-webkit-scrollbar { display: none; }
.tl-col { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1 0 14px; min-width: 14px; }
.tl-bar { display: block; width: 100%; border-radius: 2px 2px 0 0; background: var(--accent); }
.tl-col.empty .tl-bar { background: rgba(255, 255, 255, 0.12); }
.tl-yr { font-size: 9px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* ── the catalog record page: the desktop's album page ────────────────────
The kind above the title, a portrait in the byline, Last.fm's totals as
chips, the other editions in a rail, listener bars in the rows, the words
below the tracks with the lede promoted, and who said so. */
.rec-meta .kind { font-size: 10.5px; letter-spacing: 0.9px; text-transform: uppercase; font-weight: 800; color: var(--text-muted); margin-bottom: 2px; }
.rec-artist.who { display: inline-flex; align-items: center; gap: 7px; }
.rec-artist.who img { width: 20px; height: 20px; border-radius: 50%; object-fit: cover; }
.rec-acts .btn.on { background: var(--accent); color: #04121a; }
.eds { padding-bottom: 4px; }
.ed { display: flex; align-items: center; gap: 10px; width: 210px; padding: 8px 10px 8px 8px; border-radius: 12px; background: var(--surface); box-shadow: var(--ring); text-align: left; }
.ed.on { box-shadow: inset 0 0 0 1.5px var(--accent); }
.ed-art { width: 40px; height: 40px; border-radius: 6px; overflow: hidden; flex: none; background: rgba(255, 255, 255, 0.06); display: flex; align-items: center; justify-content: center; color: var(--text-muted); }
.ed-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ed-copy { min-width: 0; }
.ed-copy .n { display: block; font-size: 13px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ed-copy .y { display: block; font-size: 11.5px; color: var(--text-muted); font-weight: 600; margin-top: 1px; }
.trk.locked { opacity: 0.5; }
.trk .tick { display: inline-flex; margin-left: 6px; color: var(--accent-hover); vertical-align: -1px; }
.trk .lbar { max-width: 220px; }
.lede { font-size: 14.5px; line-height: 1.5; font-weight: 650; color: var(--text); margin: 0 0 8px; }
.srcs { font-size: 11.5px; color: var(--text-muted); line-height: 1.5; margin: 10px 0 0; }
.srcs b { color: var(--text-secondary); font-weight: 750; }
/* ── Gaps: the desktop's gap line under a followed shelf, its missing
records dimmed until touched ─────────────────────────────────────────── */
.gapline { display: flex; align-items: center; gap: 6px; width: 100%; padding: 2px var(--pad) 10px calc(var(--pad) + 56px); font-size: 12.5px; color: var(--text-muted); font-weight: 600; text-align: left; }
.gapline b { color: var(--text-secondary); font-weight: 800; }
.gapline .cta { margin-left: auto; display: inline-flex; align-items: center; gap: 3px; color: var(--accent-hover); font-weight: 750; font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.5px; }
.gapline.done { color: var(--text-muted); }
.rail.gaps { padding-bottom: 6px; }
.rail.gaps .release { opacity: 0.44; transition: opacity 150ms ease; }
.rail.gaps .release:active { opacity: 1; }
/* ── the search hero: the desktop's top result ────────────────────────────
The most popular exact match of its kind — an artist, a record or a song
— on the featured surface, its artwork blurred behind itself. */
.top { position: relative; margin: 4px var(--pad) 10px; border-radius: var(--r-card); overflow: hidden; background: var(--surface); box-shadow: var(--ring); }
.top-bg { position: absolute; inset: -30px; background-size: cover; background-position: center; filter: blur(30px) saturate(1.2); opacity: 0.5; }
.top::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(10, 10, 12, 0.5), rgba(10, 10, 12, 0.2)); }
.top-in { position: relative; z-index: 1; display: flex; gap: 14px; padding: 14px; align-items: center; }
.top-art { width: 104px; height: 104px; border-radius: 12px; overflow: hidden; flex: none; background: rgba(255, 255, 255, 0.06); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4); }
.top.round .top-art { border-radius: 50%; }
.top-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.top-art .ph { display: flex; width: 100%; height: 100%; align-items: center; justify-content: center; color: var(--text-muted); }
.top-text { min-width: 0; flex: 1; }
.top-name { font-size: 20px; font-weight: 800; letter-spacing: -0.4px; line-height: 1.1; margin: 3px 0 3px; }
.top-who { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 700; color: var(--text); vertical-align: middle; }
.top-who img { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; }
.top-text .tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
/* ── catalog results ──────────────────────────────────────────────────────
The desktop's top bar: rows for artists and songs, a rail for records. A
song row is a play button — the resolver's line replaces its duration
while the ladder runs. */
.row.cat-song .r, .row.cat-artist .r { font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.row.cat-song.playing .t { color: var(--accent-hover); }
.net-ask { gap: 8px; }
/* ── Soulseek results ─────────────────────────────────────────────────────
Grouped by PEER, because on this network the choice is whose copy to take,
not which file. The folder is the unit you act on; the tracks under it
confirm it is the right rip — and a file row plays, streamed from that
peer, the lane telling the transfer's story. */
.peer { margin: 0 var(--pad) 12px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--ring); overflow: hidden; }
html[data-material="solid"] .peer { background: var(--bg-elevated); box-shadow: none; }
.peer-head { display: flex; align-items: center; gap: 10px; padding: 10px 12px 8px; }
.peer-who { display: flex; align-items: center; gap: 9px; min-width: 0; flex: 1; }
.peer-who > span { min-width: 0; }
.peer-who b { display: block; font-size: 13.5px; font-weight: 750; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.peer-who i { display: block; font-style: normal; font-size: 11.5px; color: var(--text-muted); font-weight: 600; margin-top: 1px; }
.peer-folder {
display: grid;
grid-template-columns: 1fr auto;
gap: 2px 10px;
align-items: center;
width: 100%;
padding: 8px 12px 10px;
text-align: left;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
transition: background var(--m-fast);
}
.peer-folder:active { background: var(--hover); }
.peer-path { font-size: 13px; font-weight: 650; line-height: 1.35; word-break: break-word; }
.peer-meta { font-size: 11.5px; color: var(--text-muted); font-weight: 600; grid-column: 1; }
.peer-get {
grid-row: 1 / span 2;
grid-column: 2;
display: inline-flex;
align-items: center;
gap: 6px;
height: 34px;
padding: 0 12px;
border-radius: var(--r-pill);
background: var(--accent);
color: #04121a;
font-size: 12px;
font-weight: 800;
white-space: nowrap;
}
.peer-files { padding: 2px 12px 8px; }
.peer-file {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
min-height: 34px;
padding: 4px 6px;
margin: 0 -6px;
border-radius: 8px;
font-size: 12.5px;
color: var(--text-secondary);
text-align: left;
transition: background var(--m-fast);
}
.peer-file:active { background: var(--press); }
.peer-file.playing .pf-t { color: var(--accent-hover); }
.peer-file > span:first-child { width: 16px; text-align: right; color: var(--text-muted); font-variant-numeric: tabular-nums; font-size: 11.5px; flex: none; }
.pf-t { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pf-m { color: var(--text-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; flex: none; }
.peer-more { display: block; font-size: 11.5px; color: var(--text-muted); font-weight: 650; padding: 6px 0 2px 26px; text-align: left; }
.peer-more:active { color: var(--accent-hover); }
/* inline icons inside a line of text (Icon.svelte is display:block) */
.inl { display: inline-flex; vertical-align: -3px; }
.t-sub { font-size: 13px; font-weight: 500; color: var(--text-muted); }
/* the search field — a pill, the desktop's search box at thumb size */
.sfield {
display: flex;
align-items: center;
gap: 10px;
margin: 4px var(--pad) 8px;
height: 46px;
padding: 0 6px 0 14px;
border-radius: var(--r-pill);
background: rgba(255, 255, 255, 0.08);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
color: var(--text-secondary);
}
.sfield input {
flex: 1;
min-width: 0;
height: 100%;
background: none;
border: 0;
outline: 0;
font-size: 15px;
font-weight: 600;
color: #fff;
-webkit-appearance: none;
appearance: none;
}
.sfield input::placeholder { color: var(--text-muted); font-weight: 600; }
.sfield input::-webkit-search-cancel-button { display: none; }
.trk .n .icon { display: inline-block; color: var(--text-muted); }
.countline { display: flex; align-items: center; justify-content: space-between; padding: 0 var(--pad) 10px; }
/* a hero bleeds up under the status bar; the scrim keeps the chrome legible */
.screen.has-hero { padding-top: 0; }
/* not a stray: `.lane` is width: 100%, and the mini bar's lane is ALSO
positioned with both left: 16px and right: 16px — an over-constrained
absolute box drops `right`, so without this it comes out 32px wider than
the strip it was cut for (395 where 363 was asked for, at 411), the bars
sit off-centre and the tail runs under the bar's own overflow: hidden. */
.mini .mini-lane { width: auto; }
/* ── screen transitions ───────────────────────────────────────────────────
Material's shared-axis-X for a push and its reverse for a pop, so the
direction of travel is legible; a fade-through for a tab change, because
tabs are siblings. The outgoing screen is replaced outright, so the
incoming one carries the whole gesture. */
@keyframes scr-push { from { opacity: 0; transform: translate3d(26px, 0, 0); } to { opacity: 1; transform: none; } }
@keyframes scr-pop { from { opacity: 0; transform: translate3d(-26px, 0, 0); } to { opacity: 1; transform: none; } }
@keyframes scr-fade { from { opacity: 0; transform: scale(0.972); } to { opacity: 1; transform: none; } }
.screen.tr-push { animation: scr-push var(--spd-spatial-default) var(--sp-spatial-default) both; }
.screen.tr-pop { animation: scr-pop var(--spd-spatial-default) var(--sp-spatial-default) both; }
.screen.tr-tab { animation: scr-fade var(--spd-effects-default) var(--sp-effects-default) both; }
/* a page does not arrive all at once; its first eight blocks land in order */
@keyframes blk-rise { from { opacity: 0; transform: translate3d(0, 10px, 0); } to { opacity: 1; transform: none; } }
.screen.stagger > * { animation: blk-rise var(--spd-spatial-default) var(--sp-spatial-default) both; }
.screen.stagger > *:nth-child(1) { animation-delay: 0ms; }
.screen.stagger > *:nth-child(2) { animation-delay: 26ms; }
.screen.stagger > *:nth-child(3) { animation-delay: 52ms; }
.screen.stagger > *:nth-child(4) { animation-delay: 76ms; }
.screen.stagger > *:nth-child(5) { animation-delay: 98ms; }
.screen.stagger > *:nth-child(6) { animation-delay: 116ms; }
.screen.stagger > *:nth-child(7) { animation-delay: 130ms; }
.screen.stagger > *:nth-child(8) { animation-delay: 140ms; }
.screen.stagger > *:nth-child(n + 9) { animation: none; }
/* ── REDUCE MOTION: not faster — off ──────────────────────────────────── */
html[data-motion="off"] *, html[data-motion="off"] *::before, html[data-motion="off"] *::after {
animation-duration: 0.01ms !important;
animation-delay: 0ms !important;
transition-duration: 0.01ms !important;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-delay: 0ms !important;
transition-duration: 0.01ms !important;
}
}
/* ── playlists: the desktop's shelf and page, at phone scale ─────────── */
.plstats { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 var(--pad) 12px; }
.plstats .t-sub { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.newpl { display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 12px 0 10px; font-size: 13px; font-weight: 750; flex: none; }
.plsec { padding-top: 6px; }
.plsec h2 { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; color: var(--text-muted); }
.plgrid { padding-bottom: 8px; }
/* the pinned mark on a card, the desktop's pinned-mark */
.cover-wrap .mark { position: absolute; top: 7px; left: 7px; z-index: 2; width: 22px; height: 22px; border-radius: 50%; background: rgba(0, 0, 0, 0.55); color: #fff; display: grid; place-items: center; }
/* the page: the record page's shape with a smaller cover (a mosaic reads at any size) */
.rec-head.pl .rec-cover { width: 56%; max-width: 260px; }
.rec-meta .kind .pin-chip { display: inline-flex; align-items: center; gap: 3px; margin-left: 8px; padding: 2px 7px; border-radius: 999px; background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent-hover); letter-spacing: 0.4px; vertical-align: 1px; }
.rec-desc { margin-top: 8px; font-size: 13.5px; line-height: 1.45; color: var(--text-secondary); padding: 0 8px; }
.rec-line .warn { color: #f5a524; font-weight: 650; }
.sfield.plfilter { margin-top: 10px; }
.pl-rows .trk .thumb { width: 38px; height: 38px; border-radius: 6px; overflow: hidden; flex: none; background: rgba(255, 255, 255, 0.06); display: grid; place-items: center; color: var(--text-muted); }
.pl-rows .trk .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pl-rows .trk .n.stamped { width: auto; min-width: 22px; font-size: 11.5px; }
.pl-rows .trk.unresolved .t, .pl-rows .trk.missing .t { color: var(--text-secondary); }
.pl-rows .trk .t .tag { margin-left: 6px; vertical-align: 1px; }
.tag.un { background: rgba(232, 163, 61, 0.16); color: #e8a33d; }
.tag.miss { background: rgba(241, 94, 108, 0.16); color: #f15e6c; }
.tag.seg { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent-hover); }
/* the neutral toast: a fact with one action (Undo); the error toast keeps its red */
.toast.notice { display: flex; align-items: center; gap: 12px; background: #1f1f26; color: #fff; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(255, 255, 255, 0.1); }
.toast.notice .nt { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.toast.notice .na { flex: none; color: var(--accent-hover); font-weight: 800; font-size: 13px; padding: 4px 8px; margin: -4px -8px -4px 0; border-radius: 6px; }
.toast.notice .na:active { background: rgba(255, 255, 255, 0.1); }
/* the prompt and confirm sheets */
.prompt { padding: 4px var(--pad) 16px; display: flex; flex-direction: column; gap: 12px; }
.prompt .field.area { height: auto; min-height: 84px; padding: 12px 14px; border-radius: 14px; resize: none; line-height: 1.4; font-family: inherit; }
.prompt-acts { display: flex; gap: 8px; }
.prompt-acts .btn { flex: 1; }
.btn.btn-fill.danger { background: #f15e6c; color: #fff; }
.btn.btn-fill.danger:active { background: #d94a58; }
/* ── the import doors and the review ─────────────────────────────────── */
.plbtns { display: flex; gap: 8px; padding: 0 var(--pad) 12px; }
.plbtns .newpl { flex: 1; justify-content: center; }
.imp-head { padding: 4px var(--pad) 10px; display: flex; flex-direction: column; gap: 8px; }
.imp-src { font-size: 14.5px; font-weight: 700; }
.imp-counts { font-size: 13px; color: var(--text-muted); }
.imp-counts b { font-weight: 800; }
.imp-counts .ok { color: #3ddc84; }
.imp-counts .st { color: var(--accent-hover); }
.imp-counts .un { color: #e8a33d; }
.imp-warn { font-size: 12.5px; line-height: 1.5; color: #e8a33d; padding: 8px 12px; border-radius: 10px; background: rgba(232, 163, 61, 0.1); }
.imp-rows .trk { min-height: 44px; }
.imp-rows .trk .tag { flex: none; }
.imp-rows .trk.excluded { opacity: 0.4; }
.tag.ok { background: rgba(61, 220, 132, 0.16); color: #3ddc84; }
.tag.st { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent-hover); }
.tag.dim { opacity: 0.7; }
.imp-form { margin: 10px var(--pad) 0; }
.imp-foot { display: flex; gap: 8px; padding: 14px var(--pad) 4px; }
.imp-foot .btn { flex: 1; }
.imp-wait { margin: 40px var(--pad) 0; padding: 26px 18px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--ring); display: flex; flex-direction: column; align-items: center; gap: 12px; text-align: center; font-size: 13px; color: var(--text-secondary); }
.imp-wait b { color: #fff; font-size: 15px; }
.imp-wait .ring { width: 26px; height: 26px; border-radius: 50%; border: 3px solid rgba(255, 255, 255, 0.15); border-top-color: var(--accent); animation: imp-spin 0.9s linear infinite; }
@keyframes imp-spin { to { transform: rotate(360deg); } }
/* ── You → Sources: a window per mode, the priority inside it ─────────── */
.pref-head { padding: 12px 14px 2px; font-size: 11px; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; color: var(--text-muted); }
.pref { padding: 8px 0 12px; }
.pref + .pref { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.055); }
.pref-l { padding: 0 14px 8px; }
.pref-l .t { display: block; font-size: 14.5px; font-weight: 650; }
.pref-l .m { display: block; font-size: 12px; color: var(--text-muted); margin-top: 1px; }
.pref .chips { padding: 0 14px; }
/* ── a playlist's bulk run: the desktop's bulkline, under the actions ── */
.bulkline { display: inline-flex; align-items: center; gap: 8px; margin: 2px auto 4px; padding: 7px 8px 7px 12px; border-radius: 999px; background: rgba(255, 255, 255, 0.08); font-size: 12.5px; color: var(--text-secondary); max-width: 100%; text-align: left; }
.bulkline > span:not(.ring) { min-width: 0; }
.bulkline .ring { width: 14px; height: 14px; flex: none; border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.15); border-top-color: var(--accent); animation: imp-spin 0.9s linear infinite; }
.bulkline .bulkx { display: grid; place-items: center; width: 24px; height: 24px; flex: none; border-radius: 50%; color: var(--text-muted); }
.bulkline .bulkx:active { background: rgba(255, 255, 255, 0.12); }
/* the repair sheet's search field, inside the half sheet */
.halfsheet .sfield.plmatch { margin: 6px var(--pad) 8px; }
/* ── a peer's menu on a raw result, and the Browse screen ─────────────── */
.peer-head .peer-menu { margin: -6px -6px -6px 2px; }
.brw-head { display: flex; align-items: center; gap: 12px; padding: 12px var(--pad) 10px; }
.brw-head .b { flex: 1; min-width: 0; }
.brw-head .t { font-size: 18px; font-weight: 800; letter-spacing: -0.2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.brw-head .m { font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }
.brw-acts { display: flex; gap: 8px; padding: 2px 12px 8px; }
.brw-acts .btn { flex: 1; height: 36px; font-size: 13px; gap: 6px; }
.peer-file:disabled { opacity: 0.45; }
/* You → Sharing: the upload-slot stepper; a shared folder's path */
.stepper { display: inline-flex; align-items: center; gap: 2px; flex: none; }
.stepper .n { min-width: 24px; text-align: center; font-weight: 800; font-size: 14px; font-variant-numeric: tabular-nums; }
.group .row .t.mono { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 12.5px; font-weight: 600; }
/* Search: the spelling suggestion, inline in the note */
.sec-note .dym { color: var(--accent-hover); font-weight: 750; padding: 0 2px; }
/* ── the artwork viewer: the cover alone, over everything ─────────────── */
.viewer { position: fixed; inset: 0; z-index: 200; background: #000; display: grid; place-items: center; animation: viewer-in 160ms ease both; touch-action: none; overflow: hidden; }
.viewer img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; will-change: transform; transform-origin: center; }
@keyframes viewer-in { from { opacity: 0; } to { opacity: 1; } }
/* the lyrics face's way to its tools */
.lyr-tools { display: block; margin: 18px auto 8px; padding: 7px 14px; border-radius: 999px; background: rgba(255, 255, 255, 0.08); color: var(--text-secondary); font-size: 12.5px; font-weight: 700; }
.lyr-tools:active { background: rgba(255, 255, 255, 0.14); }
/* Community: the room filter sits with the section, not the page's own field */
.sfield.roomfilter { margin-top: 2px; }
/* the two local marks beside a name: a star for a favourite, a shield for
someone trusted. Both are this phone's own; neither is published. */
.nmark { display: inline-flex; vertical-align: -2px; margin-left: 5px; color: var(--text-muted); }
.nmark.fav { color: #e8a33d; }
.nmark.tr { color: #3ddc84; }
/* a room line whose author wears a petname keeps their own name beside it */
.msg .who .real { margin-left: 5px; font-weight: 600; opacity: 0.65; }
/* the nav's badges: transfers in flight on You, unread chats on Community */
.nav button .gl .nb { position: absolute; top: -3px; right: -12px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--accent); color: #04121a; font-size: 10px; font-weight: 800; line-height: 16px; text-align: center; box-shadow: 0 0 0 2px var(--nav-bg, #101014); }
/* A peer in full: the head wears their own picture, their own words sit in
a card, and the comparison's rows read as records they have and you don't */
.pr-head .pr-pic { width: 44px; height: 44px; flex: none; padding: 0; border: 0; border-radius: 13px; overflow: hidden; background: var(--surface); box-shadow: var(--ring); }
.pr-head .pr-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pr-about { margin: 2px var(--pad) 10px; padding: 12px 14px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--ring); font-size: 13.5px; line-height: 1.55; color: var(--text-secondary); white-space: pre-wrap; overflow-wrap: anywhere; }
.chips.pr-tags { flex-wrap: wrap; overflow: visible; row-gap: 7px; }
.pr-gap .lead { color: var(--text-muted); }
/* Community → Taste: the interests you publish, wrapped rather than
scrolled — a declared list is read, not swiped */
.tastechips { flex-wrap: wrap; overflow: visible; }
.tastechips .tchip svg { opacity: 0.62; margin-right: -3px; }
.tastechips .chip:disabled { opacity: 0.5; }
/* ── the tap-to-sync room: the song plays, the screen is the tap ───────── */
.syncroom { position: fixed; inset: 0; z-index: 150; display: flex; flex-direction: column; background: radial-gradient(120% 70% at 20% 0%, color-mix(in srgb, var(--accent) 20%, transparent), transparent 58%), var(--bg-base); padding-top: var(--inset-top); padding-bottom: var(--inset-bottom); animation: viewer-in 160ms ease both; }
.sy-head { display: flex; align-items: center; gap: 10px; padding: 8px var(--pad) 2px; flex: none; }
.sy-close { width: auto; min-width: var(--tap); padding: 0 10px; border-radius: var(--r-pill); }
.sy-discard { font-size: 12.5px; font-weight: 800; color: #f15e6c; }
.sy-who { flex: 1; min-width: 0; text-align: center; }
.sy-who b { display: block; font-size: 11px; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; color: var(--accent-hover); }
.sy-who i { display: block; font-style: normal; font-size: 13px; font-weight: 700; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sy-count { flex: none; min-width: 54px; text-align: right; font-size: 13px; font-weight: 800; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.sy-warn { margin: 4px var(--pad) 0; padding: 9px 13px; border-radius: 10px; background: rgba(232, 163, 61, 0.14); color: #e8a33d; font-size: 12.5px; font-weight: 700; flex: none; }
/* the measured vocal band under the head: where the singing is, the lines
that already have a time, and the playhead over both */
.sy-strip { position: relative; display: flex; align-items: flex-end; gap: 1px; height: 40px; margin: 8px var(--pad) 0; padding: 0 2px; border-radius: 8px; background: rgba(255, 255, 255, 0.04); flex: none; overflow: hidden; }
.sy-col { flex: 1; min-width: 0; border-radius: 1px; background: color-mix(in srgb, var(--accent) 55%, transparent); }
.sy-tick { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: #fff; opacity: 0.75; }
.sy-play { position: absolute; top: -2px; bottom: -2px; width: 2px; margin-left: -1px; background: var(--accent-hover); box-shadow: 0 0 8px var(--accent-hover); }
.sy-est { align-self: center; margin: 8px 0 0; padding: 7px 16px; border-radius: var(--r-pill); font-size: 12.5px; font-weight: 750; color: var(--accent-hover); background: color-mix(in srgb, var(--accent) 14%, transparent); flex: none; }
.sy-est:disabled { opacity: 0.45; color: var(--text-muted); background: rgba(255, 255, 255, 0.05); }
.sy-pad { flex: 1; min-height: 0; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 12px var(--pad); text-align: center; }
.sy-pad:active { background: rgba(255, 255, 255, 0.05); }
.sy-pad:disabled { opacity: 0.5; }
.sy-prev, .sy-next { display: block; width: 100%; min-height: 20px; font-size: 15px; font-weight: 650; line-height: 1.35; color: rgba(255, 255, 255, 0.3); overflow: hidden; }
.sy-now { display: block; width: 100%; font-size: 27px; font-weight: 800; letter-spacing: -0.5px; line-height: 1.25; color: #fff; text-shadow: 0 0 26px color-mix(in srgb, var(--accent) 50%, transparent); }
.sy-hint { display: block; margin-top: 6px; font-size: 12px; font-weight: 650; color: var(--text-muted); }
.sy-mark { display: block; font-size: 13px; font-weight: 800; color: var(--accent-hover); font-variant-numeric: tabular-nums; }
.sy-foot { display: flex; align-items: center; gap: 10px; padding: 8px var(--pad) 16px; flex: none; }
.sy-foot .iconbtn { width: auto; min-width: var(--tap); padding: 0 12px; border-radius: var(--r-pill); font-size: 13px; font-weight: 750; }
.sy-foot .iconbtn:disabled { opacity: 0.4; }
.sy-save { flex: 1; }
/* ── the phone turned sideways ────────────────────────────────────────────
The activity declares no screenOrientation and handles orientation and
screenSize itself, so a rotation is a RESIZE: no state is lost and the
layout is the only thing that has to answer. Two queries do it. The player
sheet is the surface whose portrait shape cannot survive a wide viewport —
its artwork is a square sized by WIDTH, so at 915x411 the cover came out
879px tall and the title, the lane, the transport and the face buttons all
sat below the bottom of a sheet that does not scroll. That is aspect, not
height, so its rule keys off orientation alone and a tablet gets it too;
everything under it is a phone-sized query.
These rules are appended, and the order is load-bearing: the side-inset
longhands below have to come after .gbar's padding shorthand, .mini's
left/right and .toast's left/right to win at equal specificity. */
/* the side insets: the cutout and the gesture pill, from the edges */
.gbar { padding-left: calc(6px + var(--inset-left)); padding-right: calc(8px + var(--inset-right)); }
.nav { padding-left: var(--inset-left); padding-right: var(--inset-right); }
.mini { left: calc(8px + var(--inset-left)); right: calc(8px + var(--inset-right)); }
.screen { padding-left: var(--inset-left); padding-right: var(--inset-right); }
.np { padding-left: var(--inset-left); padding-right: var(--inset-right); }
.halfsheet { padding-left: var(--inset-left); padding-right: var(--inset-right); }
.syncroom { padding-left: var(--inset-left); padding-right: var(--inset-right); }
.toast { left: calc(12px + var(--inset-left)); right: calc(12px + var(--inset-right)); }
/* ── the player sheet, beside itself ─────────────────────────────────── */
@media (orientation: landscape) {
.np {
display: grid;
grid-template-columns: minmax(0, 46%) minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr) auto auto auto auto;
grid-template-areas:
"head head"
"left slack"
"left meta"
"left scrub"
"left trans"
"left foot";
column-gap: 6px;
/* insurance for a viewport shorter than the controls themselves —
the keyboard up on a small phone — where the sheet would clip */
overflow-y: auto;
overscroll-behavior: contain;
}
.np-head { grid-area: head; }
.np-art { grid-area: left; padding: 10px 4px 10px var(--pad); }
/* the square is sized by whichever is smaller, the column or the height */
.np-frame { width: min(100%, calc(100vh - var(--inset-top) - var(--inset-bottom) - 92px)); }
.np-spacer { grid-area: slack; padding: 10px var(--pad) 0; justify-content: center; }
.np-face { grid-area: left; }
.np-meta { grid-area: meta; padding-top: 2px; }
.np-scrub { grid-area: scrub; padding-top: 8px; }
.np-transport { grid-area: trans; padding-top: 10px; justify-self: center; width: min(100%, 430px); }
.np-foot { grid-area: foot; padding-top: 8px; }
/* a face is up: it takes the artwork's column and the thumbnail goes —
the record is already the ground behind the whole sheet */
.np.small { grid-template-columns: minmax(0, 1fr) minmax(0, 430px); }
.np.small .np-art { display: none; }
}
/* ── a phone turned sideways: 411 pixels of height to spend ───────────── */
@media (orientation: landscape) and (max-height: 560px) {
/* the nav comes down to Android's own 48dp minimum and the mini bar to a
60dp row over the same lane strip: 22 pixels back, 46% of the height as
chrome down to 40% */
:root { --nav-h: 48px; --mini-h: 60px; }
.nav button .gl { height: 22px; }
.nav button .lb { font-size: 9.5px; }
.nav button .pill { top: 3px; height: 24px; }
.mini { padding-bottom: 24px; }
.mini .art { width: 34px; height: 34px; }
.mini .mini-lane { bottom: 5px; }
/* the hero stops being a portrait and becomes the banner the desktop's
hero has always been */
.hero { height: 190px; }
.hero-name { font-size: 27px; }
.hero-logo { max-height: 64px; }
/* the record head puts the cover beside the title, again the desktop's
own shape; .pl needs naming because its own rule is more specific */
.rec-head { flex-direction: row; align-items: center; gap: 22px; padding: calc(var(--bar-h) + 12px) var(--pad) 4px; }
.rec-head .rec-cover, .rec-head.pl .rec-cover { width: 34%; max-width: 240px; }
.rec-meta { text-align: left; }
.rec-title { margin-top: 0; }
.rec-chips { justify-content: flex-start; }
.rec-acts { justify-content: flex-start; padding: 14px 0 4px; }
/* 76% of 411 is 312; a half sheet may take all but a strip */
.halfsheet { max-height: calc(100% - 36px); }
}
/* The one live region: never drawn, never laid out, always there — so an
announcement is a change of text inside it rather than a node arriving
with its words already in it (PreviewOverlay.svelte's .sr-announce, at
phone scale) */
.sr-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
/* The Transfers head's two words: Cancel all while anything is in flight,
Clear finished after. The .sec's own small caps, which this head never
had — "Clear finished" has been sitting there at body size.
The space between them goes on the words, NOT on .phead .acts: Library
and Community hang their header icons in that same wrapper and they sit
flush, so a gap there would push three round buttons apart on two
screens this round never looked at. */
.phead .all { font-size: 12.5px; font-weight: 750; color: var(--text-secondary); white-space: nowrap; }
.phead .all:active { color: #fff; }
.phead .all + .all { margin-left: 15px; }
/* ── what the phone says when things go wrong ──────────────────────────
The error toast gets the notice's shape: the sentence, and one action
beside it where there is one to offer — Try again, or Sign in when the
trouble is that nobody is signed in. The rules mirror .toast.notice's
above rather than sharing them: this toast is the red one, and its line
WRAPS (a translated sentence is a sentence, not a label), which is the
one thing the notice's .nt must never do. */
.toast { display: flex; align-items: center; gap: 12px; }
.toast .nt { flex: 1; min-width: 0; }
.toast .na { flex: none; color: #ffd7d7; font-weight: 800; font-size: 13px; padding: 4px 8px; margin: -4px -8px -4px 0; border-radius: 6px; white-space: nowrap; }
.toast .na:active { background: rgba(255, 255, 255, 0.12); }
/* a stub that is explaining a failure rather than an emptiness: the same
card, with the backend's own words under the lead in the muted colour */
.stub.why { color: var(--text-muted); }
/* the two lines a screen says instead of a spinner that will not end */
.sec-note.rooms-note,
.sec-note.offline-note { color: #ffb4b4; }
/* the library folder the rescan could not reach — a row under the folder
itself, so the path and what to do about it are in the same place. The
path is drawn exactly as the Library folder row above it draws its own
(`.m mono`, which .row does not give a face — leave that as it is rather
than making these two rows disagree). */
.row.folder-gone .t { color: #ffb4b4; }
/* ── the first run ───────────────────────────────────────────────────────
A phone that has just installed the app: the tour's sheet, and the note
the screens that are empty BECAUSE of it carry. Both are the app's own
parts — .sheet-sec, .sheet-rows, .row, .stub — so there is almost nothing
here: the folder's path, the foot's button, and the note's rows pulled
out to the card's own edge so they read as part of the message rather
than as a second card floating under it. */
.setup-path {
padding: 0 var(--pad) 10px;
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: 11.5px;
color: var(--text-muted);
word-break: break-all;
}
.setup-foot { padding: 10px var(--pad) 4px; }
/* the note: one message, with the rows that answer it under a hairline */
.setup-note .setup-rows {
margin: 12px -16px 0;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.setup-note .setup-rows .row { padding: 10px 16px; }
.setup-note .setup-rows .row + .row { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.055); }
.setup-note .setup-rows .row .t { font-size: 13.5px; }
.setup-note .setup-rows .row .m { font-size: 12px; }
/* ── dragged into place: the playlist's rows and the queue's ───────────
Almost nothing, because both lists are already the app's own rows. No
row carries `touch-action: none` — a flick that starts on one still
scrolls the list; the gesture takes the scroll away for itself only
after the 320ms lift, and in JS (src/mobile/reorder.js). The spring is
the app's own spatial one, and the global reduce-motion rule above
flattens it for nothing. */
.tracks.dragging .trk,
.q-face.dragging .row {
transition: background var(--m-fast), transform var(--m-base);
}
/* the row in hand: off the spring, above its neighbours, lifted off the page */
.tracks.dragging .trk.lifted,
.q-face.dragging .row.lifted {
transition: none;
position: relative;
z-index: 5;
background: var(--hover);
border-radius: 10px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
}
/* why the rows will not move while the filter has something in it */
.pl-note { padding: 2px var(--pad) 8px; font-size: 12px; color: var(--text-muted); }
/* ── the whole discography ───────────────────────────────────────────────
The artist page's "All". Nearly all of it is the app's own parts — .sec,
.segtabs, .row, .eds/.ed, .more-link, .btn — so what is here is the head,
the year written into the list where the desktop has a sticky rail, the
editions line under a record, and the editions themselves widened out of
the record page's 210px rail into the width this list already has. */
.dg-head { display: flex; align-items: center; gap: 12px; padding: 12px var(--pad) 6px; }
.dg-head .b { flex: 1; min-width: 0; }
.dg-head .t { font-size: 18px; font-weight: 800; letter-spacing: -0.2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dg-head .m { font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }
.dg-por {
width: 44px; height: 44px;
border-radius: 50%;
overflow: hidden;
flex: none;
background: var(--surface);
box-shadow: var(--ring);
display: grid;
place-items: center;
color: var(--text-muted);
}
.dg-por img { width: 100%; height: 100%; object-fit: cover; }
.dg-sec { padding-top: 12px; }
/* the year. A phone list is read top to bottom, so the span the desktop's
rail jumps to is simply written into it. */
.dg-group {
padding: 16px var(--pad) 5px;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.4px;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
}
.dg-group em { font-style: normal; font-weight: 650; letter-spacing: 0; color: var(--text-muted); }
/* both of these line up under the row's TITLE, not under its cover: the
pad, the 46px cover and the row's 12px gap */
.dg-eline {
display: flex;
align-items: center;
gap: 6px;
padding: 0 var(--pad) 9px calc(var(--pad) + 58px);
font-size: 12px;
font-weight: 750;
color: var(--accent-hover);
}
.dg-eds { display: flex; flex-direction: column; gap: 6px; padding: 0 var(--pad) 12px calc(var(--pad) + 58px); }
.dg-eds .ed { width: 100%; }