diff --git a/assets/alts/butterfly.png b/assets/alts/butterfly.png new file mode 100644 index 0000000..9c86514 Binary files /dev/null and b/assets/alts/butterfly.png differ diff --git a/assets/alts/clove.png b/assets/alts/clove.png new file mode 100644 index 0000000..13b8f28 Binary files /dev/null and b/assets/alts/clove.png differ diff --git a/assets/alts/mrow.png b/assets/alts/mrow.png new file mode 100644 index 0000000..91e2b9f Binary files /dev/null and b/assets/alts/mrow.png differ diff --git a/assets/alts/uzi.png b/assets/alts/uzi.png new file mode 100644 index 0000000..48f122d Binary files /dev/null and b/assets/alts/uzi.png differ diff --git a/css/main.css b/css/main.css index 753c3e5..a1c7e3d 100644 --- a/css/main.css +++ b/css/main.css @@ -2039,6 +2039,34 @@ body:has(.friends-wrap) .hub { padding-top: 0.1rem; } +.fc-name-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.15rem 0.3rem; + min-width: 0; +} +/* name claims the full first line (so a clan tag never truncates it); the + tag chip wraps onto its own line just beneath when there isn't room */ +.fc-name-row .fc-name { flex: 1 1 100%; min-width: 0; } + +/* Discord server (clan) tag chip */ +.fc-tag { + display: inline-flex; + align-items: center; + gap: 0.15rem; + flex-shrink: 0; + padding: 0.05rem 0.3rem; + border-radius: 5px; + background: var(--surface-2); + font-size: 0.56rem; + font-weight: 700; + letter-spacing: 0.02em; + color: var(--text); +} +.fc-tag[hidden] { display: none; } +.fc-tag-badge { width: 12px; height: 12px; display: block; } + .fc-name { font-size: 0.95rem; font-weight: 700; @@ -2076,6 +2104,35 @@ a.fc-name:hover { color: rgb(var(--accent-rgb)); } .fc-badge { width: 16px; height: 16px; display: block; } .fc-badge-link { display: inline-flex; line-height: 0; } +/* ---- Alts (active + dead) ---- */ +.fc-name.active-alt::before { content: "🎭 "; } +.fc-name.dead-alt::before { content: "πŸ’€ "; } + +/* dead alts: banned accounts β€” greyed out, name struck through, no status */ +.friend-card.tier-dead-alt .fc-pfp { + filter: grayscale(1) brightness(0.6); +} +.friend-card.tier-dead-alt .fc-name { + color: var(--overlay-1); + text-decoration: line-through; +} +.friend-card.tier-dead-alt .fc-status { display: none; } + +/* the "struck off" diagonal slash across the avatar */ +.friend-card.tier-dead-alt .fc-avatar::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: 132%; + height: 4px; + background: var(--red); + border-radius: 2px; + transform: translate(-50%, -50%) rotate(-45deg); + box-shadow: 0 0 0 2px var(--surface-0); + pointer-events: none; +} + /* ===================================================================== * MUSIC PAGE (/music) β€” merged in from music.css. * Hero classes are .mnp-* to avoid colliding with the .np-* now-playing @@ -3365,7 +3422,10 @@ body:has(.presence-stage) { .presence-stage .presence-card.has-banner-color .pc-av-img { width: 120px; height: 120px; - border: 5px solid var(--surface-0); + /* solid dark plate + ring so the banner sits clearly BEHIND the avatar + (the PFP itself is partly transparent) */ + background: var(--crust); + border: 6px solid var(--crust); } .presence-stage .pc-av-deco, .presence-stage .presence-card.has-banner .pc-av-deco, diff --git a/js/friends.js b/js/friends.js index 7c0a314..de9bf11 100644 --- a/js/friends.js +++ b/js/friends.js @@ -1,26 +1,6 @@ (function friends() { "use strict"; - // ===================================================================== - // FRIENDS CONFIG ✏️ EDIT ME - // --------------------------------------------------------------------- - // Each friend is one card. Fields: - // name (required) display name - // img (required) static fallback image (used until/unless Lanyard - // gives a live avatar, and forever if they're not - // in the Lanyard server or the API is down) - // discordId (optional) Discord user ID. If set AND they're in the - // Lanyard server (https://discord.gg/lanyard), - // the card goes live: real avatar, status dot - // (green/idle/dnd/offline), badges + Nitro banner. - // Leave null for friends who aren't in the server β€” - // they get a blue "not connected" dot + static img. - // link (optional) where the card links to (their site, etc.) - // - // To make Camilla (etc.) live, paste her Discord ID into discordId. - // IMPORTANT: keep IDs as STRINGS in quotes ("123..."), not bare numbers β€” - // Discord IDs are too big for a JS number and get rounded (wrong user!). - // ===================================================================== var FRIENDS = [ { title: "FiancΓ©e", @@ -53,6 +33,16 @@ members: [ { name: "Aureal", img: "/assets/friends/aureal.gif", tier: "known", discordId: "1498977251134279900", link: "https://aureal.dev/" } ] + }, + { + title: "Alts", + subtitle: "My other accounts, dead or alive", + members: [ + { name: "Uzi", img: "/assets/alts/uzi.png", tier: "active-alt", discordId: "526626867973849123", link: null }, + { name: "Clove <3", img: "/assets/alts/clove.png", tier: "dead-alt", discordId: "1125844710511104030", link: null}, + { name: "Clove ⛀", img: "/assets/alts/butterfly.png", tier: "dead-alt", discordId: "514994021970739201", link: null }, + { name: "Mrow", img: "/assets/alts/mrow.png", tier: "dead-alt", discordId: "219480349053288450", link: null } + ] } ]; @@ -89,8 +79,12 @@ } function bannerUrl(id, hash) { if (!id || !hash) return null; - var ext = String(hash).startsWith("a_") ? "gif" : "png"; - return proxyImg("https://cdn.discordapp.com/banners/" + id + "/" + hash + "." + ext + "?size=480", { w: 480 }); + // Animated banners (a_) must be requested WITHOUT the .gif extension: + // Discord's CDN throws HTTP 415 for some a_*.gif banners, but the + // extension-less URL works (wsrv then re-serves it as cookieless webp). + var animated = String(hash).startsWith("a_"); + var url = "https://cdn.discordapp.com/banners/" + id + "/" + hash + (animated ? "" : ".png") + "?size=480"; + return proxyImg(url, { w: 480 }); } // dstn.to badge list β†’ small icon row (same source the presence card uses) @@ -107,6 +101,23 @@ }).join(""); } + // Discord server (clan) tag β€” the little guild badge + tag next to a name + function guildTagBadgeUrl(pg) { + if (!pg || !pg.badge || !pg.identity_guild_id) return null; + return proxyImg("https://cdn.discordapp.com/guild-tag-badges/" + pg.identity_guild_id + "/" + pg.badge + ".png?size=24"); + } + function renderClanTag(refs, pg) { + if (!refs.tag) return; + if (pg && pg.tag && pg.identity_enabled) { + var b = guildTagBadgeUrl(pg); + refs.tag.innerHTML = (b ? '' : "") + + '' + esc(pg.tag) + ""; + refs.tag.hidden = false; + } else { + refs.tag.hidden = true; + } + } + // ---- build one card ------------------------------------------------- function buildCard(m) { var card = document.createElement("article"); @@ -124,7 +135,10 @@ '' + '' + '' + - '<' + nameTag + ' class="fc-name' + (m.tier ? " " + m.tier : "") + '"' + nameAttrs + '>' + esc(m.name) + '' + + '' + + '<' + nameTag + ' class="fc-name' + (m.tier ? " " + m.tier : "") + '"' + nameAttrs + '>' + esc(m.name) + '' + + '' + + '' + '' + '' + '' + @@ -135,6 +149,7 @@ pfp: card.querySelector(".fc-pfp"), statusDot: card.querySelector(".fc-status"), user: card.querySelector(".fc-user"), + tag: card.querySelector(".fc-tag"), badges: card.querySelector(".fc-badges"), banner: card.querySelector(".fc-banner") }; @@ -163,6 +178,7 @@ refs.statusDot.title = STATUS_TITLE[status] || "Offline"; if (u.avatar) refs.pfp.src = avatarUrl(u); if (u.username) refs.user.textContent = "@" + u.username; + renderClanTag(refs, u.primary_guild); return true; }) .catch(function () { @@ -228,7 +244,8 @@ group.members.forEach(function (m) { var refs = buildCard(m); grid.appendChild(refs.el); - if (m.discordId) { + // dead alts are banned/retired β€” never pull live data for them + if (m.discordId && m.tier !== "dead-alt") { liveMembers.push({ m: m, refs: refs }); refreshMember(m, refs); } diff --git a/js/now-playing.js b/js/now-playing.js index f86e477..4d76bd6 100644 --- a/js/now-playing.js +++ b/js/now-playing.js @@ -237,8 +237,12 @@ // ---- banner / bio / connected accounts (extras for the /discord page) --- function bannerUrl(id, hash) { if (!id || !hash) return null; - const ext = String(hash).startsWith("a_") ? "gif" : "png"; - return proxyImg("https://cdn.discordapp.com/banners/" + id + "/" + hash + "." + ext + "?size=600", { w: 600 }); + // Animated banners (a_) must be requested WITHOUT the .gif extension: + // Discord's CDN throws HTTP 415 for some a_*.gif banners, but the + // extension-less URL works (wsrv then re-serves it as cookieless webp). + const animated = String(hash).startsWith("a_"); + const url = "https://cdn.discordapp.com/banners/" + id + "/" + hash + (animated ? "" : ".png") + "?size=600"; + return proxyImg(url, { w: 600 }); } function applyBanner(url, fallbackColor) { if (!bannerEl) return;