slyfox / cards.html
adarshxs's picture
adarshxs HF Staff
animations
b3bb47a
Raw History Blame Contribute Delete
44.5 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Slyfox · Cards</title>
<style>
:root {
--bg: #0d1117; --panel: #161b22; --panel-2: #1c2128; --border: #30363d;
--text: #e6edf3; --muted: #8b949e; --accent: #2f81f7;
--ok: #3fb950; --warn: #d29922; --err: #f85149;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
/* Rarity tier accents — tuned for dark bg, used on borders + glows */
--r-common: #6e7681;
--r-rare: #58a6ff;
--r-epic: #bc8cff;
--r-legendary: #f0b429;
--r-mythic: #ff5c8a;
}
* { box-sizing: border-box; }
html, body { background: var(--bg); color: var(--text); margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
header { display: flex; align-items: center; justify-content: space-between; padding: 14px 24px; border-bottom: 1px solid var(--border); }
header h1 { font-size: 16px; margin: 0; font-weight: 600; letter-spacing: 0.2px; }
header .nav a { color: var(--muted); margin-left: 16px; font-size: 14px; }
header .nav a.active { color: var(--text); }
main { max-width: 1180px; margin: 0 auto; padding: 24px; }
body.embed header { display: none; }
body.embed main { max-width: none; padding: 16px; }
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 18px 20px; margin-bottom: 18px; }
.panel h2 { margin: 0 0 10px; font-size: 14px; font-weight: 600; }
.panel p { color: var(--muted); font-size: 13px; line-height: 1.6; margin: 0 0 8px; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; background: var(--panel-2); color: var(--muted); border: 1px solid var(--border); margin-right: 6px; }
input[type=text], select {
background: var(--panel-2); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 8px 10px; font-size: 13px; font-family: inherit;
}
input:focus, select:focus { outline: none; border-color: var(--accent); }
button {
background: var(--accent); color: white; border: 0; border-radius: 6px;
padding: 7px 12px; font-size: 12px; cursor: pointer; font-weight: 500;
}
button:hover { filter: brightness(1.1); }
button.secondary { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); }
/* Filter strip */
.filters { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.filters input[type=text] { flex: 1; min-width: 220px; }
.filters .count { color: var(--muted); font-size: 12px; font-family: var(--mono); margin-left: auto; }
/* Card grid */
.card-grid {
display: grid; gap: 18px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.card-empty { padding: 40px; text-align: center; color: var(--muted); }
/* Trading card */
.card {
width: 100%; aspect-ratio: 320 / 450; min-height: 420px;
background: linear-gradient(180deg, #1a2030 0%, #0e1320 100%);
border: 1px solid var(--border); border-radius: 14px;
position: relative; overflow: hidden; cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease;
display: flex; flex-direction: column;
}
.card:hover { transform: translateY(-3px); box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
.card.r-common { border-color: var(--r-common); }
.card.r-rare { border-color: var(--r-rare); box-shadow: 0 0 0 1px rgba(88,166,255,0.15) inset; }
.card.r-epic { border-color: var(--r-epic); box-shadow: 0 0 0 1px rgba(188,140,255,0.20) inset; }
.card.r-legendary { border-color: var(--r-legendary); box-shadow: 0 0 0 1px rgba(240,180,41,0.25) inset, 0 0 24px rgba(240,180,41,0.08); }
.card.r-mythic { border-color: var(--r-mythic); box-shadow: 0 0 0 1px rgba(255,92,138,0.30) inset, 0 0 32px rgba(255,92,138,0.12); }
/* Holographic shimmer — subtle baseline, dialed up for high tiers. */
.card::before {
content: ""; position: absolute; inset: 0; pointer-events: none;
background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,0.04) 45%, rgba(255,255,255,0.10) 50%, rgba(255,255,255,0.04) 55%, transparent 70%);
background-size: 200% 200%; opacity: 0.4; animation: shimmer 6s linear infinite;
}
.card.r-mythic::before, .card.r-legendary::before { opacity: 0.7; animation-duration: 4s; }
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
.card-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 12px 14px 8px; position: relative; z-index: 2; }
.card-header .name { font-size: 14px; font-weight: 600; line-height: 1.2; }
.card-header .handle { font-size: 11px; color: var(--muted); font-family: var(--mono); margin-top: 2px; }
.rarity-tag { font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.6px; background: var(--panel-2); border: 1px solid var(--border); }
.rarity-tag.r-common { color: var(--r-common); }
.rarity-tag.r-rare { color: var(--r-rare); border-color: var(--r-rare); }
.rarity-tag.r-epic { color: var(--r-epic); border-color: var(--r-epic); }
.rarity-tag.r-legendary { color: var(--r-legendary); border-color: var(--r-legendary); }
.rarity-tag.r-mythic { color: var(--r-mythic); border-color: var(--r-mythic); }
.orb-stage { position: relative; height: 150px; display: flex; align-items: center; justify-content: center; z-index: 2; }
.orb-stage svg { overflow: visible; }
.radar-stage { position: relative; height: 130px; margin: 0 14px; display: flex; align-items: center; justify-content: center; z-index: 2; }
.radar-stage svg { overflow: visible; }
.model-badge { margin: 4px 14px 0; padding: 4px 8px; border-radius: 4px; font-family: var(--mono); font-size: 10px; color: var(--muted); background: rgba(0,0,0,0.3); border: 1px solid var(--border); text-align: center; z-index: 2; position: relative; }
.moves { padding: 8px 14px 6px; z-index: 2; position: relative; }
.move { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--text); padding: 3px 0; border-bottom: 1px dashed rgba(255,255,255,0.05); }
.move:last-child { border-bottom: 0; }
.move .label { font-weight: 500; }
.move .pwr { font-family: var(--mono); color: var(--muted); font-size: 10px; }
.card-footer { margin-top: auto; display: flex; justify-content: space-between; align-items: center; padding: 8px 14px 12px; z-index: 2; position: relative; border-top: 1px solid rgba(255,255,255,0.04); }
.card-footer .traces { font-size: 10px; color: var(--muted); font-family: var(--mono); }
.trade-btn { padding: 4px 10px; font-size: 11px; }
/* Pokemon-ish card reveal modal */
.modal-backdrop {
position: fixed; inset: 0; z-index: 100; padding: 22px;
display: grid; place-items: center; opacity: 0; pointer-events: none;
overflow: hidden;
background:
radial-gradient(circle at 50% 34%, rgba(255,255,255,0.16), transparent 22%),
radial-gradient(circle at 18% 18%, rgba(88,166,255,0.30), transparent 28%),
radial-gradient(circle at 84% 20%, rgba(255,92,138,0.24), transparent 30%),
linear-gradient(180deg, rgba(3,6,14,0.92), rgba(4,8,20,0.96));
backdrop-filter: blur(18px) saturate(1.35);
transition: opacity 0.22s ease;
}
.modal-backdrop.open { opacity: 1; pointer-events: auto; }
.modal-backdrop::before,
.modal-backdrop::after {
content: ""; position: absolute; inset: -20%; pointer-events: none;
background-image:
radial-gradient(circle, rgba(255,255,255,0.72) 0 1px, transparent 1.6px),
radial-gradient(circle, rgba(255,214,102,0.60) 0 1px, transparent 1.7px);
background-size: 78px 78px, 112px 112px;
animation: starDrift 10s linear infinite;
opacity: 0.34;
}
.modal-backdrop::after { animation-duration: 16s; animation-direction: reverse; filter: blur(1px); opacity: 0.22; }
.modal {
width: min(1040px, 100%); max-height: 92vh; overflow: auto; border: 0;
position: relative; background: transparent; border-radius: 28px; padding: 0;
color: var(--text); transform: translateY(18px) scale(0.96);
}
.modal-backdrop.open .modal { animation: modalDrop 0.52s cubic-bezier(.19,1,.22,1) forwards; }
.modal-shell {
position: relative; z-index: 1; isolation: isolate;
display: grid; grid-template-columns: minmax(280px, 390px) minmax(320px, 1fr);
gap: 24px; align-items: stretch; padding: 22px;
border: 1px solid rgba(255,255,255,0.18); border-radius: 28px;
background:
linear-gradient(135deg, rgba(255,255,255,0.12), rgba(255,255,255,0.02)),
rgba(8, 12, 26, 0.82);
box-shadow: 0 30px 90px rgba(0,0,0,0.65), inset 0 0 0 1px rgba(255,255,255,0.06);
}
.modal-shell::before {
content: ""; position: absolute; inset: -2px; z-index: -1; border-radius: 30px;
background: conic-gradient(from 0deg, var(--foil-a), var(--foil-b), var(--foil-c), var(--foil-a));
filter: blur(22px); opacity: 0.44; animation: ringSpin 7s linear infinite;
}
.modal-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 12px; }
.modal-head h2 { margin: 0; font-size: clamp(22px, 4vw, 34px); line-height: 1; letter-spacing: -0.03em; }
.modal-head .close {
width: 38px; height: 38px; display: grid; place-items: center;
background: rgba(255,255,255,0.08); color: var(--text);
border: 1px solid rgba(255,255,255,0.14); border-radius: 999px;
font-size: 24px; cursor: pointer; padding: 0;
}
.modal-head .close:hover { filter: brightness(1.2); }
.modal-meta {
color: var(--muted); font-size: 12px; font-family: var(--mono);
margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px;
}
.modal-pill {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 9px; border-radius: 999px;
background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.12);
}
.modal-card-stage {
position: relative; display: grid; place-items: center; min-height: 520px;
perspective: 1400px;
}
.modal-card-stage::before {
content: ""; position: absolute; width: 72%; aspect-ratio: 1; border-radius: 50%;
background: radial-gradient(circle, var(--foil-a), transparent 62%);
filter: blur(28px); opacity: 0.26; animation: pulseAura 2.7s ease-in-out infinite;
}
.showcase-card {
width: min(360px, 92vw); aspect-ratio: 320 / 450; position: relative;
border-radius: 24px; overflow: hidden; padding: 14px;
transform-style: preserve-3d;
background:
linear-gradient(135deg, rgba(255,255,255,0.34), rgba(255,255,255,0.04) 34%, rgba(0,0,0,0.42) 78%),
linear-gradient(145deg, var(--foil-a), #101827 28%, var(--foil-b) 58%, var(--foil-c));
box-shadow:
0 22px 55px rgba(0,0,0,0.58),
0 0 0 2px rgba(255,255,255,0.32) inset,
0 0 0 7px rgba(0,0,0,0.50) inset,
0 0 58px var(--rarity-color);
animation: cardSummon 0.8s cubic-bezier(.19,1,.22,1) both, cardFloat 4s ease-in-out 0.8s infinite;
}
.showcase-card::before {
content: ""; position: absolute; inset: 0; pointer-events: none; mix-blend-mode: screen;
background:
repeating-linear-gradient(115deg, rgba(255,255,255,0) 0 10px, rgba(255,255,255,0.18) 11px 12px, rgba(255,255,255,0) 13px 24px),
linear-gradient(110deg, transparent 10%, rgba(255,255,255,0.82) 44%, transparent 55%);
background-size: 220% 220%, 240% 240%;
opacity: 0.58; animation: holoSweep 2.2s ease-in-out infinite;
}
.showcase-card::after {
content: ""; position: absolute; inset: 9px; border-radius: 18px; pointer-events: none;
border: 1px solid rgba(255,255,255,0.34);
background:
radial-gradient(circle at 28% 20%, rgba(255,255,255,0.45), transparent 18%),
linear-gradient(135deg, transparent 25%, rgba(255,255,255,0.12), transparent 58%);
mix-blend-mode: screen; opacity: 0.66;
}
.showcase-inner {
position: relative; z-index: 2; height: 100%; display: flex; flex-direction: column;
border-radius: 17px; overflow: hidden; padding: 12px;
background:
radial-gradient(circle at 50% 28%, rgba(255,255,255,0.16), transparent 34%),
linear-gradient(180deg, rgba(10,16,31,0.86), rgba(5,8,18,0.96));
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.14);
}
.showcase-topline { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
.showcase-name { font-size: 22px; font-weight: 800; letter-spacing: -0.03em; text-shadow: 0 2px 16px rgba(0,0,0,0.55); }
.showcase-hp { font-family: var(--mono); font-size: 11px; color: #fff0a6; text-align: right; text-transform: uppercase; }
.showcase-art {
position: relative; display: grid; place-items: center; height: 184px; margin: 12px 0;
border-radius: 16px; border: 1px solid rgba(255,255,255,0.18); overflow: hidden;
background:
radial-gradient(circle at 50% 48%, var(--foil-a), transparent 34%),
conic-gradient(from 90deg, rgba(255,255,255,0.08), rgba(255,255,255,0), rgba(255,255,255,0.12), rgba(255,255,255,0));
}
.showcase-art::before {
content: ""; position: absolute; inset: -35%;
background: conic-gradient(from 0deg, transparent, rgba(255,255,255,0.16), transparent, rgba(255,255,255,0.10), transparent);
animation: ringSpin 5s linear infinite;
}
.showcase-art > * { position: relative; z-index: 1; transform: scale(1.12); }
.showcase-radar {
display: grid; place-items: center; min-height: 112px; margin: 0 0 10px;
border-radius: 14px; background: rgba(0,0,0,0.24);
border: 1px solid rgba(255,255,255,0.08);
}
.showcase-moves { display: grid; gap: 6px; margin-top: auto; }
.showcase-move {
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 10px;
padding: 7px 9px; border-radius: 10px;
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.09);
font-size: 12px;
}
.showcase-move b { font-weight: 700; }
.showcase-move span { font-family: var(--mono); color: #fff0a6; font-size: 11px; }
.sparkles { position: absolute; inset: 0; pointer-events: none; overflow: hidden; border-radius: inherit; }
.sparkles i {
position: absolute; width: 5px; height: 5px; border-radius: 50%; background: #fff;
box-shadow: 0 0 12px #fff, 0 0 22px var(--foil-b);
animation: sparkleFloat 2.8s ease-in-out infinite;
}
.sparkles i:nth-child(1) { left: 14%; top: 24%; animation-delay: .1s; }
.sparkles i:nth-child(2) { left: 76%; top: 16%; animation-delay: .45s; }
.sparkles i:nth-child(3) { left: 84%; top: 52%; animation-delay: .82s; }
.sparkles i:nth-child(4) { left: 24%; top: 72%; animation-delay: 1.1s; }
.sparkles i:nth-child(5) { left: 55%; top: 84%; animation-delay: 1.45s; }
.confetti-field {
position: absolute; inset: -18px; z-index: 4; pointer-events: none;
overflow: hidden; border-radius: 30px;
animation: confettiFieldVanish 4.2s ease forwards;
}
.confetti-piece {
--x: 50%; --drift: 0px; --delay: 0s; --dur: 3s; --spin: 1turn; --size: 18px;
position: absolute; left: var(--x); top: -42px;
display: grid; place-items: center; min-width: calc(var(--size) + 12px);
height: calc(var(--size) + 12px); padding: 0 5px;
border-radius: 999px; font-size: var(--size); line-height: 1;
background: rgba(255,255,255,0.10); border: 1px solid rgba(255,255,255,0.16);
box-shadow: 0 0 18px rgba(255,255,255,0.16), 0 0 28px var(--foil-b);
backdrop-filter: blur(4px);
animation: confettiFall var(--dur) cubic-bezier(.16,.8,.35,1) var(--delay) both;
}
.confetti-piece.logo {
font-family: var(--mono); font-weight: 800; letter-spacing: -0.06em;
color: #0d1117; background: #fff;
}
.confetti-piece.hf {
color: #111; background: linear-gradient(135deg, #ffcc4d, #ff9f1c);
text-shadow: 0 1px 0 rgba(255,255,255,0.45);
}
.confetti-piece.jp {
color: #fff; background: linear-gradient(135deg, rgba(255,92,138,0.82), rgba(188,140,255,0.62));
}
.confetti-piece:nth-child(3n) { filter: hue-rotate(32deg); }
.confetti-piece:nth-child(4n) { border-radius: 8px; }
@keyframes confettiFall {
0% { transform: translate3d(0, -56px, 0) rotate(0deg); opacity: 1; }
78% { opacity: 1; }
100% { transform: translate3d(var(--drift), 86vh, 0) rotate(var(--spin)); opacity: 0; }
}
@keyframes confettiFieldVanish {
0%, 72% { opacity: 1; }
100% { opacity: 0; visibility: hidden; }
}
.modal-info { display: flex; flex-direction: column; min-width: 0; }
.modal-copy {
margin-top: auto; padding: 14px; border-radius: 16px;
background: rgba(0,0,0,0.24); border: 1px solid rgba(255,255,255,0.10);
}
.modal-copy p { margin: 0 0 10px; font-size: 12px; color: var(--muted); line-height: 1.5; }
.stats-table { width: 100%; font-size: 12px; border-collapse: collapse; margin: 10px 0 16px; }
.stats-table td { padding: 8px 4px; border-bottom: 1px dashed rgba(255,255,255,0.08); }
.stats-table td:first-child { color: var(--muted); }
.stats-table td:last-child { text-align: right; font-family: var(--mono); }
.share-row { display: flex; gap: 8px; }
.share-row input { flex: 1; min-width: 0; font-family: var(--mono); font-size: 11px; }
.modal-flavor {
color: var(--muted); font-size: 13px; line-height: 1.7; margin: 4px 0 14px;
}
.modal-big-stat {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 12px 0 10px;
}
.modal-big-stat > div {
padding: 11px 10px; border-radius: 14px; background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.09);
}
.modal-big-stat .k { color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; }
.modal-big-stat .v { margin-top: 4px; font-size: 18px; font-family: var(--mono); color: var(--text); }
@media (max-width: 820px) {
.modal { max-height: 94vh; }
.modal-shell { grid-template-columns: 1fr; padding: 16px; }
.modal-card-stage { min-height: auto; }
.showcase-card { width: min(340px, 86vw); }
.modal-big-stat { grid-template-columns: 1fr; }
}
@keyframes modalDrop {
from { opacity: 0; transform: translateY(18px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes cardSummon {
0% { opacity: 0; transform: rotateX(72deg) rotateZ(-10deg) scale(0.58); filter: brightness(2.2) blur(8px); }
58% { opacity: 1; transform: rotateX(-8deg) rotateZ(2deg) scale(1.05); filter: brightness(1.35) blur(0); }
100% { transform: rotateX(0) rotateZ(0) scale(1); filter: brightness(1); }
}
@keyframes cardFloat {
0%, 100% { transform: translateY(0) rotateX(0deg) rotateY(-2deg); }
50% { transform: translateY(-9px) rotateX(2deg) rotateY(3deg); }
}
@keyframes holoSweep {
0%, 100% { background-position: 120% 0, 120% 0; opacity: 0.40; }
48% { background-position: -35% 100%, -40% 100%; opacity: 0.86; }
}
@keyframes ringSpin { to { transform: rotate(360deg); } }
@keyframes pulseAura { 0%, 100% { transform: scale(0.92); opacity: 0.20; } 50% { transform: scale(1.08); opacity: 0.38; } }
@keyframes sparkleFloat {
0%, 100% { transform: translateY(0) scale(0.45); opacity: 0; }
35% { opacity: 1; }
60% { transform: translateY(-22px) scale(1); opacity: 0.9; }
}
@keyframes starDrift { to { transform: translate3d(80px, 80px, 0); } }
@media (prefers-reduced-motion: reduce) {
.modal-backdrop::before, .modal-backdrop::after, .modal-shell::before,
.modal-card-stage::before, .showcase-card, .showcase-card::before,
.showcase-art::before, .sparkles i, .confetti-field, .confetti-piece { animation: none !important; }
}
.legend { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.legend .l-tag { font-size: 10px; padding: 2px 7px; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--border); color: var(--muted); }
</style>
</head>
<body>
<script>if (new URLSearchParams(location.search).has("embed")) document.body.classList.add("embed");</script>
<main>
<section class="panel">
<h2>Maintainer Trading Cards <span class="badge">v1</span></h2>
<p>
Every published maintainer becomes a collectible card. Their orb breathes
at the average <em>(valence, arousal)</em> coordinates of their analyzed
traces, the radar plots their 20-dimension emotion fingerprint against the
global baseline, and their signature moves are surfaced from their top
affect sequences. Collect them. Share them. Trade them.
</p>
<div class="legend">
<span class="l-tag" style="color: var(--r-common)">● Common</span>
<span class="l-tag" style="color: var(--r-rare)">● Rare</span>
<span class="l-tag" style="color: var(--r-epic)">● Epic</span>
<span class="l-tag" style="color: var(--r-legendary)">● Legendary</span>
<span class="l-tag" style="color: var(--r-mythic)">● Mythic</span>
</div>
</section>
<section class="panel">
<div class="filters">
<input id="f-search" type="text" placeholder="Search name, handle, org…" />
<select id="f-org">
<option value="">All orgs</option>
</select>
<select id="f-rarity">
<option value="">All rarities</option>
<option value="common">Common</option>
<option value="rare">Rare</option>
<option value="epic">Epic</option>
<option value="legendary">Legendary</option>
<option value="mythic">Mythic</option>
</select>
<select id="f-sort">
<option value="rarity">Sort: rarity</option>
<option value="name">Sort: name</option>
<option value="traces">Sort: trace count</option>
<option value="arousal">Sort: arousal</option>
</select>
<span class="count" id="count"></span>
</div>
</section>
<section>
<div class="card-grid" id="grid"></div>
</section>
</main>
<div class="modal-backdrop" id="modal">
<div class="modal" id="modal-content"></div>
</div>
<script>
"use strict";
// Emotion vocabulary — 20 Russell-circumplex directions. Each carries its own
// (valence, arousal) anchor so we can derive an aggregate vibe per maintainer.
const EMOTIONS = [
{ key: "joyful", v: 0.85, a: 0.65, color: "#f9d56e" },
{ key: "amused", v: 0.75, a: 0.45, color: "#ffd166" },
{ key: "hopeful", v: 0.70, a: 0.30, color: "#a0e8af" },
{ key: "curious", v: 0.55, a: 0.50, color: "#7ed6df" },
{ key: "surprised", v: 0.20, a: 0.80, color: "#dff9fb" },
{ key: "focused", v: 0.45, a: 0.25, color: "#74b9ff" },
{ key: "calm", v: 0.60, a: -0.45, color: "#81ecec" },
{ key: "content", v: 0.65, a: -0.20, color: "#b8e994" },
{ key: "tender", v: 0.55, a: -0.55, color: "#fab1a0" },
{ key: "nostalgic", v: 0.20, a: -0.40, color: "#cdb4db" },
{ key: "melancholic", v: -0.40, a: -0.55, color: "#778da9" },
{ key: "sad", v: -0.65, a: -0.35, color: "#4a6fa5" },
{ key: "tired", v: -0.30, a: -0.70, color: "#6c757d" },
{ key: "bored", v: -0.25, a: -0.65, color: "#8d99ae" },
{ key: "anxious", v: -0.55, a: 0.65, color: "#9d4edd" },
{ key: "fear", v: -0.75, a: 0.55, color: "#7209b7" },
{ key: "frustrated", v: -0.60, a: 0.50, color: "#e76f51" },
{ key: "anger", v: -0.80, a: 0.75, color: "#e63946" },
{ key: "disgust", v: -0.70, a: 0.10, color: "#7d8a2e" },
{ key: "contempt", v: -0.55, a: 0.20, color: "#bc6c25" },
];
const EMOTION_KEYS = EMOTIONS.map(e => e.key);
// Global baseline = uniform 0.5, drawn faintly behind each radar.
const BASELINE = EMOTIONS.map(() => 0.5);
// MOCK_MAINTAINERS — replace via /api/community/runs (see loadData adapter).
// Each: identity + profile (20-length [0,1]) + traces (drives rarity).
const MOCK_MAINTAINERS = [
{
id: "arthurz",
name: "Arthur Zucker",
handle: "@ArthurZ",
org: "transformers",
model: "Qwen/Qwen2.5-7B-Instruct",
traces: 142,
profile: mk({ focused: 0.92, curious: 0.78, frustrated: 0.66, anxious: 0.58, joyful: 0.45, amused: 0.42 }),
},
{
id: "lysandre",
name: "Lysandre Debut",
handle: "@LysandreJik",
org: "transformers",
model: "google/gemma-2-9b-it",
traces: 98,
profile: mk({ calm: 0.88, content: 0.74, focused: 0.70, hopeful: 0.58, curious: 0.52, tender: 0.40 }),
},
{
id: "celina",
name: "Celina Hanouti",
handle: "@hanouticelina",
org: "hf-hub",
model: "meta-llama/Llama-3.1-8B-Instruct",
traces: 211,
profile: mk({ curious: 0.95, joyful: 0.72, hopeful: 0.68, surprised: 0.60, amused: 0.55, focused: 0.50 }),
},
{
id: "alvaro",
name: "Alvaro Bartolome",
handle: "@AlvaroBartt",
org: "hf-mem",
model: "mistralai/Mistral-7B-Instruct-v0.3",
traces: 64,
profile: mk({ focused: 0.80, frustrated: 0.62, anxious: 0.55, content: 0.40, anger: 0.38, melancholic: 0.30 }),
},
{
id: "pcuenca",
name: "Pedro Cuenca",
handle: "@pcuenq",
org: "diffusers",
model: "Qwen/Qwen2.5-14B-Instruct",
traces: 173,
profile: mk({ amused: 0.90, joyful: 0.82, hopeful: 0.75, calm: 0.65, content: 0.60, tender: 0.48 }),
},
{
id: "thom",
name: "Thom Wolf",
handle: "@Thom_Wolf",
org: "transformers",
model: "anthropic/claude-3-5-sonnet",
traces: 305,
profile: mk({ hopeful: 0.96, curious: 0.85, joyful: 0.78, focused: 0.70, calm: 0.55, surprised: 0.48 }),
},
{
id: "merve",
name: "Merve Noyan",
handle: "@mervenoyan",
org: "transformers",
model: "Qwen/Qwen2.5-7B-Instruct",
traces: 89,
profile: mk({ amused: 0.85, joyful: 0.75, curious: 0.72, surprised: 0.55, hopeful: 0.50, focused: 0.42 }),
},
{
id: "vaibhavs10",
name: "Vaibhav Srivastav",
handle: "@reach_vb",
org: "audio",
model: "openai/gpt-4o-mini",
traces: 54,
profile: mk({ joyful: 0.78, surprised: 0.66, amused: 0.62, curious: 0.55, frustrated: 0.30, focused: 0.45 }),
},
];
// Sparse mock helper. Unspecified dims get low-amplitude noise (0.05–0.20).
function mk(weights) {
return EMOTION_KEYS.map(k => weights[k] ?? (0.05 + Math.random() * 0.15));
}
// ---- Derived stats ----
function dominantEmotion(profile) {
let best = 0;
for (let i = 1; i < profile.length; i++) if (profile[i] > profile[best]) best = i;
return EMOTIONS[best];
}
function topN(profile, n) {
return profile
.map((p, i) => ({ p, i }))
.sort((a, b) => b.p - a.p)
.slice(0, n)
.map(({ i }) => EMOTIONS[i]);
}
// Weighted-mean (valence, arousal) over the profile.
function aggregateVA(profile) {
let v = 0, a = 0, w = 0;
for (let i = 0; i < profile.length; i++) {
v += EMOTIONS[i].v * profile[i];
a += EMOTIONS[i].a * profile[i];
w += profile[i];
}
return w === 0 ? { v: 0, a: 0 } : { v: v / w, a: a / w };
}
// Normalized Shannon entropy → 0..1; pairs with trace count for rarity.
function diversity(profile) {
const sum = profile.reduce((s, x) => s + x, 0) || 1;
let h = 0;
for (const p of profile) {
const q = p / sum;
if (q > 0) h -= q * Math.log(q);
}
return h / Math.log(profile.length); // 0..1
}
function rarityFor(m) {
// Volume × diversity. 1000 traces all in one emotion ≠ legendary.
const t = Math.min(1, m.traces / 300);
const d = diversity(m.profile);
const score = 0.55 * t + 0.45 * d;
if (score >= 0.78) return "mythic";
if (score >= 0.68) return "legendary";
if (score >= 0.55) return "epic";
if (score >= 0.40) return "rare";
return "common";
}
// Signature moves — flavor strings keyed off the top-3 emotions. Pick is
// deterministic per maintainer so refreshes don't reshuffle the card.
const MOVE_TEMPLATES = {
focused: ["Tunnel Vision", "Deep Refactor", "The Quiet Push"],
curious: ["What If We…", "The Detour", "Sidequest Reflex"],
frustrated: ["The 4 AM Recurse", "Compiler Argument", "Backspace Storm"],
anxious: ["Pre-Merge Spiral", "Watcher's Eye", "The Triple-Check"],
joyful: ["Green CI Grin", "First-Try Magic", "Confetti Commit"],
amused: ["LGTM Wink", "The Side-Eye Comment", "Soft Roast"],
hopeful: ["Next Sprint Energy", "Believer's Patch", "Sunrise Branch"],
calm: ["Pacific Calm", "Steady Hand", "Untouched Cadence"],
content: ["Reviewer's Peace", "Tea-Break Diff", "Slow Roast"],
surprised: ["The Stack Trace Gasp", "Wait, What?", "Edge Case Echo"],
melancholic: ["Legacy Lament", "Yesterday's Branch", "The Old Tag"],
sad: ["Revert in Sorrow", "Deprecation Blues", "Lonely Test"],
tired: ["Friday Patch", "End-of-Sprint Drag", "Coffee IV"],
bored: ["Routine Drift", "The Yawn Lint", "Autopilot Diff"],
fear: ["Production Dread", "The Big Red Button", "Rollback Reflex"],
anger: ["RAGE QUIT", "Force Push Fury", "The Strong Opinion"],
contempt: ["Closed Without Comment", "The Drive-By", "Eyebrow Raise"],
disgust: ["Code Smell", "The N+1 Glare", "Refactor or Die"],
tender: ["Mentor Mode", "The Gentle Nit", "Pair-Programming Hug"],
nostalgic: ["v0.1 Vibes", "The Old README", "Founders' Branch"],
};
function signatureMoves(profile) {
const top = topN(profile, 3);
const seed = top.reduce((s, e) => s + e.key.charCodeAt(0), 0);
return top.map((e, idx) => {
const pool = MOVE_TEMPLATES[e.key] || [e.key];
const pick = pool[(seed + idx * 7) % pool.length];
const pwr = Math.round(40 + profile[EMOTION_KEYS.indexOf(e.key)] * 60);
return { label: pick, color: e.color, power: pwr };
});
}
// Breathing orb. Color = dominant emotion. Period = f(arousal). R drifts
// slightly with valence so happier orbs sit a hair bigger.
function orbSVG(m) {
const dom = dominantEmotion(m.profile);
const { v, a } = aggregateVA(m.profile);
const period = (2.4 - (a + 1) * 0.75).toFixed(2); // arousal → 2.4s..0.9s
const baseR = 26 + v * 4;
const id = "orb-" + m.id;
return `
<svg width="140" height="140" viewBox="-70 -70 140 140" aria-hidden="true">
<defs>
<radialGradient id="${id}-g">
<stop offset="0%" stop-color="${dom.color}" stop-opacity="0.95"/>
<stop offset="55%" stop-color="${dom.color}" stop-opacity="0.45"/>
<stop offset="100%" stop-color="${dom.color}" stop-opacity="0"/>
</radialGradient>
<filter id="${id}-blur"><feGaussianBlur stdDeviation="1.4"/></filter>
</defs>
<circle r="${baseR + 18}" fill="url(#${id}-g)" opacity="0.35">
<animate attributeName="r" values="${baseR + 12};${baseR + 22};${baseR + 12}" dur="${(period * 1.4).toFixed(2)}s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.20;0.45;0.20" dur="${(period * 1.4).toFixed(2)}s" repeatCount="indefinite"/>
</circle>
<circle r="${baseR}" fill="url(#${id}-g)" filter="url(#${id}-blur)">
<animate attributeName="r" values="${baseR};${baseR + 6};${baseR}" dur="${period}s" repeatCount="indefinite"/>
</circle>
<circle r="${baseR * 0.35}" fill="${dom.color}" opacity="0.9">
<animate attributeName="opacity" values="0.7;1;0.7" dur="${period}s" repeatCount="indefinite"/>
</circle>
</svg>
`;
}
// 20-spoke radar. Maintainer polygon on top, dashed baseline behind it.
function radarSVG(profile, size, opts = {}) {
const cx = size / 2, cy = size / 2;
const R = size * 0.40;
const n = profile.length;
const showLabels = !!opts.labels;
const pts = (vals) => vals.map((v, i) => {
const ang = (i / n) * Math.PI * 2 - Math.PI / 2;
const r = R * Math.max(0.02, Math.min(1, v));
return [cx + Math.cos(ang) * r, cy + Math.sin(ang) * r];
});
const poly = (vals) => pts(vals).map(p => p.join(",")).join(" ");
const dom = dominantEmotion(profile);
const rings = [0.25, 0.5, 0.75, 1.0].map(f =>
`<circle cx="${cx}" cy="${cy}" r="${R * f}" fill="none" stroke="#2a313c" stroke-width="0.6"/>`
).join("");
const spokes = Array.from({ length: n }, (_, i) => {
const ang = (i / n) * Math.PI * 2 - Math.PI / 2;
return `<line x1="${cx}" y1="${cy}" x2="${cx + Math.cos(ang) * R}" y2="${cy + Math.sin(ang) * R}" stroke="#262c36" stroke-width="0.5"/>`;
}).join("");
const labels = showLabels ? Array.from({ length: n }, (_, i) => {
const ang = (i / n) * Math.PI * 2 - Math.PI / 2;
const lr = R + 14;
const x = cx + Math.cos(ang) * lr;
const y = cy + Math.sin(ang) * lr;
return `<text x="${x}" y="${y}" font-size="8" fill="#8b949e" text-anchor="middle" dominant-baseline="middle">${EMOTIONS[i].key}</text>`;
}).join("") : "";
return `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" aria-hidden="true">
${rings}
${spokes}
<polygon points="${poly(BASELINE)}" fill="rgba(139,148,158,0.10)" stroke="rgba(139,148,158,0.45)" stroke-width="0.8" stroke-dasharray="2 2"/>
<polygon points="${poly(profile)}" fill="${dom.color}" fill-opacity="0.25" stroke="${dom.color}" stroke-width="1.4"/>
${pts(profile).map(([x, y]) => `<circle cx="${x}" cy="${y}" r="1.6" fill="${dom.color}"/>`).join("")}
${labels}
</svg>
`;
}
// ---- Card rendering ----
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
function renderCard(m) {
const rarity = rarityFor(m);
const moves = signatureMoves(m.profile);
return `
<div class="card r-${rarity}" data-id="${m.id}">
<div class="card-header">
<div>
<div class="name">${escapeHtml(m.name)}</div>
<div class="handle">${escapeHtml(m.handle)} · ${escapeHtml(m.org)}</div>
</div>
<div class="rarity-tag r-${rarity}">${rarity}</div>
</div>
<div class="orb-stage">${orbSVG(m)}</div>
<div class="radar-stage">${radarSVG(m.profile, 120)}</div>
<div class="model-badge">⚙ ${escapeHtml(m.model)}</div>
<div class="moves">
${moves.map(mv => `
<div class="move">
<span class="label" style="color:${mv.color}">▸ ${escapeHtml(mv.label)}</span>
<span class="pwr">PWR ${mv.power}</span>
</div>
`).join("")}
</div>
<div class="card-footer">
<span class="traces">${m.traces} traces</span>
<button class="trade-btn secondary" data-act="trade" data-id="${m.id}">Trade ⇄</button>
</div>
</div>
`;
}
// ---- Filtering + sorting + grid ----
const RARITY_ORDER = { mythic: 5, legendary: 4, epic: 3, rare: 2, common: 1 };
let ALL = [];
function applyFilters() {
const q = document.getElementById("f-search").value.trim().toLowerCase();
const org = document.getElementById("f-org").value;
const rar = document.getElementById("f-rarity").value;
const sort = document.getElementById("f-sort").value;
let rows = ALL.filter(m => {
if (q) {
const blob = (m.name + " " + m.handle + " " + m.org).toLowerCase();
if (!blob.includes(q)) return false;
}
if (org && m.org !== org) return false;
if (rar && rarityFor(m) !== rar) return false;
return true;
});
rows.sort((a, b) => {
if (sort === "name") return a.name.localeCompare(b.name);
if (sort === "traces") return b.traces - a.traces;
if (sort === "arousal") return aggregateVA(b.profile).a - aggregateVA(a.profile).a;
const dr = RARITY_ORDER[rarityFor(b)] - RARITY_ORDER[rarityFor(a)];
return dr !== 0 ? dr : b.traces - a.traces;
});
const grid = document.getElementById("grid");
grid.innerHTML = rows.length
? rows.map(renderCard).join("")
: `<div class="card-empty">No cards match those filters.</div>`;
document.getElementById("count").textContent =
`${rows.length} / ${ALL.length} cards`;
}
function populateOrgs() {
const orgs = [...new Set(ALL.map(m => m.org))].sort();
const sel = document.getElementById("f-org");
sel.innerHTML = `<option value="">All orgs</option>` +
orgs.map(o => `<option value="${escapeHtml(o)}">${escapeHtml(o)}</option>`).join("");
}
// ---- Modal ----
function hashCode(s) {
let h = 0;
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
return h;
}
const CONFETTI_MARKS = [
{ text: "🌸", cls: "jp" },
{ text: "💮", cls: "jp" },
{ text: "🍥", cls: "jp" },
{ text: "⛩", cls: "jp" },
{ text: "🗾", cls: "jp" },
{ text: "✨", cls: "jp" },
{ text: "🤗", cls: "hf" },
{ text: "HF", cls: "hf logo" },
{ text: "GH", cls: "logo" },
{ text: "🐙", cls: "logo" },
];
function confettiHTML(seed) {
let s = Math.abs(hashCode(seed)) || 1;
const pieces = [];
for (let i = 0; i < 34; i++) {
s = (s * 1664525 + 1013904223) >>> 0;
const mark = CONFETTI_MARKS[s % CONFETTI_MARKS.length];
const x = 3 + ((s >>> 8) % 94);
const drift = ((s >>> 10) % 121) - 60;
const delay = ((s >>> 16) % 140) / 100;
const dur = 2.2 + ((s >>> 4) % 24) / 10;
const spin = ((s % 2) ? 1 : -1) * (0.7 + ((s >>> 12) % 20) / 10);
const size = 18 + ((s >>> 20) % 15);
pieces.push(
`<span class="confetti-piece ${mark.cls}" style="--x:${x}%; --drift:${drift}px; --delay:${delay}s; --dur:${dur.toFixed(1)}s; --spin:${spin.toFixed(1)}turn; --size:${size}px;">${mark.text}</span>`
);
}
return `<div class="confetti-field" aria-hidden="true">${pieces.join("")}</div>`;
}
function openModal(m) {
const rarity = rarityFor(m);
const { v, a } = aggregateVA(m.profile);
const dom = dominantEmotion(m.profile);
const moves = signatureMoves(m.profile);
const top = topN(m.profile, 5);
const shareURL = location.origin + "/?tab=cards#" + encodeURIComponent(m.id);
const cardNo = String(Math.abs(hashCode(m.id)) % 999).padStart(3, "0");
document.getElementById("modal-content").innerHTML = `
${confettiHTML(m.id)}
<div class="modal-shell" style="--foil-a:${top[0].color}; --foil-b:${top[1].color}; --foil-c:${top[2].color}; --rarity-color:var(--r-${rarity});">
<div class="modal-card-stage">
<div class="showcase-card r-${rarity}" aria-label="${escapeHtml(m.name)} trading card">
<div class="sparkles"><i></i><i></i><i></i><i></i><i></i></div>
<div class="showcase-inner">
<div class="showcase-topline">
<div>
<div class="showcase-name">${escapeHtml(m.name)}</div>
<div class="handle">${escapeHtml(m.handle)} · ${escapeHtml(m.org)}</div>
</div>
<div class="showcase-hp">${rarity}<br/>#${cardNo}</div>
</div>
<div class="showcase-art">${orbSVG(m)}</div>
<div class="showcase-radar">${radarSVG(m.profile, 158)}</div>
<div class="showcase-moves">
${moves.map(mv => `
<div class="showcase-move">
<b style="color:${mv.color}">${escapeHtml(mv.label)}</b>
<span>PWR ${mv.power}</span>
</div>
`).join("")}
</div>
</div>
</div>
</div>
<div class="modal-info">
<div class="modal-head">
<div>
<h2>${escapeHtml(m.name)}</h2>
<div class="modal-meta">
<span class="modal-pill">${escapeHtml(m.handle)}</span>
<span class="modal-pill">${escapeHtml(m.org)}</span>
<span class="modal-pill" style="color: var(--r-${rarity})">${rarity} foil</span>
</div>
</div>
<button class="close" data-act="close" aria-label="Close card">×</button>
</div>
<p class="modal-flavor">
${escapeHtml(m.name)} is a ${rarity} maintainer card with a
<b style="color:${dom.color}">${dom.key}</b> core. The orb is breathing at
the card's average valence/arousal and the radar is the full emotion
fingerprint against the baseline.
</p>
<div class="modal-big-stat">
<div><div class="k">Traces</div><div class="v">${m.traces}</div></div>
<div><div class="k">Valence</div><div class="v">${v.toFixed(2)}</div></div>
<div><div class="k">Arousal</div><div class="v">${a.toFixed(2)}</div></div>
</div>
<table class="stats-table">
<tr><td>best-fit model</td><td>${escapeHtml(m.model)}</td></tr>
<tr><td>dominant emotion</td><td style="color:${dom.color}">${dom.key}</td></tr>
<tr><td>emotion diversity</td><td>${diversity(m.profile).toFixed(2)}</td></tr>
<tr><td>top emotions</td><td>${top.map(e => e.key).join(" · ")}</td></tr>
<tr><td>rarity tier</td><td style="color: var(--r-${rarity})">${rarity}</td></tr>
</table>
<div class="modal-copy">
<p>Copy this and it opens directly inside the Cards tab with the reveal animation.</p>
<div class="share-row">
<input type="text" readonly id="share-url" value="${escapeHtml(shareURL)}" />
<button data-act="copy">Copy card</button>
</div>
</div>
</div>
</div>
`;
document.getElementById("modal").classList.add("open");
}
function closeModal() {
document.getElementById("modal").classList.remove("open");
}
// ---- Event wiring ----
document.getElementById("grid").addEventListener("click", (ev) => {
const tradeBtn = ev.target.closest("button[data-act=trade]");
if (tradeBtn) { ev.stopPropagation(); const m = ALL.find(x => x.id === tradeBtn.dataset.id); if (m) openModal(m); return; }
const card = ev.target.closest(".card");
if (card) { const m = ALL.find(x => x.id === card.dataset.id); if (m) openModal(m); }
});
document.getElementById("modal").addEventListener("click", async (ev) => {
if (ev.target.id === "modal") { closeModal(); return; }
if (ev.target.closest("[data-act=close]")) { closeModal(); return; }
if (ev.target.closest("[data-act=copy]")) {
const btn = ev.target.closest("[data-act=copy]");
const inp = document.getElementById("share-url");
inp.select();
try { await navigator.clipboard.writeText(inp.value); btn.textContent = "Copied"; setTimeout(() => btn.textContent = "Copy card", 1200); }
catch { /* fallback select is enough */ }
}
});
document.addEventListener("keydown", (ev) => { if (ev.key === "Escape") closeModal(); });
["f-search", "f-org", "f-rarity", "f-sort"].forEach(id => {
document.getElementById(id).addEventListener("input", applyFilters);
document.getElementById(id).addEventListener("change", applyFilters);
});
// ---- Data loading: try real endpoint, fall back to mocks ----
async function loadData() {
try {
const res = await fetch("/api/community/runs");
if (res.ok) {
const data = await res.json();
const runs = data.runs || [];
if (runs.length) {
// ADAPTER — expand when the run schema is finalized.
// Expected: owner, name, org, model_id, n_traces, profile (20-array).
ALL = runs.map((r, idx) => ({
id: r.run_id || r.owner || ("run-" + idx),
name: r.name || r.owner || "Unknown",
handle: r.owner ? "@" + r.owner.replace(/^@/, "") : "@unknown",
org: r.org || r.source || "unknown",
model: r.model_id || "—",
traces: r.n_traces || r.traces || 1,
// 20-length [0,1] required; synth from owner-string if missing.
profile: Array.isArray(r.profile) && r.profile.length === 20
? r.profile
: synthProfile(r.owner || r.run_id || String(idx)),
}));
}
}
} catch (e) {
console.warn("community runs unavailable, using mocks:", e.message);
}
if (!ALL.length) ALL = MOCK_MAINTAINERS;
populateOrgs();
applyFilters();
// Deep-link: /cards#alvaro opens that card.
const hashId = location.hash.slice(1);
if (hashId) {
const m = ALL.find(x => x.id === hashId);
if (m) openModal(m);
}
}
// Deterministic seeded 20-vector so missing real-data fields still produce
// distinct, recognizable cards (string → stable [0,1]^20).
function synthProfile(seedStr) {
let s = 0;
for (let i = 0; i < seedStr.length; i++) s = (s * 31 + seedStr.charCodeAt(i)) >>> 0;
const out = [];
for (let i = 0; i < EMOTIONS.length; i++) {
s = (s * 1664525 + 1013904223) >>> 0;
out.push(((s >>> 8) & 0xffff) / 0xffff);
}
return out;
}
loadData();
</script>
</body>
</html>