Game — Leaderboard / Rankings Page
A competitive game leaderboard page with a glowing top-3 podium, a ranked stat table (tier badges from Bronze to Master, score, win-rate bars, 24h trend arrows), season selector, and Global, Friends, and Region scope tabs. Vanilla JS powers tab and region switching that re-scopes and re-sorts the ladder, sortable score and win-rate columns, search-to-jump with a flash highlight, a pinned highlighted you row, animated rank-change indicators, and load-more pagination.
MCP
Code
:root {
--bg: #0a0b10;
--bg-2: #12131c;
--panel: #171926;
--panel-2: #1f2233;
--text: #e7e9f3;
--muted: #9aa0bf;
--line: rgba(231, 233, 243, 0.1);
--line-2: rgba(231, 233, 243, 0.18);
--accent: #00e5ff;
--accent-2: #7c4dff;
--accent-3: #ff3d71;
--success: #36e27a;
--warn: #ffc857;
--danger: #ff4d4d;
--glow: 0 0 18px rgba(0, 229, 255, 0.45);
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--font-display: "Orbitron", sans-serif;
--font-body: "Inter", sans-serif;
/* tier colors */
--tier-bronze: #c98a5b;
--tier-silver: #b9c2d4;
--tier-gold: #ffd35c;
--tier-platinum: #7ee8e2;
--tier-diamond: #7ab4ff;
--tier-master: #d28bff;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
/* ===== Backdrop grid ===== */
.bg-grid {
position: fixed;
inset: 0;
pointer-events: none;
background-image:
radial-gradient(ellipse 70% 40% at 50% -5%, rgba(124, 77, 255, 0.16), transparent 60%),
radial-gradient(ellipse 50% 35% at 80% 0%, rgba(0, 229, 255, 0.08), transparent 55%),
linear-gradient(rgba(231, 233, 243, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(231, 233, 243, 0.03) 1px, transparent 1px);
background-size: 100% 100%, 100% 100%, 48px 48px, 48px 48px;
z-index: 0;
}
/* ===== Topbar ===== */
.topbar {
position: sticky;
top: 0;
z-index: 30;
background: rgba(10, 11, 16, 0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--line);
}
.topbar-inner {
max-width: 1180px;
margin: 0 auto;
padding: 12px 24px;
display: flex;
align-items: center;
gap: 28px;
}
.logo {
display: inline-flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--text);
}
.logo-mark {
font-family: var(--font-display);
font-weight: 900;
font-size: 0.8rem;
color: #051014;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
width: 34px;
height: 34px;
display: grid;
place-items: center;
clip-path: polygon(22% 0, 100% 0, 100% 78%, 78% 100%, 0 100%, 0 22%);
box-shadow: var(--glow);
}
.logo-text {
font-family: var(--font-display);
font-weight: 700;
font-size: 0.92rem;
letter-spacing: 0.14em;
}
.logo-text em {
font-style: normal;
color: var(--accent);
text-shadow: 0 0 12px rgba(0, 229, 255, 0.6);
}
.topnav {
display: flex;
gap: 4px;
margin-right: auto;
}
.topnav-link {
color: var(--muted);
text-decoration: none;
font-size: 0.85rem;
font-weight: 600;
padding: 8px 14px;
border-radius: var(--r-sm);
transition: color 0.15s, background 0.15s;
}
.topnav-link:hover { color: var(--text); background: rgba(231, 233, 243, 0.06); }
.topnav-link.is-active {
color: var(--accent);
position: relative;
}
.topnav-link.is-active::after {
content: "";
position: absolute;
left: 14px;
right: 14px;
bottom: 2px;
height: 2px;
background: var(--accent);
box-shadow: 0 0 8px rgba(0, 229, 255, 0.8);
}
.topbar-actions {
display: flex;
align-items: center;
gap: 14px;
}
.season-select {
display: inline-flex;
align-items: center;
gap: 8px;
}
.season-label {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
select {
appearance: none;
background: var(--panel) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%239aa0bf'/%3E%3C/svg%3E") no-repeat right 12px center;
border: 1px solid var(--line-2);
color: var(--text);
font-family: var(--font-body);
font-size: 0.82rem;
font-weight: 600;
padding: 8px 32px 8px 12px;
border-radius: var(--r-sm);
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
}
select:hover { border-color: var(--accent); }
select:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--glow); }
/* ===== Buttons ===== */
.btn {
font-family: var(--font-display);
font-weight: 700;
font-size: 0.78rem;
letter-spacing: 0.1em;
text-transform: uppercase;
border: none;
cursor: pointer;
padding: 11px 22px;
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
transition: transform 0.15s, box-shadow 0.2s, background 0.2s, color 0.2s;
}
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.btn:active { transform: translateY(1px) scale(0.985); }
.btn-primary {
background: linear-gradient(120deg, var(--accent), #16b6d8);
color: #051014;
box-shadow: var(--glow);
}
.btn-primary:hover {
box-shadow: 0 0 28px rgba(0, 229, 255, 0.65);
background: linear-gradient(120deg, #4cf0ff, var(--accent));
}
.btn-ghost {
background: var(--panel-2);
color: var(--text);
border: 1px solid var(--line-2);
clip-path: none;
border-radius: var(--r-sm);
}
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 14px rgba(0, 229, 255, 0.25); }
.btn-ghost[disabled] { opacity: 0.45; cursor: not-allowed; box-shadow: none; color: var(--muted); border-color: var(--line); }
/* ===== Layout ===== */
.wrap {
position: relative;
z-index: 1;
max-width: 1180px;
margin: 0 auto;
padding: 40px 24px 64px;
}
/* ===== Hero ===== */
.hero { text-align: center; padding: 16px 0 40px; }
.hero-kicker {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent);
margin: 0 0 12px;
}
.hero-title {
font-family: var(--font-display);
font-weight: 900;
font-size: clamp(1.8rem, 5vw, 3.2rem);
margin: 0 0 14px;
letter-spacing: 0.04em;
background: linear-gradient(100deg, #fff 20%, var(--accent) 55%, var(--accent-2) 90%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 40px rgba(0, 229, 255, 0.25);
}
.hero-sub {
color: var(--muted);
max-width: 560px;
margin: 0 auto 28px;
font-size: 0.95rem;
}
.hero-sub strong { color: var(--text); }
.hero-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
max-width: 760px;
margin: 0 auto;
}
.hero-stat {
background: linear-gradient(180deg, var(--panel), var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 12px;
position: relative;
overflow: hidden;
}
.hero-stat::before {
content: "";
position: absolute;
top: 0;
left: 12%;
right: 12%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
opacity: 0.7;
}
.hero-stat-num {
display: block;
font-family: var(--font-display);
font-weight: 700;
font-size: 1.25rem;
color: var(--text);
font-variant-numeric: tabular-nums;
}
.hero-stat-unit { font-size: 0.7em; color: var(--muted); margin-right: 2px; }
.hero-stat-label {
display: block;
margin-top: 4px;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
/* ===== Podium ===== */
.podium-section { margin-bottom: 32px; }
.podium {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
align-items: end;
max-width: 860px;
margin: 0 auto;
}
.podium-card {
background: linear-gradient(180deg, var(--panel-2), var(--panel));
border: 1px solid var(--line-2);
clip-path: polygon(16px 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 0 100%, 0 16px);
padding: 26px 18px 22px;
text-align: center;
position: relative;
animation: podium-rise 0.55s cubic-bezier(0.2, 0.9, 0.3, 1.2) both;
}
.podium-card:nth-child(1) { animation-delay: 0.1s; }
.podium-card:nth-child(2) { animation-delay: 0s; }
.podium-card:nth-child(3) { animation-delay: 0.18s; }
@keyframes podium-rise {
from { opacity: 0; transform: translateY(26px); }
to { opacity: 1; transform: translateY(0); }
}
.podium-card::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(0, 229, 255, 0.1), transparent 70%);
pointer-events: none;
}
.podium-card.place-1 {
border-color: rgba(255, 211, 92, 0.55);
padding-top: 38px;
padding-bottom: 34px;
box-shadow: 0 0 34px rgba(255, 211, 92, 0.14), inset 0 1px 0 rgba(255, 211, 92, 0.25);
}
.podium-card.place-1::before {
background: radial-gradient(ellipse 80% 45% at 50% 0%, rgba(255, 211, 92, 0.16), transparent 70%);
}
.podium-card.place-2 { border-color: rgba(185, 194, 212, 0.45); }
.podium-card.place-3 { border-color: rgba(201, 138, 91, 0.5); }
.podium-place {
font-family: var(--font-display);
font-weight: 900;
font-size: 0.95rem;
letter-spacing: 0.1em;
margin-bottom: 12px;
}
.place-1 .podium-place { color: var(--tier-gold); text-shadow: 0 0 14px rgba(255, 211, 92, 0.6); }
.place-2 .podium-place { color: var(--tier-silver); }
.place-3 .podium-place { color: var(--tier-bronze); }
.podium-avatar {
width: 68px;
height: 68px;
margin: 0 auto 12px;
display: grid;
place-items: center;
font-family: var(--font-display);
font-weight: 700;
font-size: 1.3rem;
color: #051014;
clip-path: polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
}
.place-1 .podium-avatar { width: 84px; height: 84px; font-size: 1.6rem; }
.podium-name {
font-weight: 800;
font-size: 1rem;
margin: 0 0 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.podium-region { font-size: 0.72rem; color: var(--muted); letter-spacing: 0.08em; }
.podium-score {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.15rem;
color: var(--accent);
text-shadow: 0 0 12px rgba(0, 229, 255, 0.5);
margin-top: 10px;
font-variant-numeric: tabular-nums;
}
.place-1 .podium-score { font-size: 1.4rem; }
.podium-tier { margin-top: 10px; display: inline-block; }
/* ===== Panel / Board ===== */
.panel {
background: linear-gradient(180deg, var(--panel), var(--bg-2));
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: inset 0 1px 0 rgba(231, 233, 243, 0.05), 0 18px 50px rgba(0, 0, 0, 0.45);
overflow: hidden;
}
.board-head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
padding: 20px 22px;
border-bottom: 1px solid var(--line);
}
.board-title {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.05rem;
letter-spacing: 0.1em;
text-transform: uppercase;
margin: 0;
margin-right: auto;
}
.board-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
/* Scope tabs */
.scope-tabs {
display: inline-flex;
background: var(--bg);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 4px;
gap: 4px;
}
.scope-tab {
font-family: var(--font-display);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
background: transparent;
border: none;
border-radius: var(--r-sm);
padding: 8px 16px;
cursor: pointer;
transition: color 0.15s, background 0.2s, box-shadow 0.2s;
}
.scope-tab:hover { color: var(--text); }
.scope-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.scope-tab.is-active {
color: #051014;
background: linear-gradient(120deg, var(--accent), #2ad0ec);
box-shadow: 0 0 14px rgba(0, 229, 255, 0.4);
}
/* Search */
.search-box {
position: relative;
display: inline-flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: var(--muted);
pointer-events: none;
}
.search-box input {
background: var(--bg);
border: 1px solid var(--line-2);
color: var(--text);
font-family: var(--font-body);
font-size: 0.85rem;
padding: 9px 14px 9px 34px;
border-radius: var(--r-md);
width: 210px;
transition: border-color 0.15s, box-shadow 0.15s, width 0.2s;
}
.search-box input::placeholder { color: var(--muted); }
.search-box input:hover { border-color: var(--line-2); }
.search-box input:focus-visible {
outline: none;
border-color: var(--accent);
box-shadow: var(--glow);
width: 240px;
}
/* ===== Table ===== */
.table-scroll { overflow-x: auto; }
.table-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.board-table {
width: 100%;
border-collapse: collapse;
min-width: 680px;
}
.board-table th {
text-align: left;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
padding: 12px 16px;
border-bottom: 1px solid var(--line-2);
background: rgba(10, 11, 16, 0.5);
white-space: nowrap;
}
.sort-btn {
font: inherit;
letter-spacing: inherit;
text-transform: inherit;
color: inherit;
background: none;
border: none;
cursor: pointer;
padding: 2px 4px;
margin: -2px -4px;
border-radius: var(--r-sm);
display: inline-flex;
align-items: center;
gap: 5px;
transition: color 0.15s;
}
.sort-btn:hover { color: var(--text); }
.sort-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sort-arrow { opacity: 0.35; transition: transform 0.2s, opacity 0.2s; display: inline-block; }
.sort-btn.is-sorted { color: var(--accent); }
.sort-btn.is-sorted .sort-arrow { opacity: 1; }
.sort-btn.is-sorted.dir-asc .sort-arrow { transform: rotate(180deg); }
.board-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--line);
font-size: 0.88rem;
vertical-align: middle;
}
.board-row { transition: background 0.15s; animation: row-in 0.3s ease both; }
@keyframes row-in {
from { opacity: 0; transform: translateX(-8px); }
to { opacity: 1; transform: translateX(0); }
}
.board-row:hover { background: rgba(0, 229, 255, 0.045); }
/* Rank cell */
.rank-num {
font-family: var(--font-display);
font-weight: 700;
font-size: 0.92rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.board-row.is-top .rank-num { color: var(--text); }
/* Player cell */
.player-cell { display: flex; align-items: center; gap: 12px; min-width: 0; }
.player-avatar {
flex: none;
width: 34px;
height: 34px;
display: grid;
place-items: center;
font-family: var(--font-display);
font-weight: 700;
font-size: 0.7rem;
color: #051014;
clip-path: polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
}
.player-name { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.player-meta { font-size: 0.7rem; color: var(--muted); letter-spacing: 0.06em; }
.you-chip {
display: inline-block;
margin-left: 8px;
font-family: var(--font-display);
font-size: 0.58rem;
font-weight: 700;
letter-spacing: 0.14em;
color: #051014;
background: var(--accent);
padding: 2px 7px;
border-radius: 99px;
vertical-align: 2px;
box-shadow: 0 0 10px rgba(0, 229, 255, 0.55);
}
/* Tier badges */
.tier-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--font-display);
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 99px;
border: 1px solid currentColor;
background: rgba(10, 11, 16, 0.6);
white-space: nowrap;
}
.tier-badge::before {
content: "";
width: 8px;
height: 8px;
background: currentColor;
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
box-shadow: 0 0 8px currentColor;
}
.tier-bronze { color: var(--tier-bronze); }
.tier-silver { color: var(--tier-silver); }
.tier-gold { color: var(--tier-gold); }
.tier-platinum { color: var(--tier-platinum); }
.tier-diamond { color: var(--tier-diamond); }
.tier-master { color: var(--tier-master); text-shadow: 0 0 10px rgba(210, 139, 255, 0.6); }
/* Score */
.score-num {
font-family: var(--font-display);
font-weight: 700;
font-size: 0.88rem;
font-variant-numeric: tabular-nums;
}
/* Win rate */
.wr-cell { display: flex; align-items: center; gap: 10px; min-width: 130px; }
.wr-num {
font-weight: 700;
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
width: 44px;
}
.wr-bar {
flex: 1;
height: 6px;
background: rgba(231, 233, 243, 0.08);
border-radius: 99px;
overflow: hidden;
min-width: 60px;
}
.wr-fill {
height: 100%;
width: 0;
border-radius: 99px;
background: linear-gradient(90deg, var(--accent-2), var(--accent));
box-shadow: 0 0 8px rgba(0, 229, 255, 0.5);
transition: width 0.7s cubic-bezier(0.2, 0.8, 0.3, 1);
}
.wr-fill.wr-low { background: linear-gradient(90deg, var(--accent-3), var(--warn)); box-shadow: 0 0 8px rgba(255, 200, 87, 0.4); }
/* Trend */
.trend {
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 800;
font-size: 0.78rem;
font-variant-numeric: tabular-nums;
}
.trend-up { color: var(--success); animation: trend-pulse 1.6s ease-in-out 2; }
.trend-down { color: var(--danger); animation: trend-pulse 1.6s ease-in-out 2; }
.trend-flat { color: var(--muted); }
@keyframes trend-pulse {
0%, 100% { text-shadow: none; }
50% { text-shadow: 0 0 12px currentColor; }
}
.trend-arrow { font-size: 0.65rem; }
/* You row */
.board-row.is-you {
background: linear-gradient(90deg, rgba(0, 229, 255, 0.1), rgba(124, 77, 255, 0.08));
box-shadow: inset 3px 0 0 var(--accent);
}
.board-row.is-you:hover { background: linear-gradient(90deg, rgba(0, 229, 255, 0.14), rgba(124, 77, 255, 0.1)); }
.board-row.is-you .player-name { color: var(--accent); }
/* Pinned you separator */
.row-sep td {
text-align: center;
color: var(--muted);
font-size: 0.7rem;
letter-spacing: 0.3em;
padding: 6px;
border-bottom: 1px solid var(--line);
background: rgba(10, 11, 16, 0.4);
}
/* Search highlight flash */
.board-row.is-flash { animation: row-flash 1.6s ease-out 1; }
@keyframes row-flash {
0%, 60% { background: rgba(0, 229, 255, 0.18); box-shadow: inset 3px 0 0 var(--accent); }
100% { background: transparent; box-shadow: none; }
}
/* Empty state */
.empty-row td {
text-align: center;
padding: 40px 16px;
color: var(--muted);
font-size: 0.9rem;
}
/* ===== Board foot ===== */
.board-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 22px;
}
.board-count { margin: 0; font-size: 0.78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
/* ===== Footer ===== */
.page-foot {
text-align: center;
margin-top: 36px;
color: var(--muted);
font-size: 0.74rem;
letter-spacing: 0.04em;
}
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
background: var(--panel-2);
border: 1px solid var(--accent);
color: var(--text);
font-size: 0.84rem;
font-weight: 600;
padding: 11px 20px;
border-radius: var(--r-md);
box-shadow: var(--glow), 0 10px 30px rgba(0, 0, 0, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 60;
max-width: min(90vw, 420px);
text-align: center;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* ===== Reduced motion ===== */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* ===== Responsive ===== */
@media (max-width: 900px) {
.topnav { display: none; }
.hero-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
.wrap { padding: 24px 14px 48px; }
.topbar-inner { padding: 10px 14px; gap: 12px; }
.logo-text { display: none; }
.season-label { display: none; }
.btn-primary { padding: 10px 14px; font-size: 0.68rem; }
.hero { padding-bottom: 28px; }
.hero-stats { gap: 8px; }
.hero-stat-num { font-size: 1rem; }
.podium { grid-template-columns: 1fr; gap: 10px; max-width: 360px; }
.podium-card { display: flex; align-items: center; gap: 14px; text-align: left; padding: 14px 16px !important; }
.podium-card::before { display: none; }
.podium-avatar, .place-1 .podium-avatar { width: 48px; height: 48px; font-size: 0.95rem; margin: 0; }
.podium-place { margin: 0; min-width: 46px; }
.podium-body { min-width: 0; flex: 1; }
.podium-score { margin-top: 2px; font-size: 1rem !important; }
.podium-tier { display: none; }
.board-head { padding: 14px; }
.board-controls { width: 100%; }
.scope-tabs { width: 100%; }
.scope-tab { flex: 1; padding: 8px 6px; }
.search-box, .search-box input, .search-box input:focus-visible { width: 100%; }
.region-select, .region-select select { width: 100%; }
.board-foot { flex-direction: column; }
}/* Hollow Reign — Season Rankings (demo data, vanilla JS) */
(() => {
"use strict";
/* ---------- Toast helper ---------- */
const toastEl = document.getElementById("toast");
let toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("is-show"), 2400);
}
/* ---------- Tiers ---------- */
const TIERS = [
{ id: "master", label: "Master", min: 96000 },
{ id: "diamond", label: "Diamond", min: 88000 },
{ id: "platinum", label: "Platinum", min: 78000 },
{ id: "gold", label: "Gold", min: 66000 },
{ id: "silver", label: "Silver", min: 52000 },
{ id: "bronze", label: "Bronze", min: 0 },
];
const tierFor = (score) => TIERS.find((t) => score >= t.min);
/* ---------- Demo dataset ---------- */
const NAMES = [
["VoidReaperX", "NA"], ["Karnage_07", "EU"], ["NeonDriftQueen", "APAC"],
["Sgt_Hollowpoint", "NA"], ["AshenVanguard", "EU"], ["Mirage.Zero", "APAC"],
["FrostbyteFury", "NA"], ["La_Sombra", "SA"], ["QuantumLeapr", "EU"],
["xX_DreadKnot_Xx", "NA"], ["SilentCircuit", "APAC"], ["WraithOfKiev", "EU"],
["TurboTanuki", "APAC"], ["IronVulture", "NA"], ["ZephyrBlade", "EU"],
["Nocturne_99", "SA"], ["PixelPyre", "NA"], ["GhostMeridian", "EU"],
["CrimsonHavoc", "APAC"], ["StellarKraken", "NA"], ["OmenSeeker", "EU"],
["RogueAntenna", "SA"], ["HyperionFall", "APAC"], ["DuneStalker", "NA"],
["VelvetThorn", "EU"], ["BlitzMagnet", "NA"], ["EchoSaboteur", "APAC"],
["FeralCompass", "SA"], ["LunarPariah", "EU"], ["StaticMantis", "NA"],
["ObsidianLark", "APAC"], ["RiftWalker_J", "EU"], ["CinderFoxx", "NA"],
["TempestGrin", "SA"], ["NullProphet", "EU"], ["AeonSparrow", "APAC"],
["GrimLattice", "NA"], ["SolsticeRex", "EU"], ["PhantomQuill", "SA"],
];
const FRIEND_NAMES = new Set([
"TurboTanuki", "PixelPyre", "CinderFoxx", "RogueAntenna", "EchoSaboteur",
"StaticMantis", "BlitzMagnet", "FeralCompass", "AeonSparrow", "Nocturne_99",
]);
// Deterministic pseudo-random so every load looks identical.
function mulberry32(seed) {
return () => {
seed |= 0; seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function buildSeason(seedBase) {
const rnd = mulberry32(seedBase);
const players = NAMES.map(([name, region], i) => {
const score = Math.round(123400 - i * (1500 + rnd() * 900) - rnd() * 700);
return {
name,
region,
friend: FRIEND_NAMES.has(name),
you: false,
score: Math.max(score, 18000),
winrate: Math.round((38 + rnd() * 34) * 10) / 10,
delta: Math.round((rnd() - 0.45) * 14),
level: 40 + Math.round(rnd() * 260),
};
});
players.push({
name: "VektorPrime",
region: "NA",
friend: true,
you: true,
score: Math.round(96000 + rnd() * 4200),
winrate: 57.4,
delta: 6,
level: 188,
});
return players;
}
const SEASONS = { s7: buildSeason(7077), s6: buildSeason(6066), s5: buildSeason(5055) };
/* ---------- State ---------- */
const PAGE = 12;
const state = {
season: "s7",
scope: "global",
region: "NA",
query: "",
sortKey: "score",
sortDir: "desc",
visible: PAGE,
};
/* ---------- DOM ---------- */
const podiumEl = document.getElementById("podium");
const bodyEl = document.getElementById("board-body");
const countEl = document.getElementById("board-count");
const loadMoreBtn = document.getElementById("load-more");
const searchInput = document.getElementById("search");
const regionWrap = document.getElementById("region-wrap");
const regionSelect = document.getElementById("region");
const seasonSelect = document.getElementById("season");
const tabs = Array.from(document.querySelectorAll(".scope-tab"));
const sortBtns = Array.from(document.querySelectorAll(".sort-btn"));
/* ---------- Helpers ---------- */
const fmt = (n) => n.toLocaleString("en-US");
const AVATAR_GRADS = [
["#00e5ff", "#7c4dff"], ["#ff3d71", "#ffc857"], ["#36e27a", "#00e5ff"],
["#7c4dff", "#ff3d71"], ["#ffc857", "#36e27a"], ["#00e5ff", "#36e27a"],
];
function avatarStyle(name) {
let h = 0;
for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
const [a, b] = AVATAR_GRADS[h % AVATAR_GRADS.length];
return `background:linear-gradient(135deg,${a},${b})`;
}
const initials = (name) =>
name.replace(/[^a-zA-Z]/g, " ").trim().split(/\s+|(?=[A-Z])/)
.filter(Boolean).slice(0, 2).map((p) => p[0].toUpperCase()).join("") || name.slice(0, 2).toUpperCase();
function scopedPlayers() {
const all = SEASONS[state.season];
if (state.scope === "friends") return all.filter((p) => p.friend || p.you);
if (state.scope === "region") return all.filter((p) => p.region === state.region);
return all.slice();
}
// Ranks are always assigned by score, independent of column sorting.
function ranked() {
const list = scopedPlayers().sort((a, b) => b.score - a.score);
list.forEach((p, i) => { p._rank = i + 1; });
return list;
}
function sorted(list) {
const dir = state.sortDir === "asc" ? 1 : -1;
return list.slice().sort((a, b) => (a[state.sortKey] - b[state.sortKey]) * dir);
}
/* ---------- Render: podium ---------- */
function renderPodium(list) {
const order = [1, 0, 2]; // 2nd, 1st, 3rd visual order
const top3 = list.slice(0, 3);
podiumEl.innerHTML = order
.filter((i) => top3[i])
.map((i) => {
const p = top3[i];
const t = tierFor(p.score);
return `
<article class="podium-card place-${i + 1}" aria-label="Rank ${i + 1}: ${p.name}">
<div class="podium-place">#${i + 1}</div>
<div class="podium-avatar" style="${avatarStyle(p.name)}" aria-hidden="true">${initials(p.name)}</div>
<div class="podium-body">
<h3 class="podium-name">${p.name}</h3>
<div class="podium-region">${p.region} · LVL ${p.level}</div>
<div class="podium-score">${fmt(p.score)}</div>
</div>
<span class="tier-badge tier-${t.id} podium-tier">${t.label}</span>
</article>`;
})
.join("");
}
/* ---------- Render: table ---------- */
function rowHTML(p, idx) {
const t = tierFor(p.score);
const trend =
p.delta > 0
? `<span class="trend trend-up"><span class="trend-arrow" aria-hidden="true">▲</span>${p.delta}<span class="sr-only"> ranks up</span></span>`
: p.delta < 0
? `<span class="trend trend-down"><span class="trend-arrow" aria-hidden="true">▼</span>${Math.abs(p.delta)}<span class="sr-only"> ranks down</span></span>`
: `<span class="trend trend-flat">—</span>`;
return `
<tr class="board-row${p._rank <= 3 ? " is-top" : ""}${p.you ? " is-you" : ""}" data-name="${p.name.toLowerCase()}" style="animation-delay:${Math.min(idx * 28, 320)}ms">
<td><span class="rank-num">#${p._rank}</span></td>
<td>
<div class="player-cell">
<span class="player-avatar" style="${avatarStyle(p.name)}" aria-hidden="true">${initials(p.name)}</span>
<div>
<div class="player-name">${p.name}${p.you ? '<span class="you-chip">YOU</span>' : ""}</div>
<div class="player-meta">${p.region} · LVL ${p.level}</div>
</div>
</div>
</td>
<td><span class="tier-badge tier-${t.id}">${t.label}</span></td>
<td><span class="score-num">${fmt(p.score)}</span></td>
<td>
<div class="wr-cell">
<span class="wr-num">${p.winrate.toFixed(1)}%</span>
<span class="wr-bar"><span class="wr-fill${p.winrate < 48 ? " wr-low" : ""}" data-w="${p.winrate}"></span></span>
</div>
</td>
<td>${trend}</td>
</tr>`;
}
function render() {
const byRank = ranked();
renderPodium(byRank);
const list = sorted(byRank);
const shown = list.slice(0, state.visible);
if (!shown.length) {
bodyEl.innerHTML = `<tr class="empty-row"><td colspan="6">No players found in this scope.</td></tr>`;
} else {
let html = shown.map(rowHTML).join("");
// Pin "you" below the visible page if cut off.
const you = list.find((p) => p.you);
if (you && !shown.includes(you)) {
html += `<tr class="row-sep" aria-hidden="true"><td colspan="6">···</td></tr>${rowHTML(you, shown.length)}`;
}
bodyEl.innerHTML = html;
}
countEl.textContent = `Showing ${Math.min(state.visible, list.length)} of ${list.length} players`;
loadMoreBtn.disabled = state.visible >= list.length;
// Animate win-rate bars after layout.
requestAnimationFrame(() => {
bodyEl.querySelectorAll(".wr-fill").forEach((el) => {
el.style.width = `${el.dataset.w}%`;
});
});
updateSortIndicators();
}
function updateSortIndicators() {
sortBtns.forEach((btn) => {
const active = btn.dataset.sort === state.sortKey;
btn.classList.toggle("is-sorted", active);
btn.classList.toggle("dir-asc", active && state.sortDir === "asc");
btn.setAttribute("aria-pressed", String(active));
});
}
/* ---------- Interactions ---------- */
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
if (tab.dataset.scope === state.scope) return;
tabs.forEach((t) => {
const on = t === tab;
t.classList.toggle("is-active", on);
t.setAttribute("aria-selected", String(on));
});
state.scope = tab.dataset.scope;
state.visible = PAGE;
regionWrap.hidden = state.scope !== "region";
const labels = { global: "Global ladder", friends: "Friends ladder", region: `Region ladder — ${regionSelect.selectedOptions[0].text}` };
toast(labels[state.scope]);
render();
});
});
regionSelect.addEventListener("change", () => {
state.region = regionSelect.value;
state.visible = PAGE;
toast(`Region: ${regionSelect.selectedOptions[0].text}`);
render();
});
seasonSelect.addEventListener("change", () => {
state.season = seasonSelect.value;
state.visible = PAGE;
toast(`Loaded ${seasonSelect.selectedOptions[0].text}`);
render();
});
sortBtns.forEach((btn) => {
btn.addEventListener("click", () => {
const key = btn.dataset.sort;
if (state.sortKey === key) {
state.sortDir = state.sortDir === "desc" ? "asc" : "desc";
} else {
state.sortKey = key;
state.sortDir = "desc";
}
render();
});
});
loadMoreBtn.addEventListener("click", () => {
state.visible += PAGE;
render();
});
/* Search-to-jump: expands the table to the match, scrolls and flashes it. */
let searchTimer = null;
function jumpToPlayer() {
const q = searchInput.value.trim().toLowerCase();
state.query = q;
if (!q) return;
const list = sorted(ranked());
const idx = list.findIndex((p) => p.name.toLowerCase().includes(q));
if (idx === -1) {
toast(`No player matching “${searchInput.value.trim()}” in this scope`);
return;
}
if (idx >= state.visible) {
state.visible = Math.ceil((idx + 1) / PAGE) * PAGE;
render();
}
const row = bodyEl.querySelector(`.board-row[data-name*="${CSS.escape(q)}"]`);
if (row) {
row.scrollIntoView({ behavior: "smooth", block: "center" });
row.classList.remove("is-flash");
void row.offsetWidth; // restart animation
row.classList.add("is-flash");
toast(`Found ${list[idx].name} — rank #${list[idx]._rank}`);
}
}
searchInput.addEventListener("input", () => {
clearTimeout(searchTimer);
if (searchInput.value.trim().length >= 2) {
searchTimer = setTimeout(jumpToPlayer, 420);
}
});
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
clearTimeout(searchTimer);
jumpToPlayer();
}
});
document.getElementById("play-btn").addEventListener("click", () => {
toast("Launching Hollow Reign… (demo)");
});
/* ---------- Hero counters ---------- */
function animateCounters() {
document.querySelectorAll("[data-counter]").forEach((el) => {
const target = Number(el.dataset.counter);
const start = performance.now();
const dur = 1100;
const tick = (now) => {
const t = Math.min((now - start) / dur, 1);
const eased = 1 - Math.pow(1 - t, 3);
el.textContent = fmt(Math.round(target * eased));
if (t < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
}
/* ---------- Screen-reader-only utility (injected) ---------- */
const srStyle = document.createElement("style");
srStyle.textContent = ".sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}";
document.head.appendChild(srStyle);
/* ---------- Init ---------- */
render();
animateCounters();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hollow Reign — Season 7 Rankings</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="bg-grid" aria-hidden="true"></div>
<header class="topbar">
<div class="topbar-inner">
<a class="logo" href="#" aria-label="Hollow Reign home">
<span class="logo-mark" aria-hidden="true">HR</span>
<span class="logo-text">HOLLOW <em>REIGN</em></span>
</a>
<nav class="topnav" aria-label="Main">
<a href="#" class="topnav-link">Overview</a>
<a href="#" class="topnav-link is-active" aria-current="page">Rankings</a>
<a href="#" class="topnav-link">Esports</a>
<a href="#" class="topnav-link">Patch Notes</a>
</nav>
<div class="topbar-actions">
<label class="season-select" for="season">
<span class="season-label">Season</span>
<select id="season" aria-label="Select season">
<option value="s7" selected>S7 — Crimson Protocol</option>
<option value="s6">S6 — Null Ascension</option>
<option value="s5">S5 — Ghost Meridian</option>
</select>
</label>
<button class="btn btn-primary" type="button" id="play-btn">Play Now</button>
</div>
</div>
</header>
<main class="wrap">
<!-- ===== Hero ===== -->
<section class="hero" aria-labelledby="hero-title">
<p class="hero-kicker">Ranked · Conquest Mode · by Nullforge Studios</p>
<h1 class="hero-title" id="hero-title">Season 7 Leaderboard</h1>
<p class="hero-sub">The Crimson Protocol ladder closes in <strong>12 days</strong>. Top 500 players earn the <strong>Obsidian Wraith</strong> armor set and a Master season frame.</p>
<div class="hero-stats" role="list">
<div class="hero-stat" role="listitem">
<span class="hero-stat-num" data-counter="2847119">0</span>
<span class="hero-stat-label">Players Ranked</span>
</div>
<div class="hero-stat" role="listitem">
<span class="hero-stat-num" data-counter="412">0</span>
<span class="hero-stat-label">Your Rank</span>
</div>
<div class="hero-stat" role="listitem">
<span class="hero-stat-num" data-counter="98314">0</span>
<span class="hero-stat-label">Your Score</span>
</div>
<div class="hero-stat" role="listitem">
<span class="hero-stat-num">12<span class="hero-stat-unit">d</span> 07<span class="hero-stat-unit">h</span></span>
<span class="hero-stat-label">Season Ends</span>
</div>
</div>
</section>
<!-- ===== Podium ===== -->
<section class="podium-section" aria-label="Top three players">
<div class="podium" id="podium">
<!-- podium cards rendered by JS -->
</div>
</section>
<!-- ===== Board ===== -->
<section class="board panel" aria-labelledby="board-title">
<div class="board-head">
<h2 class="board-title" id="board-title">Rankings</h2>
<div class="board-controls">
<div class="scope-tabs" role="tablist" aria-label="Leaderboard scope">
<button class="scope-tab is-active" role="tab" aria-selected="true" data-scope="global" id="tab-global">Global</button>
<button class="scope-tab" role="tab" aria-selected="false" data-scope="friends" id="tab-friends">Friends</button>
<button class="scope-tab" role="tab" aria-selected="false" data-scope="region" id="tab-region">Region</button>
</div>
<label class="region-select" for="region" id="region-wrap" hidden>
<select id="region" aria-label="Select region">
<option value="NA" selected>North America</option>
<option value="EU">Europe</option>
<option value="APAC">Asia Pacific</option>
<option value="SA">South America</option>
</select>
</label>
<div class="search-box">
<svg class="search-icon" aria-hidden="true" viewBox="0 0 24 24" width="15" height="15"><path fill="currentColor" d="M10 2a8 8 0 1 0 4.9 14.3l5.4 5.4 1.4-1.4-5.4-5.4A8 8 0 0 0 10 2Zm0 2a6 6 0 1 1 0 12 6 6 0 0 1 0-12Z"/></svg>
<input type="search" id="search" placeholder="Find a gamertag…" aria-label="Search players" autocomplete="off" />
</div>
</div>
</div>
<div class="table-scroll" role="region" aria-label="Rankings table" tabindex="0">
<table class="board-table">
<thead>
<tr>
<th scope="col" class="col-rank">Rank</th>
<th scope="col" class="col-player">Player</th>
<th scope="col" class="col-tier">Tier</th>
<th scope="col" class="col-score">
<button class="sort-btn" type="button" data-sort="score" aria-label="Sort by score">
Score <span class="sort-arrow" aria-hidden="true">▾</span>
</button>
</th>
<th scope="col" class="col-wr">
<button class="sort-btn" type="button" data-sort="winrate" aria-label="Sort by win rate">
Win Rate <span class="sort-arrow" aria-hidden="true">▾</span>
</button>
</th>
<th scope="col" class="col-trend">24h</th>
</tr>
</thead>
<tbody id="board-body">
<!-- rows rendered by JS -->
</tbody>
</table>
</div>
<div class="board-foot">
<p class="board-count" id="board-count" aria-live="polite"></p>
<button class="btn btn-ghost" type="button" id="load-more">Load More <span aria-hidden="true">+</span></button>
</div>
</section>
<footer class="page-foot">
<p>HOLLOW REIGN © 2026 Nullforge Studios. Rankings refresh every 15 minutes. Fictional demo data.</p>
</footer>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Leaderboard / Rankings Page
A full competitive rankings screen for the fictional shooter Hollow Reign (studio Nullforge). A neon hero banner with animated counters sits above a glowing top-3 podium, where first place rises higher with a gold accent and each finalist shows an avatar, region, level, score, and rank tier. Below it, a ranked table lists every player with a hex avatar, gamertag, tier badge (Bronze, Silver, Gold, Platinum, Diamond, Master), score, an animated win-rate bar, and a 24-hour trend arrow that pulses green for climbers and red for fallers. The current player is rendered as a highlighted YOU row with an accent edge glow.
Interactions are all vanilla JS over deterministic demo data. The Global / Friends / Region scope tabs re-filter the ladder, with a region selector appearing for region scope; a season selector swaps to a different dataset. The Score and Win Rate column headers are sortable (toggle ascending/descending), while ranks stay pinned to true score order. Search-to-jump expands the table to the matching gamertag, scrolls to it, and flashes the row; if your own row falls below the visible page it is pinned at the bottom after a separator. A load-more button paginates the ladder twelve players at a time, and a toast helper confirms each action.
The whole page is keyboard-usable with visible focus rings, respects prefers-reduced-motion, and
collapses the podium and controls into a stacked single-column layout down to ~360px.
Illustrative UI only — fictional games, studios, characters, and data. Not engine integrations.