- {#if extras.profile.listeners}
{fmtCount(extras.profile.listeners)} listeners{/if}
- {#if extras.profile.plays}
{fmtCount(extras.profile.plays)} plays{/if}
+
+ {#if extras?.profile?.listeners || extras?.profile?.plays || playTime}
+
+ {[extras?.profile?.listeners ? `${fmtCount(extras.profile.listeners)} listeners` : "", extras?.profile?.plays ? `${fmtCount(extras.profile.plays)} plays` : "", playTime ? `${playTime} played here` : ""].filter(Boolean).join(" · ")}
{/if}
- {#if extras.bio}
-
(bioOpen = !bioOpen)} role="button" tabindex="0" aria-expanded={bioOpen} onkeydown={() => {}}>{extras.bio}
+ {#if extras?.bio}
+
(bioOpen = !bioOpen)} role="button" tabindex="0" aria-expanded={bioOpen} onkeydown={() => {}}>{extras.bio}
{/if}
- {#if extras.profile?.tags?.length}
-
{#each extras.profile.tags.slice(0, 6) as t (t)}{t}{/each}
+ {#if extras?.profile?.tags?.length}
+
{#each extras.profile.tags.slice(0, 6) as t (t)}{t}{/each}
+ {/if}
+ {#if socials.length}
+
+ {#each socials as link (link.kind + link.url)}
+
+ {/each}
+
{/if}
{/if}
-{#if !albums.length && !popular.length && !tabs.length && entry?.status !== "loading"}
+{#if entry?.status === "error" && !albums.length && !popularAll.length}
+
The catalog did not answer — only what this phone holds is here.
+{/if}
+{#if empty}
Nothing here yet.No records by {shownName} on this phone, and the catalog {entry?.status === "error" ? "didn't answer" : "had nothing to add"}.
{/if}
diff --git a/src/mobile/shell.css b/src/mobile/shell.css
index cdf6741..bdf2409 100644
--- a/src/mobile/shell.css
+++ b/src/mobile/shell.css
@@ -555,7 +555,7 @@ html[data-material="solid"] .card, html[data-material="solid"] .group { backgrou
/* ── 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: 258px; display: flex; align-items: flex-end; overflow: hidden; margin-bottom: -6px; }
+.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 24%; transform: scale(1.04); }
/* the blurred SEED: the picture the library already has, soft and
oversized, until a sharp one exists */
@@ -568,18 +568,18 @@ html[data-material="solid"] .card, html[data-material="solid"] .group { backgrou
}
/* The hero's picture again, behind the scroller and outside its mask, so the
photograph is allowed under the clock while the words are not. Its geometry
- has to match .hero/.hero-img exactly — 258px tall, the same 24% focal point,
+ has to match .hero/.hero-img exactly — 300px tall, the same 24% focal point,
the same 1.04 scale — or the seam shows. */
.hero-behind {
position: absolute;
left: 0;
right: 0;
top: 0;
- height: 258px;
+ height: 300px;
z-index: 0;
pointer-events: none;
/* the clip .hero gives its own picture; without it the 1.04 below renders
- 268px in a 258px box and five pixels of photograph appear under the
+ 312px in a 300px box and twelve pixels of photograph appear under the
hero's foot, below the black the scrim fades to */
overflow: hidden;
transform: translateY(var(--hero-shift, 0px));
@@ -1039,17 +1039,11 @@ html[data-material="solid"] .halfsheet { background: #1c1c20; backdrop-filter: n
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; }
@@ -1683,3 +1677,81 @@ html[data-motion="off"] *, html[data-motion="off"] *::before, html[data-motion="
}
.dg-eds { display: flex; flex-direction: column; gap: 6px; padding: 0 var(--pad) 12px calc(var(--pad) + 58px); }
.dg-eds .ed { width: 100%; }
+
+/* ── the artist page, B (2026-09-18) ──────────────────────────────────────
+ The editorial shape Kyle picked from three mockups: the name ON the
+ 300dp portrait with the play disc beside it, one line of facts, the
+ latest release first, Top Songs as swipeable columns of four, the
+ records as carousels, About last. The bar over the hero is the shell's
+ two floating discs; nothing else. */
+.hero.ar .hero-in { padding-bottom: 14px; }
+.hero-in.row2 { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; }
+.hero-in.row2 .hero-id { min-width: 0; flex: 1; }
+.hero-in.row2 .disc { flex: none; margin-bottom: 2px; }
+.hero.ar .hero-name { margin: 0; overflow-wrap: anywhere; }
+.ar-kick { font-size: 11px; font-weight: 800; letter-spacing: 0.9px; text-transform: uppercase; color: rgba(255, 255, 255, 0.7); margin-bottom: 5px; text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5); }
+.ar-acts { display: flex; align-items: center; gap: 10px; padding: 14px var(--pad) 6px; }
+.ar-facts { margin-left: auto; min-width: 0; font-size: 12.5px; font-weight: 600; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.ar-facts.wide { margin: 0 0 8px; white-space: normal; }
+.btn-line { box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.22); color: #fff; background: transparent; }
+.btn-line.on { box-shadow: inset 0 0 0 1.5px var(--accent); color: var(--accent-hover); }
+.btn-line:disabled { opacity: 0.5; }
+.btn.sm { height: 34px; padding: 0 14px; font-size: 13px; }
+
+/* the latest release */
+.ar-latest { display: flex; gap: 14px; margin: 6px var(--pad) 4px; padding: 12px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--ring); }
+.ar-latest-art { flex: none; width: 96px; height: 96px; border-radius: 10px; overflow: hidden; background: rgba(255, 255, 255, 0.06); display: block; padding: 0; }
+.ar-latest-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
+.ar-latest-art .ph { display: flex; width: 100%; height: 100%; align-items: center; justify-content: center; color: var(--text-muted); }
+.ar-latest-copy { min-width: 0; flex: 1; display: flex; flex-direction: column; justify-content: center; }
+.ar-latest-copy .k { font-size: 10.5px; letter-spacing: 0.8px; text-transform: uppercase; font-weight: 800; color: var(--accent-hover); }
+.ar-latest-t { font-size: 15px; font-weight: 800; letter-spacing: -0.2px; line-height: 1.2; margin: 3px 0 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.ar-latest-copy .m { font-size: 12px; color: var(--text-secondary); font-weight: 600; }
+.ar-latest-acts { display: flex; gap: 8px; margin-top: 10px; }
+
+/* top songs, in columns of four */
+.row.ar-pop .ar-n { width: 18px; text-align: right; color: var(--text-muted); font-weight: 700; font-variant-numeric: tabular-nums; font-size: 13px; flex: none; }
+.row.ar-pop .r { display: inline-flex; align-items: center; gap: 10px; color: var(--text-muted); }
+.row.ar-pop .ar-dur { font-variant-numeric: tabular-nums; font-size: 12.5px; }
+.ar-pages { display: flex; gap: 12px; overflow-x: auto; scroll-snap-type: x mandatory; padding: 0 var(--pad); scrollbar-width: none; scroll-padding-inline: var(--pad); }
+.ar-pages::-webkit-scrollbar { display: none; }
+.ar-col { flex: none; width: 86%; scroll-snap-align: start; }
+.ar-col .row { margin: 0 calc(-1 * var(--pad)); }
+.ar-col .row.tight { padding-top: 6px; padding-bottom: 6px; }
+.ar-dots { display: flex; justify-content: center; gap: 6px; padding: 6px 0 2px; }
+.ar-dots i { width: 6px; height: 6px; border-radius: 50%; background: rgba(255, 255, 255, 0.22); transition: background var(--m-fast); }
+.ar-dots i.on { background: #fff; }
+
+/* the carousels */
+.rail { scroll-padding-inline: var(--pad); }
+.ar-rail .release { width: 156px; }
+.ar-rail.sm .release { width: 132px; }
+.more-link.ar-disco { display: inline-flex; align-items: center; gap: 4px; padding: 14px var(--pad) 4px; }
+
+/* about */
+.ar-about { padding: 0 var(--pad); }
+.ar-bio { font-size: 13.5px; line-height: 1.6; color: var(--text-secondary); display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; margin: 0; }
+.ar-bio.open { display: block; }
+.ar-tags { padding: 0; margin-top: 12px; }
+.ar-socials { display: flex; gap: 6px; margin-top: 12px; }
+.ar-socials .iconbtn { background: rgba(255, 255, 255, 0.055); }
+
+/* skeletons: the shape of what is coming, so the page never jumps when it
+ arrives (the mockup's .skel, at the app's radii) */
+.skel { display: block; background: rgba(255, 255, 255, 0.07); border-radius: 6px; animation: skel-breathe 1.4s ease-in-out infinite; }
+.skel.ln { height: 11px; }
+.skel.art { width: 46px; height: 46px; border-radius: 8px; flex: none; }
+.skel.cover-wrap { aspect-ratio: 1; width: 100%; border-radius: var(--r-art); }
+.skel-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
+.skel-row .b { flex: 1; min-width: 0; }
+.skel-card { pointer-events: none; }
+.ar-latest.skel-card .ar-latest-art { background: rgba(255, 255, 255, 0.07); }
+@keyframes skel-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
+
+/* the listener / progress bars other screens draw under a row (CatalogAlbum,
+ Transfers, Diagnostics) — the artist page's Popular used them too, once */
+.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; }
+/* a rail's cards line up at the TOP — a two-line title must not lift its cover */
+.ar-rail { align-items: flex-start; }