Spaces:
Sleeping
Sleeping
Download cards.html from HF-slyfox/slyfox: direct link, hf CLI and curl.
- Browser
- Download file 44.5 kB
-
https://huggingface.co/spaces/HF-slyfox/slyfox/resolve/main/cards.html
- Command line
-
hf download hf://spaces/HF-slyfox/slyfox/cards.html
-
curl -L -o cards.html https://huggingface.co/spaces/HF-slyfox/slyfox/resolve/main/cards.html
44.5 kB
| <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 ; } | |
| } | |
| .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> | |
| ; | |
| // 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 => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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> | |