diff --git a/README.md b/README.md
index ffe87cd..18b74fc 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ Not affiliated with pl8list.
## Screenshots
-The [monochrome redesign previews](docs/redesign/README.md) show the current visual treatment using a synthetic track. The live-playback captures below predate that pass.
+The [updated design previews](docs/redesign/README.md) show the current visual treatment using a synthetic track. The live-playback captures below predate that pass.

diff --git a/docs/redesign/README.md b/docs/redesign/README.md
index ba5b963..3a544ce 100644
--- a/docs/redesign/README.md
+++ b/docs/redesign/README.md
@@ -1,9 +1,11 @@
-# Monochrome visual pass
+# Artwork-driven visual pass
-These screenshots use a synthetic track and deterministic audio features in Chromium, not live pl8list playback. They demonstrate desktop (1440×1000), mobile (390×844), short mobile (390×667), settings and login styling.
+These screenshots use a synthetic track, colorful generated SVG artwork and deterministic audio features in Chromium, not live pl8list playback. They demonstrate desktop (1440×1000), mobile (390×844), short mobile (390×667), settings and login styling.
Validated: Bun frozen-lockfile installation, TypeScript check, production build, local font loading, WebGL shader creation/rendering, opening and dismissing settings, and a sustained synthetic drop whose one-shot envelope decays without retriggering. Chromium reported no page errors.
Still requires real-device validation: real-track onset timing, touch gestures, GPU context restoration, reduced-motion transitions, audio fallback on iOS, and comparative frame-time/battery measurements. Docker's Bun build stage was updated but the full container image was not built.
Run the frontend with `cd web && bun install --frozen-lockfile && bun run dev`. Build with `bun run build`.
+
+The chromatic artwork material, artist accents and rounded controls have been restored following visual feedback. The drop button has a centered row beneath the waveform, no arrow, and a 44px minimum height. Chart selectors have dedicated clearance on short mobile screens.
diff --git a/docs/redesign/desktop.png b/docs/redesign/desktop.png
index be9c954..ecd2b42 100644
Binary files a/docs/redesign/desktop.png and b/docs/redesign/desktop.png differ
diff --git a/docs/redesign/login.png b/docs/redesign/login.png
index b605bab..10c1a1c 100644
Binary files a/docs/redesign/login.png and b/docs/redesign/login.png differ
diff --git a/docs/redesign/mobile.png b/docs/redesign/mobile.png
index 1c3b162..2a448fe 100644
Binary files a/docs/redesign/mobile.png and b/docs/redesign/mobile.png differ
diff --git a/docs/redesign/settings.png b/docs/redesign/settings.png
index d1d51d6..28a0e86 100644
Binary files a/docs/redesign/settings.png and b/docs/redesign/settings.png differ
diff --git a/docs/redesign/short-mobile.png b/docs/redesign/short-mobile.png
index 61af483..ad07429 100644
Binary files a/docs/redesign/short-mobile.png and b/docs/redesign/short-mobile.png differ
diff --git a/web/index.html b/web/index.html
index a0ffdea..55de86c 100644
--- a/web/index.html
+++ b/web/index.html
@@ -318,7 +318,7 @@
-
0:00 drop
+ 0:00 drop
diff --git a/web/src/blob/caustics.ts b/web/src/blob/caustics.ts
index 700e069..6b8b9df 100644
--- a/web/src/blob/caustics.ts
+++ b/web/src/blob/caustics.ts
@@ -141,7 +141,7 @@ export class Caustics {
gl.uniform1f(u.uHue, look.hue);
gl.uniform1f(u.uIri, look.iri);
gl.uniform1f(u.uHeat, look.heat);
- gl.uniform1f(u.uAmount, c.amount * look.caustics * 0.22);
+ gl.uniform1f(u.uAmount, c.amount * look.caustics * 0.65);
gl.disable(gl.DEPTH_TEST);
gl.depthMask(false);
gl.bindVertexArray(this.vao);
diff --git a/web/src/blob/shaders.ts b/web/src/blob/shaders.ts
index a08c1c0..4186a3c 100644
--- a/web/src/blob/shaders.ts
+++ b/web/src/blob/shaders.ts
@@ -265,25 +265,22 @@ void main() {
// Looking through the surface bends the artwork like water does.
vec2 bend = n.xy * (0.22 + 0.1 * uDrop) * uMorph;
vec2 uv = 0.5 + vec2(vObj.x, -vObj.y) * 0.48 - vec2(bend.x, -bend.y) * 0.5;
- vec2 split = n.xy * (0.001 + 0.003 * uKick + 0.002 * uSnare) * uMorph;
+ vec2 split = n.xy * (0.008 + 0.025 * uKick + 0.015 * uSnare + 0.01 * uGrowl * uBass) * uMorph;
// Turned sideways the projection smears; blur it into flowing colour instead.
float blur = 4.0 * pow(1.0 - abs(vObj.z), 3.0) * uMorph;
vec3 art = vec3(texture(uArt, uv + split, blur).r, texture(uArt, uv, blur).g, texture(uArt, uv - split, blur).b);
- float luminance = dot(art, vec3(0.2126, 0.7152, 0.0722));
- vec3 trace = mix(vec3(luminance), art, 0.22);
- // Preserve the flat artwork during entry; blackened metal takes over as it morphs.
- art = mix(art, vec3(0.035, 0.038, 0.042) + trace * 0.22, uMorph);
+ art = mix(art, smoothstep(0.0, 1.0, art), 0.6 * uDrop);
vec3 light = normalize(vec3(-0.5, 0.7, 0.6));
float spec = pow(max(dot(n, normalize(light + v)), 0.0), mix(40.0, 140.0, uMetal)) * mix(0.35, 0.8, uMetal);
// The hue sways around the accent but never leaves it, so the drop stays in the
// card's colours.
- vec3 rim = hueRotate(mix(vec3(0.72), uAccent, 0.16), uHue + uIri * (fres + 0.5 * sin(uTime * 0.3)));
+ vec3 rim = hueRotate(uAccent, uHue + uIri * (fres + 0.5 * sin(uTime * 0.3)));
rim = mix(rim, vec3(1.0, 0.35, 0.1), uHeat * 0.6);
// Oil-slick fringes: each channel cycles at its own phase across the edge, worn
// over the accent, more for iridescent tracks.
vec3 slick = 0.5 + 0.5 * cos(6.2831853 * (fres * 1.6 + vec3(0.0, 0.33, 0.67) + uTime * 0.05));
- rim = mix(rim, rim * 0.4 + slick * length(rim) * 0.75, 0.035 + 0.08 * uIri);
+ rim = mix(rim, rim * 0.4 + slick * length(rim) * 0.75, 0.25 + 0.5 * uIri);
// The fresnel per channel, red reaching in further than blue: a lens fringe.
float nv = 1.0 - max(dot(n, v), 0.0);
vec3 fresC = vec3(nv * nv * nv * mix(1.0, nv, -0.35), fres, nv * nv * nv * mix(1.0, nv, 0.35));
@@ -293,13 +290,13 @@ void main() {
// Out on a spike the liquid turns to ink, like ferrofluid: the artwork darkens and
// the gloss takes over.
vec3 col = art * mix(1.0, 0.45, fres * uMorph) * (1.0 - 0.6 * vSpike);
- col += rim * fresC * (0.3 + 0.35 * uKick + 0.15 * uDrop + 0.12 * uBass) * uMorph;
- col += mix(rim, vec3(1.0), 0.5) * edge * (0.15 + 0.45 * uKick + 0.25 * uDrop) * uMorph;
+ col += rim * fresC * (0.65 + 0.9 * uKick + 0.5 * uDrop + 0.5 * uBass) * uMorph;
+ col += mix(rim, vec3(1.0), 0.5) * edge * (0.3 + 1.2 * uKick + 0.8 * uDrop) * uMorph;
col += spec * (1.0 + 0.8 * vSpike) * uMorph;
// Gloss: a dark room lit by the accent from above, and one soft window. They stay
// put while the surface turns under them, which is what makes a spin read.
vec3 r = reflect(-v, n);
- vec3 env = mix(vec3(0.015), mix(vec3(0.5), uAccent * 0.5, 0.12), pow(smoothstep(-0.4, 1.0, r.y), 2.0));
+ vec3 env = mix(vec3(0.015), uAccent * 0.5, pow(smoothstep(-0.4, 1.0, r.y), 2.0));
env += 0.5 * smoothstep(0.92, 0.985, dot(r, normalize(vec3(-0.5, 0.7, 0.5))));
col += env * (0.1 + 0.5 * fres + 0.5 * vSpike) * uMorph;
// The icon sits on a disc of frosted glass, painted onto the surface so it turns
diff --git a/web/src/cards.ts b/web/src/cards.ts
index 60d562e..ebb5dd7 100644
--- a/web/src/cards.ts
+++ b/web/src/cards.ts
@@ -283,7 +283,7 @@ export class Card {
ctx.clearRect(0, 0, w, h);
const data = this.track.waveform || [];
const max = Math.max(1, ...data);
- const accent = getComputedStyle(canvas).getPropertyValue("--accent").trim() || "#e9e8e3";
+ const accent = getComputedStyle(canvas).getPropertyValue("--card-accent").trim() || "#ff4d6d";
const base = Math.round(h * (1 - MIRROR)) - 1;
const below = h - base - 2;
const level = (i: number) => (data.length ? data[Math.floor((i / bars) * data.length)] / max : 0.15);
diff --git a/web/src/color.ts b/web/src/color.ts
index 579ac05..0ee48a3 100644
--- a/web/src/color.ts
+++ b/web/src/color.ts
@@ -50,8 +50,8 @@ async function compute(src: string): Promise {
const BACKDROP = 64; // px square; it is shown blurred, so this is plenty
const BLUR = 2; // px each side, per box blur pass at that size
-const DIM = 0.16;
-const SATURATE = 0.15;
+const DIM = 0.45;
+const SATURATE = 1.3;
/// The card's background from the artist image, as a data URL: cropped square,
/// blurred and darkened here, once, as `.bg`'s CSS filter would do on every frame.
diff --git a/web/src/main.ts b/web/src/main.ts
index ef1ff9a..e8cd898 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -1319,7 +1319,7 @@ function signedIn(user: User, pl8list?: Pl8Status): void {
startEl.classList.remove("welcome");
accountBtn.textContent = guest ? "sign in" : (user.name || user.handle)[0]?.toUpperCase() ?? "?";
accountBtn.classList.toggle("guest", guest);
- accountBtn.style.backgroundColor = guest ? "var(--glass-strong)" : "var(--accent)";
+ accountBtn.style.backgroundColor = guest ? "var(--glass-strong)" : accentForName(user.handle);
accountBtn.title = guest ? "listening as a guest: sign in, or forget me" : `@${user.handle}`;
$("account-handle").textContent = guest ? "listening as a guest" : `@${user.handle}`;
$("delete-handle").textContent = user.handle;
diff --git a/web/src/style.css b/web/src/style.css
index 3996e43..e0240a9 100644
--- a/web/src/style.css
+++ b/web/src/style.css
@@ -5,21 +5,20 @@
:root {
color-scheme: dark;
- --bg: #090a0b;
+ --bg: #000;
--fg: #f2f2f2;
--muted: #b5b5b8;
--faint: #96969a;
- --accent: #e9e8e3;
- --danger: #ff8b8b;
- --panel: #141517;
+ --accent: #ff4d6d;
+ --panel: rgba(18, 18, 20, 0.94);
--line: #ffffff1f;
--line-strong: #333;
--scrim: #000b;
--glass: rgba(255, 255, 255, 0.1);
--glass-strong: rgba(255, 255, 255, 0.2);
- --radius-s: 4px;
- --radius-m: 6px;
- --radius-l: 6px;
+ --radius-s: 8px;
+ --radius-m: 14px;
+ --radius-l: 18px;
--shadow: 0 16px 40px #000a;
--font: Barlow, system-ui, sans-serif;
--font-display: "Barlow Condensed", "Arial Narrow", sans-serif;
@@ -50,7 +49,7 @@ button {
cursor: pointer;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
- border-radius: 5px;
+ border-radius: 999px;
padding: 0.35em 0.9em;
}
@@ -94,7 +93,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
display: flex;
gap: 0.2rem;
padding: 0.2rem;
- border-radius: 5px;
+ border-radius: 999px;
background: var(--glass);
backdrop-filter: blur(12px);
}
@@ -151,7 +150,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
appearance: none;
-webkit-appearance: none;
background: linear-gradient(90deg, var(--fg) var(--v), #ffffff30 var(--v)) center / 100% 4px no-repeat;
- border-radius: 5px;
+ border-radius: 999px;
cursor: pointer;
}
#volume-slider::-webkit-slider-thumb {
@@ -251,7 +250,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
flex: 1 1 10rem;
min-width: 0;
padding: 0.55em 0.9em;
- border-radius: 5px;
+ border-radius: 999px;
border: 1px solid var(--line-strong);
background: #111;
}
@@ -262,7 +261,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
.modes input { position: absolute; opacity: 0; pointer-events: none; }
.modes label:has(input) {
padding: 0.3em 0.9em;
- border-radius: 5px;
+ border-radius: 999px;
border: 1px solid #ffffff40;
font-size: 0.85rem;
transition: background var(--fast);
@@ -278,7 +277,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
width: 2.6rem;
height: 1.5rem;
margin: 0;
- border-radius: 5px;
+ border-radius: 999px;
background: #ffffff26;
position: relative;
cursor: pointer;
@@ -439,7 +438,7 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
position: absolute;
inset: -40px;
background: #111 center / cover no-repeat;
- filter: blur(40px) brightness(0.16) saturate(0.15);
+ filter: blur(40px) brightness(0.45) saturate(1.3);
}
/* Already blurred and darkened (color.ts): a CSS blur over the whole screen would
be worked out again whenever anything on top of it moves. */
@@ -450,7 +449,7 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
inset: 0;
pointer-events: none;
background: radial-gradient(55% 40% at 50% 40%, color-mix(in oklab, var(--card-accent) 55%, transparent), transparent 70%);
- opacity: calc(0.03 + var(--energy) * 0.07);
+ opacity: calc(0.18 + var(--energy) * 0.55);
will-change: opacity;
}
@@ -506,11 +505,11 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
.card.liquid .art::before {
content: "";
position: absolute;
- inset: -25%;
+ inset: -65%;
background: radial-gradient(closest-side, color-mix(in oklab, var(--card-accent) 83%, transparent), transparent);
/* Pulses through transform and opacity only, so it never has to be painted again. */
transform: scale(calc(0.87 + var(--energy) * 0.13));
- opacity: calc(0.06 + var(--energy) * 0.12);
+ opacity: calc(0.34 + var(--energy) * 0.66);
will-change: transform, opacity;
pointer-events: none;
}
@@ -611,27 +610,27 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
gap: 0.3em;
font-size: 0.75rem;
padding: 0.2em 0.75em 0.2em 0.55em;
- border-radius: 5px;
+ border-radius: 999px;
border: 1px solid #ffffff40;
background: transparent;
transition: background var(--fast), border-color var(--fast);
}
.pill svg { width: 0.95em; height: 0.95em; }
.pill:hover { background: var(--glass); }
-.follow.on { background: var(--accent); color: #111; border-color: transparent; }
+.follow.on { background: color-mix(in oklab, var(--card-accent) 25%, transparent); border-color: transparent; }
.chips { display: flex; gap: 0.4rem; flex-wrap: wrap; justify-content: center; }
.chips span {
font-size: 0.75rem;
padding: 0.1em 0.6em;
- border-radius: 5px;
+ border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
}
.chips .free { background: #1f7a4d; }
.chips .new { background: var(--fg); color: #000; font-weight: 700; }
-.chips span:first-child { background: #252729; }
+.chips span:first-child { background: color-mix(in oklab, var(--card-accent) 35%, transparent); }
.chips .preview { background: #6a4b16; }
-.chips .tag { background: transparent; box-shadow: inset 0 0 0 1px var(--line); }
+.chips .tag { background: transparent; box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--card-accent) 60%, transparent); }
.desc {
margin: 0;
@@ -791,7 +790,7 @@ body.no-wave-comments :is(.wave-comments, .wc-bubble), body.zen :is(.wave-commen
margin-right: auto;
padding: 0.05em 0.6em;
border: 1px solid var(--line-strong);
- border-radius: 5px;
+ border-radius: 999px;
background: none;
color: var(--fg);
font-size: inherit;
@@ -889,7 +888,7 @@ body.no-wave-comments :is(.wave-comments, .wc-bubble), body.zen :is(.wave-commen
bottom: calc(max(1rem, env(safe-area-inset-bottom)) + 3.4rem);
transform: translate(-50%, 1rem);
padding: 0.6em 1.1em;
- border-radius: 5px;
+ border-radius: 999px;
background: #222e;
border: 1px solid #ffffff22;
font-size: 0.9rem;
@@ -1010,7 +1009,7 @@ body.zen #up-next { display: none; }
}
.start-welcome a { color: inherit; }
/* A guest's account button says what it is for. */
-.avatar-btn.guest { width: auto; padding: 0 0.9rem; border-radius: 5px; font-weight: 600; font-size: 0.85rem; color: var(--fg); }
+.avatar-btn.guest { width: auto; padding: 0 0.9rem; border-radius: 999px; font-weight: 600; font-size: 0.85rem; color: var(--fg); }
/* Radio sheet: a bottom sheet on phones, a centred box elsewhere. */
#station {
@@ -1144,7 +1143,10 @@ button.primary:disabled { opacity: 0.35; cursor: default; }
display: grid;
place-items: center;
padding: 1rem;
- background: var(--bg);
+ background:
+ radial-gradient(60rem 40rem at 20% 10%, #ff4d6d33, transparent 60%),
+ radial-gradient(50rem 40rem at 90% 90%, #4d6dff2e, transparent 60%),
+ #050507;
}
.login-box {
@@ -1225,7 +1227,7 @@ button.primary:disabled { opacity: 0.35; cursor: default; }
width: 2.6rem;
height: 0.3rem;
margin: 0.55rem auto 0;
- border-radius: 5px;
+ border-radius: 999px;
background: #fff5;
}
.content { width: calc(100vw - 2rem); margin-bottom: 6.5rem; gap: 0.6rem; }
@@ -1269,7 +1271,7 @@ body.zen .content { gap: 0.3rem; }
.bar-end {
gap: 0.15rem;
padding: 0.25rem;
- border-radius: 5px;
+ border-radius: 999px;
background: var(--glass);
backdrop-filter: blur(12px);
}
@@ -1333,38 +1335,47 @@ body.zen .content { gap: 0.3rem; }
.wc-bubble::before { display: none; }
}
-/* Monochrome soundsystem surfaces. Keep the controls still around the sculpture. */
-.logo, .start-logo, .login-box h1 { font-family: var(--font-display); font-weight: 700; letter-spacing: 0.025em; }
-.title { font-family: var(--font-display); font-weight: 700; line-height: 1.05; letter-spacing: 0.015em; }
-.time, .counts, .chips, .station-kicker, .up-next-label { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; }
-#mode-tabs, .bar-end, #up-next { background: var(--panel); border: 1px solid var(--line); backdrop-filter: none; }
-button { min-height: 2.75rem; }
-button:hover { border-color: #737578; }
-button:active { filter: brightness(0.85); }
-button:disabled { cursor: default; opacity: 0.45; }
-.icon-btn { min-width: 2.75rem; min-height: 2.75rem; }
-.chips span { border-radius: 3px; background: #202225; color: var(--fg); border: 1px solid var(--line); }
-.chips span.free, .chips span.new { background: #252729; color: var(--fg); }
-.act .ring { background: #191b1d; border: 1px solid var(--line); backdrop-filter: none; }
-.art { box-shadow: 0 12px 35px #0009, 0 0 0 1px #ffffff24; scale: 1; }
-/* Fade the canvas spill before the title or navigation, including the caustics. */
-.liquid-canvas { mask-image: linear-gradient(transparent 2%, #000 18% 72%, transparent 88%); }
-.card.liquid .art { margin-bottom: 2.8rem; }
-.sheet, .station-box { box-shadow: 0 20px 80px #000b; }
-.track-row { padding-block: 0.7rem; }
-.login-box button[type=submit], .help-box button { color: #111; }
-.link.danger, #login-error { color: var(--danger); }
-button.danger:not(.link) { background: var(--danger); color: #111; }
-#login input { border-radius: var(--radius-s); }
+/* Color belongs to the artist; controls keep their own clear spacing. */
+.title { font-family: var(--font-display); line-height: 1.08; }
+.card.liquid .art { margin-bottom: 2rem; }
+.byline { gap: 0.6rem; row-gap: 0.65rem; }
+.bar-end { gap: 0.45rem; }
+.bar-end .icon-btn, .avatar-btn { flex-shrink: 0; border-radius: 50%; }
+#discovery-options { top: 4.8rem; }
+#active-filters { top: 7.8rem; }
+.time { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 0.75rem 0.5rem; }
+.time .cur { grid-column: 1; grid-row: 1; justify-self: start; }
+.time .counts { grid-column: 2; grid-row: 1; text-align: center; overflow-wrap: anywhere; }
+.time .dur { grid-column: 3; grid-row: 1; justify-self: end; }
+.time .to-drop {
+ grid-column: 1 / -1; grid-row: 2; justify-self: center;
+ margin: 0.25rem 0 0; min-width: 8rem; min-height: 44px;
+ justify-content: center; padding: 0.5rem 1.5rem;
+ font: 600 1rem/1.2 var(--font); border-radius: 999px;
+ background: color-mix(in oklab, var(--card-accent) 18%, #111);
+ border-color: color-mix(in oklab, var(--card-accent) 65%, #555);
+ color: var(--fg);
+}
+.time .to-drop:hover { background: color-mix(in oklab, var(--card-accent) 35%, #111); color: var(--fg); }
@media (max-width: 640px) {
- #mode-tabs button { padding-inline: 0.7em; }
- .content { margin-top: 3rem; }
+ .bar-end { gap: 0.45rem; }
+ .bar-end .icon-btn, .avatar-btn { width: 2.75rem; height: 2.75rem; }
.art { width: min(14rem, 29dvh, 58vw); }
- .card.liquid .art { margin-bottom: 1.8rem; }
- .actions { gap: 0.5rem; }
+ .card.liquid .art { margin-bottom: 1.5rem; }
+ .content { margin-top: 2.5rem; }
+ #discovery-options { top: calc(max(0.6rem, env(safe-area-inset-top)) + 6.4rem); gap: 0.75rem; }
+ #active-filters { top: calc(max(0.6rem, env(safe-area-inset-top)) + 9.2rem); }
}
@media (max-width: 640px) and (max-height: 700px) {
- .art { width: min(10rem, 22dvh, 48vw); }
- .card.liquid .art { margin-bottom: 1.2rem; }
+ .art { width: min(9rem, 21dvh, 48vw); }
+ .card.liquid .art { margin-bottom: 1rem; }
.content { gap: 0.25rem; }
}
+/* Chart selectors occupy a third header row on phones. */
+@media (max-width: 640px) {
+ body:has(#discovery-options:not([hidden])) .content { margin-top: 7rem; }
+ body:has(#discovery-options:not([hidden])) .art { width: min(12rem, 25dvh, 52vw); }
+}
+@media (max-width: 640px) and (max-height: 700px) {
+ body:has(#discovery-options:not([hidden])) .art { width: min(8rem, 18dvh, 44vw); }
+}