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 + '>' +
+ '' +
+ '<' + nameTag + ' class="fc-name' + (m.tier ? " " + m.tier : "") + '"' + nameAttrs + '>' + esc(m.name) + '' + nameTag + '>' +
+ '' +
+ '' +
'' +
'' +
'' +
@@ -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;