/* ═══════════════════════════════════════════════════════════════════════════ 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,"); 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); } .iconbtn.liked { color: var(--accent); } /* ── 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; } .cardpad { padding: 14px; } .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 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; } .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; } .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); } }