OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      3ecd8d4266b7
DATE      2025-09-16
SUBJECT   web: support class/realm/region player leaderboards
FILES     11 CHANGED
HASH      3ecd8d4266b7
DATE      2025-09-16
SUBJECT   web: support class/realm/region player
          leaderboards
FILES     11 CHANGED
 

diff --git a/web/.astro/data-store.json b/web/.astro/data-store.json
index fb97c30..3aef997 100644
--- a/web/.astro/data-store.json
+++ b/web/.astro/data-store.json
@@ -1 +1 @@
-[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.12.0","astro-config-
digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{}
,\"site\":\"https://wowsimstats.com\",\"compressHTML\":true,\"base\":\"/\",\"trail
ingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",
\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astr
o\",\"serverEntry\":\"entry.mjs\",\"redirects\":false,\"inlineStylesheets\":\"auto
\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":24173,\"s
treaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"r
oute\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"
config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"de
vToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\
",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"th
eme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPl
ugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":t
rue},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\
":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false
,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\
":false,\"csp\":false,\"rawEnvValues\":false},\"legacy\":{\"collections\":false}}"
]
\ No newline at end of file
+[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.12.0","astro-config-
digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{}
,\"site\":\"https://wowsimstats.com\",\"compressHTML\":true,\"base\":\"/\",\"trail
ingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",
\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astr
o\",\"serverEntry\":\"entry.mjs\",\"redirects\":false,\"inlineStylesheets\":\"auto
\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":30711,\"s
treaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"r
oute\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"
config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"de
vToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\
",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"th
eme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPl
ugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":t
rue},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\
":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false
,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\
":false,\"csp\":false,\"rawEnvValues\":false},\"legacy\":{\"collections\":false}}"
]
\ No newline at end of file

diff --git a/web/.astro/data-store.json b/we
b/.astro/data-store.json
index fb97c30..3aef997 100644
--- a/web/.astro/data-store.json
+++ b/web/.astro/data-store.json
@@ -1 +1 @@
-[["Map",1,2],"meta::meta",["Map",3,4,5,6],"
astro-version","5.12.0","astro-config-digest
","{\"root\":{},\"srcDir\":{},\"publicDir\":
{},\"outDir\":{},\"cacheDir\":{},\"site\":\"
https://wowsimstats.com\",\"compressHTML\":t
rue,\"base\":\"/\",\"trailingSlash\":\"ignor
e\",\"output\":\"server\",\"scopedStyleStrat
egy\":\"attribute\",\"build\":{\"format\":\"
directory\",\"client\":{},\"server\":{},\"as
sets\":\"_astro\",\"serverEntry\":\"entry.mj
s\",\"redirects\":false,\"inlineStylesheets\
":\"auto\",\"concurrency\":1},\"server\":{\"
open\":false,\"host\":false,\"port\":24173,\
"streaming\":true,\"allowedHosts\":[]},\"red
irects\":{},\"image\":{\"endpoint\":{\"route
\":\"/_image\"},\"service\":{\"entrypoint\":
\"astro/assets/services/sharp\",\"config\":{
}},\"domains\":[],\"remotePatterns\":[],\"re
sponsiveStyles\":false},\"devToolbar\":{\"en
abled\":true},\"markdown\":{\"syntaxHighligh
t\":{\"type\":\"shiki\",\"excludeLangs\":[\"
math\"]},\"shikiConfig\":{\"langs\":[],\"lan
gAlias\":{},\"theme\":\"github-dark\",\"them
es\":{},\"wrap\":false,\"transformers\":[]},
\"remarkPlugins\":[],\"rehypePlugins\":[],\"
remarkRehype\":{},\"gfm\":true,\"smartypants
\":true},\"security\":{\"checkOrigin\":true}
,\"env\":{\"schema\":{},\"validateSecrets\":
false},\"experimental\":{\"clientPrerender\"
:false,\"contentIntellisense\":false,\"headi
ngIdCompat\":false,\"preserveScriptOrder\":f
alse,\"liveContentCollections\":false,\"csp\
":false,\"rawEnvValues\":false},\"legacy\":{
\"collections\":false}}"]
\ No newline at end of file
+[["Map",1,2],"meta::meta",["Map",3,4,5,6],"
astro-version","5.12.0","astro-config-digest
","{\"root\":{},\"srcDir\":{},\"publicDir\":
{},\"outDir\":{},\"cacheDir\":{},\"site\":\"
https://wowsimstats.com\",\"compressHTML\":t
rue,\"base\":\"/\",\"trailingSlash\":\"ignor
e\",\"output\":\"server\",\"scopedStyleStrat
egy\":\"attribute\",\"build\":{\"format\":\"
directory\",\"client\":{},\"server\":{},\"as
sets\":\"_astro\",\"serverEntry\":\"entry.mj
s\",\"redirects\":false,\"inlineStylesheets\
":\"auto\",\"concurrency\":1},\"server\":{\"
open\":false,\"host\":false,\"port\":30711,\
"streaming\":true,\"allowedHosts\":[]},\"red
irects\":{},\"image\":{\"endpoint\":{\"route
\":\"/_image\"},\"service\":{\"entrypoint\":
\"astro/assets/services/sharp\",\"config\":{
}},\"domains\":[],\"remotePatterns\":[],\"re
sponsiveStyles\":false},\"devToolbar\":{\"en
abled\":true},\"markdown\":{\"syntaxHighligh
t\":{\"type\":\"shiki\",\"excludeLangs\":[\"
math\"]},\"shikiConfig\":{\"langs\":[],\"lan
gAlias\":{},\"theme\":\"github-dark\",\"them
es\":{},\"wrap\":false,\"transformers\":[]},
\"remarkPlugins\":[],\"rehypePlugins\":[],\"
remarkRehype\":{},\"gfm\":true,\"smartypants
\":true},\"security\":{\"checkOrigin\":true}
,\"env\":{\"schema\":{},\"validateSecrets\":
false},\"experimental\":{\"clientPrerender\"
:false,\"contentIntellisense\":false,\"headi
ngIdCompat\":false,\"preserveScriptOrder\":f
alse,\"liveContentCollections\":false,\"csp\
":false,\"rawEnvValues\":false},\"legacy\":{
\"collections\":false}}"]
\ No newline at end of file
 

diff --git a/web/.astro/types.d.ts b/web/.astro/types.d.ts
index f964fe0..03d7cc4 100644
--- a/web/.astro/types.d.ts
+++ b/web/.astro/types.d.ts
@@ -1 +1,2 @@
 /// <reference types="astro/client" />
+/// <reference path="content.d.ts" />
\ No newline at end of file

diff --git a/web/.astro/types.d.ts b/web/.as
tro/types.d.ts
index f964fe0..03d7cc4 100644
--- a/web/.astro/types.d.ts
+++ b/web/.astro/types.d.ts
@@ -1 +1,2 @@
 /// <reference types="astro/client" />
+/// <reference path="content.d.ts" />
\ No newline at end of file
 

diff --git a/web/src/client/filter-panel.ts b/web/src/client/filter-panel.ts
index 5b15d8a..f191e28 100644
--- a/web/src/client/filter-panel.ts
+++ b/web/src/client/filter-panel.ts
@@ -37,13 +37,11 @@ class FilterPanel {
     if (initDungeon) this.dungeonSelect.value = initDungeon;
 
     this.initializeFromURL();
-    // If URL didn't drive an initial load, auto-load current selection once tabl
e is ready
-    if (!this.didInitFromURL) {
+    // If URL didn't drive an initial load, only auto-load if the dungeon section
 is active
+    if (!this.didInitFromURL && this.isDungeonSectionActive()) {
       const region = this.regionSelect.value;
       const dungeon = this.dungeonSelect.value;
-      if (region && dungeon) {
-        this.loadWhenReady(1);
-      }
+      if (region && dungeon) this.loadWhenReady(1);
     }
   }
 
@@ -56,6 +54,17 @@ class FilterPanel {
     this.dungeonSelect.addEventListener("change", () => this.loadLeaderboard());
   }
 
+  private isDungeonSectionActive(): boolean {
+    try {
+      const section = this.container.closest("#dungeon-section") as
+        | HTMLElement
+        | null;
+      return !!section && section.classList.contains("active");
+    } catch {
+      return false;
+    }
+  }
+
   private updateRealmOptions() {
     const region = this.regionSelect.value;
     const allOptions = this.realmSelect.querySelectorAll("option[data-region]");

diff --git a/web/src/client/filter-panel.ts 
b/web/src/client/filter-panel.ts
index 5b15d8a..f191e28 100644
--- a/web/src/client/filter-panel.ts
+++ b/web/src/client/filter-panel.ts
@@ -37,13 +37,11 @@ class FilterPanel {
     if (initDungeon) this.dungeonSelect.val
ue = initDungeon;
 
     this.initializeFromURL();
-    // If URL didn't drive an initial load,
 auto-load current selection once table is r
eady
-    if (!this.didInitFromURL) {
+    // If URL didn't drive an initial load,
 only auto-load if the dungeon section is ac
tive
+    if (!this.didInitFromURL && this.isDung
eonSectionActive()) {
       const region = this.regionSelect.valu
e;
       const dungeon = this.dungeonSelect.va
lue;
-      if (region && dungeon) {
-        this.loadWhenReady(1);
-      }
+      if (region && dungeon) this.loadWhenR
eady(1);
     }
   }
 
@@ -56,6 +54,17 @@ class FilterPanel {
     this.dungeonSelect.addEventListener("ch
ange", () => this.loadLeaderboard());
   }
 
+  private isDungeonSectionActive(): boolean
 {
+    try {
+      const section = this.container.closes
t("#dungeon-section") as
+        | HTMLElement
+        | null;
+      return !!section && section.classList
.contains("active");
+    } catch {
+      return false;
+    }
+  }
+
   private updateRealmOptions() {
     const region = this.regionSelect.value;
     const allOptions = this.realmSelect.que
rySelectorAll("option[data-region]");
 

diff --git a/web/src/client/leaderboard-table.ts b/web/src/client/leaderboard-tabl
e.ts
index adea96f..2931fd6 100644
--- a/web/src/client/leaderboard-table.ts
+++ b/web/src/client/leaderboard-table.ts
@@ -13,6 +13,7 @@ class LeaderboardTable {
   private realm: string;
   private dungeon: string;
   private currentPage = 1;
+  private lastRenderWasEmpty = false;
 
   constructor(container: HTMLElement) {
     this.container = container;
@@ -131,6 +132,8 @@ class LeaderboardTable {
     } catch (error) {
       this.showError(error as Error);
       console.error("Leaderboard loading error:", error);
+      // Treat error state as empty for transition tracking
+      this.lastRenderWasEmpty = true;
     } finally {
       this.hideLoading();
     }
@@ -141,21 +144,74 @@ class LeaderboardTable {
       "#leaderboard-content",
     ) as HTMLElement | null;
     if (!content) return;
-    if (!data.leading_groups || data.leading_groups.length === 0) {
+
+    const hasData = data.leading_groups && data.leading_groups.length > 0;
+    
+    console.log(`[LeaderboardTable] Rendering leaderboard - hasData: ${hasData}, 
lastRenderWasEmpty: ${this.lastRenderWasEmpty}`);
+    
+    // Always reset all UI elements first
+    const paginationContainer = this.container.querySelector(
+      ".pagination-container",
+    ) as HTMLElement | null;
+    if (paginationContainer) {
+      paginationContainer.style.display = "none";
+    }
+
+    const crossRealmNote = this.container.querySelector(
+      "#cross-realm-note",
+    ) as HTMLElement | null;
+    if (crossRealmNote) {
+      crossRealmNote.style.display = "none";
+    }
+
+    // Clear all loading/error states
+    this.hideLoading();
+    const errorContainer = this.container.querySelector("#error-container") as HT
MLElement | null;
+    if (errorContainer) {
+      errorContainer.style.display = "none";
+    }
+
+    // Ensure content is visible (in case it was hidden by error state)
+    content.style.display = "";
+
+    // Always clear existing content completely to ensure clean slate
+    content.innerHTML = "";
+    
+    if (!hasData) {
+      console.log(`[LeaderboardTable] Showing empty state`);
       content.innerHTML = `
         <div class="empty-state">
           <p>No runs found for this leaderboard.</p>
           <p>Try selecting a different region, realm, or dungeon.</p>
         </div>
       `;
+      this.lastRenderWasEmpty = true;
       return;
     }
 
+    // Force DOM cleanup when transitioning from empty/error to populated
+    if (this.lastRenderWasEmpty) {
+      console.log(`[LeaderboardTable] Transitioning from empty/error to populated
 - forcing DOM reflow`);
+      // Force reflow to ensure DOM is clean
+      content.offsetHeight;
+      // Additional cleanup - remove any residual event listeners
+      const oldRows = this.container.querySelectorAll('.leaderboard-table-row');
+      oldRows.forEach(row => row.remove());
+    }
+
     content.innerHTML =
       '<div id="leaderboard-rows" class="leaderboard-table"></div>';
     const rowsContainer = content.querySelector(
       "#leaderboard-rows",
     ) as HTMLElement | null;
+    
+    if (!rowsContainer) {
+      console.error(`[LeaderboardTable] Failed to create rowsContainer`);
+      return;
+    }
+    
+    console.log(`[LeaderboardTable] About to render ${data.leading_groups.length}
 runs`);
+    this.lastRenderWasEmpty = false;
     data.leading_groups.forEach((run: any, index: number) => {
       const rank = run.ranking || (this.currentPage - 1) * 25 + index + 1;
       const duration = formatDurationMMSS(run.duration);
@@ -278,6 +334,8 @@ class LeaderboardTable {
       `;
       rowsContainer?.appendChild(row);
     });
+    
+    console.log(`[LeaderboardTable] Completed rendering ${data.leading_groups.len
gth} runs`);
   }
 
   private updatePagination(pagination: any) {

diff --git a/web/src/client/leaderboard-tabl
e.ts b/web/src/client/leaderboard-table.ts
index adea96f..2931fd6 100644
--- a/web/src/client/leaderboard-table.ts
+++ b/web/src/client/leaderboard-table.ts
@@ -13,6 +13,7 @@ class LeaderboardTable {
   private realm: string;
   private dungeon: string;
   private currentPage = 1;
+  private lastRenderWasEmpty = false;
 
   constructor(container: HTMLElement) {
     this.container = container;
@@ -131,6 +132,8 @@ class LeaderboardTable {
     } catch (error) {
       this.showError(error as Error);
       console.error("Leaderboard loading er
ror:", error);
+      // Treat error state as empty for tra
nsition tracking
+      this.lastRenderWasEmpty = true;
     } finally {
       this.hideLoading();
     }
@@ -141,21 +144,74 @@ class LeaderboardTable
 {
       "#leaderboard-content",
     ) as HTMLElement | null;
     if (!content) return;
-    if (!data.leading_groups || data.leadin
g_groups.length === 0) {
+
+    const hasData = data.leading_groups && 
data.leading_groups.length > 0;
+    
+    console.log(`[LeaderboardTable] Renderi
ng leaderboard - hasData: ${hasData}, lastRe
nderWasEmpty: ${this.lastRenderWasEmpty}`);
+    
+    // Always reset all UI elements first
+    const paginationContainer = this.contai
ner.querySelector(
+      ".pagination-container",
+    ) as HTMLElement | null;
+    if (paginationContainer) {
+      paginationContainer.style.display = "
none";
+    }
+
+    const crossRealmNote = this.container.q
uerySelector(
+      "#cross-realm-note",
+    ) as HTMLElement | null;
+    if (crossRealmNote) {
+      crossRealmNote.style.display = "none"
;
+    }
+
+    // Clear all loading/error states
+    this.hideLoading();
+    const errorContainer = this.container.q
uerySelector("#error-container") as HTMLElem
ent | null;
+    if (errorContainer) {
+      errorContainer.style.display = "none"
;
+    }
+
+    // Ensure content is visible (in case i
t was hidden by error state)
+    content.style.display = "";
+
+    // Always clear existing content comple
tely to ensure clean slate
+    content.innerHTML = "";
+    
+    if (!hasData) {
+      console.log(`[LeaderboardTable] Showi
ng empty state`);
       content.innerHTML = `
         <div class="empty-state">
           <p>No runs found for this leaderb
oard.</p>
           <p>Try selecting a different regi
on, realm, or dungeon.</p>
         </div>
       `;
+      this.lastRenderWasEmpty = true;
       return;
     }
 
+    // Force DOM cleanup when transitioning
 from empty/error to populated
+    if (this.lastRenderWasEmpty) {
+      console.log(`[LeaderboardTable] Trans
itioning from empty/error to populated - for
cing DOM reflow`);
+      // Force reflow to ensure DOM is clea
n
+      content.offsetHeight;
+      // Additional cleanup - remove any re
sidual event listeners
+      const oldRows = this.container.queryS
electorAll('.leaderboard-table-row');
+      oldRows.forEach(row => row.remove());
+    }
+
     content.innerHTML =
       '<div id="leaderboard-rows" class="le
aderboard-table"></div>';
     const rowsContainer = content.querySele
ctor(
       "#leaderboard-rows",
     ) as HTMLElement | null;
+    
+    if (!rowsContainer) {
+      console.error(`[LeaderboardTable] Fai
led to create rowsContainer`);
+      return;
+    }
+    
+    console.log(`[LeaderboardTable] About t
o render ${data.leading_groups.length} runs`
);
+    this.lastRenderWasEmpty = false;
     data.leading_groups.forEach((run: any, 
index: number) => {
       const rank = run.ranking || (this.cur
rentPage - 1) * 25 + index + 1;
       const duration = formatDurationMMSS(r
un.duration);
@@ -278,6 +334,8 @@ class LeaderboardTable {
       `;
       rowsContainer?.appendChild(row);
     });
+    
+    console.log(`[LeaderboardTable] Complet
ed rendering ${data.leading_groups.length} r
uns`);
   }
 
   private updatePagination(pagination: any)
 {
 

diff --git a/web/src/client/player-filter-panel.ts b/web/src/client/player-filter-
panel.ts
new file mode 100644
index 0000000..595dad2
--- /dev/null
+++ b/web/src/client/player-filter-panel.ts
@@ -0,0 +1,169 @@
+import { REALM_DATA } from "../lib/realms";
+import { buildPlayersLeaderboardURL } from "../lib/utils.ts";
+
+class PlayerFilterPanelClient {
+  private container: HTMLElement;
+  private regionSelect: HTMLSelectElement;
+  private realmSelect: HTMLSelectElement;
+  private classSelect: HTMLSelectElement;
+
+  constructor(container: HTMLElement) {
+    this.container = container;
+    this.regionSelect = container.querySelector(
+      "#player-region-select",
+    ) as HTMLSelectElement;
+    this.realmSelect = container.querySelector(
+      "#player-realm-select",
+    ) as HTMLSelectElement;
+    this.classSelect = container.querySelector(
+      "#player-class-select",
+    ) as HTMLSelectElement;
+
+    this.bindEvents();
+    this.updateRealmOptions();
+    this.initFromURLOrDataset();
+  }
+
+  private bindEvents() {
+    this.regionSelect.addEventListener("change", () => {
+      this.updateRealmOptions();
+      this.notifyLeaderboard(1);
+    });
+    this.realmSelect.addEventListener("change", () => this.notifyLeaderboard(1));
+    this.classSelect.addEventListener("change", () => this.notifyLeaderboard(1));
+  }
+
+  private updateRealmOptions() {
+    const region = this.regionSelect.value;
+    const options = this.realmSelect.querySelectorAll("option[data-region]");
+    options.forEach((opt) => ((opt as HTMLOptionElement).style.display = "none"))
;
+    if (region === "global") {
+      this.realmSelect.disabled = true;
+      this.realmSelect.value = "all";
+    } else {
+      this.realmSelect.disabled = false;
+      const regionOptions = this.realmSelect.querySelectorAll(
+        `option[data-region="${region}"]`,
+      );
+      regionOptions.forEach(
+        (opt) => ((opt as HTMLOptionElement).style.display = "block"),
+      );
+      // Ensure valid selection
+      const current = this.realmSelect.value;
+      if (current && current !== "all") {
+        const exists = Array.from(regionOptions).some(
+          (opt) => (opt as HTMLOptionElement).value === current,
+        );
+        if (!exists) this.realmSelect.value = "all";
+      } else {
+        this.realmSelect.value = "all";
+      }
+    }
+  }
+
+  private initFromURLOrDataset() {
+    const { scope, region, realm, klass, page } = this.parsePlayersPath();
+    this.regionSelect.value = region || "global";
+    this.updateRealmOptions();
+    if (region && region !== "global") this.realmSelect.value = realm || "all";
+    this.classSelect.value = klass || "";
+    // Only notify/load if players section is currently active; otherwise wait fo
r tab switch
+    if (this.isPlayersSectionActive()) {
+      this.notifyLeaderboard(page || 1);
+    }
+  }
+
+  private notifyLeaderboard(page: number) {
+    const region = this.regionSelect.value;
+    const realmVal = this.realmSelect.value || "all";
+    const classKey = this.classSelect.value || "";
+
+    const scope = region === "global" ? "global" : realmVal !== "all" ? "realm" :
 "regional";
+    const realmSlug = scope === "realm" ? realmVal : undefined;
+
+    const w: any = window as any;
+    const pl = w.playerLeaderboard;
+    if (pl && typeof pl.setFilters === "function") {
+      pl.setFilters({ scope, region: region === "global" ? undefined : region, re
almSlug, classKey, page });
+    }
+
+    const newPath = buildPlayersLeaderboardURL(scope, {
+      region: region === "global" ? undefined : region,
+      realmSlug,
+      classKey,
+      page,
+    });
+    try { sessionStorage.setItem("lastPlayersPath", newPath); } catch {}
+    window.history.pushState({}, "", newPath);
+  }
+
+  private isPlayersSectionActive(): boolean {
+    try {
+      const section = this.container.closest("#players-section") as
+        | HTMLElement
+        | null;
+      return !!section && section.classList.contains("active");
+    } catch {
+      return false;
+    }
+  }
+
+  private parsePlayersPath(): { scope: "global" | "regional" | "realm"; region?: 
string; realm?: string; klass?: string; page?: number } {
+    const path = window.location.pathname.replace(/\/+$/, "");
+    const parts = path.split("/").filter(Boolean);
+    // Expect: ["challenge-mode","players", ...]
+    if (parts.length >= 2 && parts[0] === "challenge-mode" && parts[1] === "playe
rs") {
+      const rest = parts.slice(2);
+      if (rest.length === 0) {
+        return { scope: "global", klass: undefined, page: this.readPageParam() };
+      }
+      if (rest[0] === "global") {
+        const klass = rest[1] || "";
+        return { scope: "global", klass, page: this.readPageParam() };
+      }
+      const region = rest[0];
+      if (rest.length === 1) {
+        return { scope: "regional", region, page: this.readPageParam() };
+      }
+      // Two or more segments: could be region/class or region/realm[/class]
+      if (rest.length === 2) {
+        // Ambiguous; assume region/class if second looks like a class (letters/u
nderscores)
+        const second = rest[1];
+        if (/^[a-z_]+$/.test(second)) {
+          return { scope: "regional", region, klass: second, page: this.readPageP
aram() };
+        }
+        return { scope: "realm", region, realm: second, page: this.readPageParam(
) };
+      }
+      // region/realm/class
+      return { scope: "realm", region, realm: rest[1], klass: rest[2], page: this
.readPageParam() };
+    }
+    // Fallback: derive from query params as legacy
+    const url = new URL(window.location.href);
+    const region = url.searchParams.get("region") || undefined;
+    const realm = url.searchParams.get("realm") || undefined;
+    const klass = url.searchParams.get("class") || undefined;
+    const page = this.readPageParam();
+    if (!region) return { scope: "global", klass, page };
+    if (realm) return { scope: "realm", region, realm, klass, page };
+    return { scope: "regional", region, klass, page };
+  }
+
+  private readPageParam(): number | undefined {
+    const url = new URL(window.location.href);
+    const page = parseInt(url.searchParams.get("page") || "1", 10);
+    return isNaN(page) || page <= 1 ? undefined : page;
+  }
+}
+
+function initPlayerFilterPanel() {
+  const el = document.querySelector(
+    ".player-filter-panel",
+  ) as HTMLElement | null;
+  if (el) new PlayerFilterPanelClient(el);
+}
+
+if (document.readyState === "loading") {
+  document.addEventListener("DOMContentLoaded", initPlayerFilterPanel);
+} else {
+  initPlayerFilterPanel();
+}

diff --git a/web/src/client/player-filter-pa
nel.ts b/web/src/client/player-filter-panel.
ts
new file mode 100644
index 0000000..595dad2
--- /dev/null
+++ b/web/src/client/player-filter-panel.ts
@@ -0,0 +1,169 @@
+import { REALM_DATA } from "../lib/realms";
+import { buildPlayersLeaderboardURL } from 
"../lib/utils.ts";
+
+class PlayerFilterPanelClient {
+  private container: HTMLElement;
+  private regionSelect: HTMLSelectElement;
+  private realmSelect: HTMLSelectElement;
+  private classSelect: HTMLSelectElement;
+
+  constructor(container: HTMLElement) {
+    this.container = container;
+    this.regionSelect = container.querySele
ctor(
+      "#player-region-select",
+    ) as HTMLSelectElement;
+    this.realmSelect = container.querySelec
tor(
+      "#player-realm-select",
+    ) as HTMLSelectElement;
+    this.classSelect = container.querySelec
tor(
+      "#player-class-select",
+    ) as HTMLSelectElement;
+
+    this.bindEvents();
+    this.updateRealmOptions();
+    this.initFromURLOrDataset();
+  }
+
+  private bindEvents() {
+    this.regionSelect.addEventListener("cha
nge", () => {
+      this.updateRealmOptions();
+      this.notifyLeaderboard(1);
+    });
+    this.realmSelect.addEventListener("chan
ge", () => this.notifyLeaderboard(1));
+    this.classSelect.addEventListener("chan
ge", () => this.notifyLeaderboard(1));
+  }
+
+  private updateRealmOptions() {
+    const region = this.regionSelect.value;
+    const options = this.realmSelect.queryS
electorAll("option[data-region]");
+    options.forEach((opt) => ((opt as HTMLO
ptionElement).style.display = "none"));
+    if (region === "global") {
+      this.realmSelect.disabled = true;
+      this.realmSelect.value = "all";
+    } else {
+      this.realmSelect.disabled = false;
+      const regionOptions = this.realmSelec
t.querySelectorAll(
+        `option[data-region="${region}"]`,
+      );
+      regionOptions.forEach(
+        (opt) => ((opt as HTMLOptionElement
).style.display = "block"),
+      );
+      // Ensure valid selection
+      const current = this.realmSelect.valu
e;
+      if (current && current !== "all") {
+        const exists = Array.from(regionOpt
ions).some(
+          (opt) => (opt as HTMLOptionElemen
t).value === current,
+        );
+        if (!exists) this.realmSelect.value
 = "all";
+      } else {
+        this.realmSelect.value = "all";
+      }
+    }
+  }
+
+  private initFromURLOrDataset() {
+    const { scope, region, realm, klass, pa
ge } = this.parsePlayersPath();
+    this.regionSelect.value = region || "gl
obal";
+    this.updateRealmOptions();
+    if (region && region !== "global") this
.realmSelect.value = realm || "all";
+    this.classSelect.value = klass || "";
+    // Only notify/load if players section 
is currently active; otherwise wait for tab 
switch
+    if (this.isPlayersSectionActive()) {
+      this.notifyLeaderboard(page || 1);
+    }
+  }
+
+  private notifyLeaderboard(page: number) {
+    const region = this.regionSelect.value;
+    const realmVal = this.realmSelect.value
 || "all";
+    const classKey = this.classSelect.value
 || "";
+
+    const scope = region === "global" ? "gl
obal" : realmVal !== "all" ? "realm" : "regi
onal";
+    const realmSlug = scope === "realm" ? r
ealmVal : undefined;
+
+    const w: any = window as any;
+    const pl = w.playerLeaderboard;
+    if (pl && typeof pl.setFilters === "fun
ction") {
+      pl.setFilters({ scope, region: region
 === "global" ? undefined : region, realmSlu
g, classKey, page });
+    }
+
+    const newPath = buildPlayersLeaderboard
URL(scope, {
+      region: region === "global" ? undefin
ed : region,
+      realmSlug,
+      classKey,
+      page,
+    });
+    try { sessionStorage.setItem("lastPlaye
rsPath", newPath); } catch {}
+    window.history.pushState({}, "", newPat
h);
+  }
+
+  private isPlayersSectionActive(): boolean
 {
+    try {
+      const section = this.container.closes
t("#players-section") as
+        | HTMLElement
+        | null;
+      return !!section && section.classList
.contains("active");
+    } catch {
+      return false;
+    }
+  }
+
+  private parsePlayersPath(): { scope: "glo
bal" | "regional" | "realm"; region?: string
; realm?: string; klass?: string; page?: num
ber } {
+    const path = window.location.pathname.r
eplace(/\/+$/, "");
+    const parts = path.split("/").filter(Bo
olean);
+    // Expect: ["challenge-mode","players",
 ...]
+    if (parts.length >= 2 && parts[0] === "
challenge-mode" && parts[1] === "players") {
+      const rest = parts.slice(2);
+      if (rest.length === 0) {
+        return { scope: "global", klass: un
defined, page: this.readPageParam() };
+      }
+      if (rest[0] === "global") {
+        const klass = rest[1] || "";
+        return { scope: "global", klass, pa
ge: this.readPageParam() };
+      }
+      const region = rest[0];
+      if (rest.length === 1) {
+        return { scope: "regional", region,
 page: this.readPageParam() };
+      }
+      // Two or more segments: could be reg
ion/class or region/realm[/class]
+      if (rest.length === 2) {
+        // Ambiguous; assume region/class i
f second looks like a class (letters/undersc
ores)
+        const second = rest[1];
+        if (/^[a-z_]+$/.test(second)) {
+          return { scope: "regional", regio
n, klass: second, page: this.readPageParam()
 };
+        }
+        return { scope: "realm", region, re
alm: second, page: this.readPageParam() };
+      }
+      // region/realm/class
+      return { scope: "realm", region, real
m: rest[1], klass: rest[2], page: this.readP
ageParam() };
+    }
+    // Fallback: derive from query params a
s legacy
+    const url = new URL(window.location.hre
f);
+    const region = url.searchParams.get("re
gion") || undefined;
+    const realm = url.searchParams.get("rea
lm") || undefined;
+    const klass = url.searchParams.get("cla
ss") || undefined;
+    const page = this.readPageParam();
+    if (!region) return { scope: "global", 
klass, page };
+    if (realm) return { scope: "realm", reg
ion, realm, klass, page };
+    return { scope: "regional", region, kla
ss, page };
+  }
+
+  private readPageParam(): number | undefin
ed {
+    const url = new URL(window.location.hre
f);
+    const page = parseInt(url.searchParams.
get("page") || "1", 10);
+    return isNaN(page) || page <= 1 ? undef
ined : page;
+  }
+}
+
+function initPlayerFilterPanel() {
+  const el = document.querySelector(
+    ".player-filter-panel",
+  ) as HTMLElement | null;
+  if (el) new PlayerFilterPanelClient(el);
+}
+
+if (document.readyState === "loading") {
+  document.addEventListener("DOMContentLoad
ed", initPlayerFilterPanel);
+} else {
+  initPlayerFilterPanel();
+}
 

diff --git a/web/src/client/player-leaderboard.ts b/web/src/client/player-leaderbo
ard.ts
index 0280c1a..62e00c3 100644
--- a/web/src/client/player-leaderboard.ts
+++ b/web/src/client/player-leaderboard.ts
@@ -1,4 +1,4 @@
-import { buildPlayerProfileURL } from "../lib/utils.ts";
+import { buildPlayerProfileURL, buildStaticPlayerLeaderboardPath } from "../lib/u
tils.ts";
 import {
   getClassColor,
   getSpecInfo,
@@ -11,8 +11,10 @@ class PlayerLeaderboardClient {
   private error: HTMLElement | null;
   private loading: HTMLElement | null;
   private currentPage = 1;
-  private scope: "global" | "regional" = "global";
+  private scope: "global" | "regional" | "realm" = "global";
   private region: string | null = null;
+  private realmSlug: string | null = null;
+  private classKey: string | null = null;
 
   constructor(container: HTMLElement) {
     this.container = container;
@@ -27,19 +29,80 @@ class PlayerLeaderboardClient {
     ) as HTMLElement | null;
 
     this.bindPagination();
-    // initial load
-    this.refreshCurrentPage();
+    // initial load only if players section is active; otherwise the layout will 
trigger refresh on tab switch
+    if (this.isPlayersSectionActive()) {
+      this.refreshCurrentPage();
+    }
+  }
+
+  private isPlayersSectionActive(): boolean {
+    try {
+      const section = this.container.closest("#players-section") as
+        | HTMLElement
+        | null;
+      return !!section && section.classList.contains("active");
+    } catch {
+      return false;
+    }
   }
 
   public refreshCurrentPage() {
-    const urlParams = new URLSearchParams(window.location.search);
-    const pageParam = urlParams.get("page");
-    this.currentPage = pageParam ? Math.max(1, parseInt(pageParam)) : 1;
+    const { scope, region, realm, klass, page } = this.parsePlayersPath();
+    this.currentPage = page || 1;
+    this.scope = scope;
+    this.region = region || null;
+    this.realmSlug = realm || null;
+    this.classKey = klass || null;
     this.load(this.currentPage).catch((e) =>
       console.error("Player leaderboard load error:", e),
     );
   }
 
+  private parsePlayersPath(): { scope: "global" | "regional" | "realm"; region?: 
string; realm?: string; klass?: string; page?: number } {
+    const path = window.location.pathname.replace(/\/+$/, "");
+    const parts = path.split("/").filter(Boolean);
+    const url = new URL(window.location.href);
+    const page = parseInt(url.searchParams.get("page") || "1", 10);
+    const pageNum = isNaN(page) || page <= 1 ? undefined : page;
+    if (parts.length >= 2 && parts[0] === "challenge-mode" && parts[1] === "playe
rs") {
+      const rest = parts.slice(2);
+      if (rest.length === 0) return { scope: "global", page: pageNum };
+      if (rest[0] === "global") return { scope: "global", klass: rest[1], page: p
ageNum };
+      const region = rest[0];
+      if (rest.length === 1) return { scope: "regional", region, page: pageNum };
+      if (rest.length === 2) {
+        const second = rest[1];
+        if (/^[a-z_]+$/.test(second)) return { scope: "regional", region, klass: 
second, page: pageNum };
+        return { scope: "realm", region, realm: second, page: pageNum };
+      }
+      return { scope: "realm", region, realm: rest[1], klass: rest[2], page: page
Num };
+    }
+    // Fallback to query params
+    const regionQ = url.searchParams.get("region") || undefined;
+    const realmQ = url.searchParams.get("realm") || undefined;
+    const klassQ = url.searchParams.get("class") || undefined;
+    if (!regionQ) return { scope: "global", klass: klassQ, page: pageNum };
+    if (realmQ) return { scope: "realm", region: regionQ, realm: realmQ, klass: k
lassQ, page: pageNum };
+    return { scope: "regional", region: regionQ, klass: klassQ, page: pageNum };
+  }
+
+  public setFilters(opts: {
+    scope: "global" | "regional" | "realm";
+    region?: string;
+    realmSlug?: string;
+    classKey?: string;
+    page?: number;
+  }) {
+    this.scope = opts.scope;
+    this.region = opts.region || null;
+    this.realmSlug = opts.realmSlug || null;
+    this.classKey = opts.classKey || null;
+    this.currentPage = Math.max(1, opts.page || 1);
+    this.load(this.currentPage).catch((e) =>
+      console.error("Player leaderboard setFilters error:", e),
+    );
+  }
+
   private bindPagination() {
     const prevBtn = this.container.querySelector(
       "#prev-page",
@@ -50,25 +113,22 @@ class PlayerLeaderboardClient {
     prevBtn?.addEventListener("click", () => {
       if (this.currentPage > 1) {
         this.currentPage--;
-        this.load(this.currentPage, { preserveScroll: false });
+        this.load(this.currentPage);
       }
     });
     nextBtn?.addEventListener("click", () => {
       this.currentPage++;
-      this.load(this.currentPage, { preserveScroll: false });
+      this.load(this.currentPage);
     });
   }
 
   private async fetchData(page: number) {
-    let url: string;
-
-    if (this.scope === "global") {
-      url = `/api/leaderboard/players/global/${page}.json`;
-    } else if (this.region) {
-      url = `/api/leaderboard/players/${this.region}/${page}.json`;
-    } else {
-      url = `/api/leaderboard/players/global/${page}.json`;
-    }
+    const url = buildStaticPlayerLeaderboardPath(
+      this.scope,
+      this.region || undefined,
+      page,
+      { realmSlug: this.realmSlug || undefined, classKey: this.classKey || undefi
ned },
+    );
 
     console.log("[PlayerLeaderboard] fetching:", url);
     const res = await fetch(url);
@@ -256,6 +316,13 @@ class PlayerLeaderboardClient {
     try {
       const data = await this.fetchData(page);
       this.render(data);
+      // Update URL to reflect current filters + page
+      try {
+        // Keep current path; only update page param to reflect pagination
+        const url = new URL(window.location.href);
+        if (this.currentPage > 1) url.searchParams.set("page", String(this.curren
tPage)); else url.searchParams.delete("page");
+        window.history.replaceState({}, "", url.toString());
+      } catch {}
     } catch (e: any) {
       this.showError(String(e?.message || e));
     } finally {

diff --git a/web/src/client/player-leaderboa
rd.ts b/web/src/client/player-leaderboard.ts
index 0280c1a..62e00c3 100644
--- a/web/src/client/player-leaderboard.ts
+++ b/web/src/client/player-leaderboard.ts
@@ -1,4 +1,4 @@
-import { buildPlayerProfileURL } from "../l
ib/utils.ts";
+import { buildPlayerProfileURL, buildStatic
PlayerLeaderboardPath } from "../lib/utils.t
s";
 import {
   getClassColor,
   getSpecInfo,
@@ -11,8 +11,10 @@ class PlayerLeaderboardCl
ient {
   private error: HTMLElement | null;
   private loading: HTMLElement | null;
   private currentPage = 1;
-  private scope: "global" | "regional" = "g
lobal";
+  private scope: "global" | "regional" | "r
ealm" = "global";
   private region: string | null = null;
+  private realmSlug: string | null = null;
+  private classKey: string | null = null;
 
   constructor(container: HTMLElement) {
     this.container = container;
@@ -27,19 +29,80 @@ class PlayerLeaderboardC
lient {
     ) as HTMLElement | null;
 
     this.bindPagination();
-    // initial load
-    this.refreshCurrentPage();
+    // initial load only if players section
 is active; otherwise the layout will trigge
r refresh on tab switch
+    if (this.isPlayersSectionActive()) {
+      this.refreshCurrentPage();
+    }
+  }
+
+  private isPlayersSectionActive(): boolean
 {
+    try {
+      const section = this.container.closes
t("#players-section") as
+        | HTMLElement
+        | null;
+      return !!section && section.classList
.contains("active");
+    } catch {
+      return false;
+    }
   }
 
   public refreshCurrentPage() {
-    const urlParams = new URLSearchParams(w
indow.location.search);
-    const pageParam = urlParams.get("page")
;
-    this.currentPage = pageParam ? Math.max
(1, parseInt(pageParam)) : 1;
+    const { scope, region, realm, klass, pa
ge } = this.parsePlayersPath();
+    this.currentPage = page || 1;
+    this.scope = scope;
+    this.region = region || null;
+    this.realmSlug = realm || null;
+    this.classKey = klass || null;
     this.load(this.currentPage).catch((e) =
>
       console.error("Player leaderboard loa
d error:", e),
     );
   }
 
+  private parsePlayersPath(): { scope: "glo
bal" | "regional" | "realm"; region?: string
; realm?: string; klass?: string; page?: num
ber } {
+    const path = window.location.pathname.r
eplace(/\/+$/, "");
+    const parts = path.split("/").filter(Bo
olean);
+    const url = new URL(window.location.hre
f);
+    const page = parseInt(url.searchParams.
get("page") || "1", 10);
+    const pageNum = isNaN(page) || page <= 
1 ? undefined : page;
+    if (parts.length >= 2 && parts[0] === "
challenge-mode" && parts[1] === "players") {
+      const rest = parts.slice(2);
+      if (rest.length === 0) return { scope
: "global", page: pageNum };
+      if (rest[0] === "global") return { sc
ope: "global", klass: rest[1], page: pageNum
 };
+      const region = rest[0];
+      if (rest.length === 1) return { scope
: "regional", region, page: pageNum };
+      if (rest.length === 2) {
+        const second = rest[1];
+        if (/^[a-z_]+$/.test(second)) retur
n { scope: "regional", region, klass: second
, page: pageNum };
+        return { scope: "realm", region, re
alm: second, page: pageNum };
+      }
+      return { scope: "realm", region, real
m: rest[1], klass: rest[2], page: pageNum };
+    }
+    // Fallback to query params
+    const regionQ = url.searchParams.get("r
egion") || undefined;
+    const realmQ = url.searchParams.get("re
alm") || undefined;
+    const klassQ = url.searchParams.get("cl
ass") || undefined;
+    if (!regionQ) return { scope: "global",
 klass: klassQ, page: pageNum };
+    if (realmQ) return { scope: "realm", re
gion: regionQ, realm: realmQ, klass: klassQ,
 page: pageNum };
+    return { scope: "regional", region: reg
ionQ, klass: klassQ, page: pageNum };
+  }
+
+  public setFilters(opts: {
+    scope: "global" | "regional" | "realm";
+    region?: string;
+    realmSlug?: string;
+    classKey?: string;
+    page?: number;
+  }) {
+    this.scope = opts.scope;
+    this.region = opts.region || null;
+    this.realmSlug = opts.realmSlug || null
;
+    this.classKey = opts.classKey || null;
+    this.currentPage = Math.max(1, opts.pag
e || 1);
+    this.load(this.currentPage).catch((e) =
>
+      console.error("Player leaderboard set
Filters error:", e),
+    );
+  }
+
   private bindPagination() {
     const prevBtn = this.container.querySel
ector(
       "#prev-page",
@@ -50,25 +113,22 @@ class PlayerLeaderboard
Client {
     prevBtn?.addEventListener("click", () =
> {
       if (this.currentPage > 1) {
         this.currentPage--;
-        this.load(this.currentPage, { prese
rveScroll: false });
+        this.load(this.currentPage);
       }
     });
     nextBtn?.addEventListener("click", () =
> {
       this.currentPage++;
-      this.load(this.currentPage, { preserv
eScroll: false });
+      this.load(this.currentPage);
     });
   }
 
   private async fetchData(page: number) {
-    let url: string;
-
-    if (this.scope === "global") {
-      url = `/api/leaderboard/players/globa
l/${page}.json`;
-    } else if (this.region) {
-      url = `/api/leaderboard/players/${thi
s.region}/${page}.json`;
-    } else {
-      url = `/api/leaderboard/players/globa
l/${page}.json`;
-    }
+    const url = buildStaticPlayerLeaderboar
dPath(
+      this.scope,
+      this.region || undefined,
+      page,
+      { realmSlug: this.realmSlug || undefi
ned, classKey: this.classKey || undefined },
+    );
 
     console.log("[PlayerLeaderboard] fetchi
ng:", url);
     const res = await fetch(url);
@@ -256,6 +316,13 @@ class PlayerLeaderboard
Client {
     try {
       const data = await this.fetchData(pag
e);
       this.render(data);
+      // Update URL to reflect current filt
ers + page
+      try {
+        // Keep current path; only update p
age param to reflect pagination
+        const url = new URL(window.location
.href);
+        if (this.currentPage > 1) url.searc
hParams.set("page", String(this.currentPage)
); else url.searchParams.delete("page");
+        window.history.replaceState({}, "",
 url.toString());
+      } catch {}
     } catch (e: any) {
       this.showError(String(e?.message || e
));
     } finally {
 

diff --git a/web/src/components/islands/PlayerFilterPanel.astro b/web/src/componen
ts/islands/PlayerFilterPanel.astro
new file mode 100644
index 0000000..417247d
--- /dev/null
+++ b/web/src/components/islands/PlayerFilterPanel.astro
@@ -0,0 +1,98 @@
+---
+import { REALM_DATA } from "../../lib/realms";
+import { CLASS_COLORS } from "../../lib/wow-constants";
+
+export interface Props {
+  initialRegion?: string;
+  initialRealm?: string;
+  initialClass?: string;
+  class?: string;
+}
+
+const {
+  initialRegion = "global",
+  initialRealm = "",
+  initialClass = "",
+  class: className,
+} = Astro.props;
+---
+
+<div
+  class={`player-filter-panel ${className || ""}`.trim()}
+  data-initial-region={initialRegion}
+  data-initial-realm={initialRealm}
+  data-initial-class={initialClass}
+>
+  <div class="filter-row">
+    <div class="filter-group">
+      <label for="player-region-select" class="filter-label">Region</label>
+      <select id="player-region-select" class="filter-select">
+        <option value="global" selected={initialRegion === "global"}>Global</opti
on>
+        <option value="us" selected={initialRegion === "us"}>US</option>
+        <option value="eu" selected={initialRegion === "eu"}>EU</option>
+        <option value="kr" selected={initialRegion === "kr"}>KR</option>
+      </select>
+    </div>
+
+    <div class="filter-group">
+      <label for="player-realm-select" class="filter-label">Realm</label>
+      <select id="player-realm-select" class="filter-select" disabled={initialReg
ion === "global"}>
+        <option value="all">All Realms</option>
+        {
+          Object.entries(REALM_DATA.us)
+            .sort((a, b) => a[1].localeCompare(b[1]))
+            .map(([slug, name]) => (
+              <option value={slug} data-region="us" style="display: none;">{name}
</option>
+            ))
+        }
+        {
+          Object.entries(REALM_DATA.eu)
+            .sort((a, b) => a[1].localeCompare(b[1]))
+            .map(([slug, name]) => (
+              <option value={slug} data-region="eu" style="display: none;">{name}
</option>
+            ))
+        }
+        {
+          Object.entries(REALM_DATA.kr)
+            .sort((a, b) => a[1].localeCompare(b[1]))
+            .map(([slug, name]) => (
+              <option value={slug} data-region="kr" style="display: none;">{name}
</option>
+            ))
+        }
+      </select>
+    </div>
+
+    <div class="filter-group">
+      <label for="player-class-select" class="filter-label">Class</label>
+      <select id="player-class-select" class="filter-select">
+        <option value="">All Classes</option>
+        {
+          Object.keys(CLASS_COLORS)
+            .sort()
+            .map((key) => {
+              const label = key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUp
perCase());
+              return <option value={key} selected={initialClass === key}>{label}<
/option>;
+            })
+        }
+      </select>
+    </div>
+  </div>
+</div>
+
+<script src="../../client/player-filter-panel.ts"></script>
+
+<style>
+  .player-filter-panel {
+    background-color: var(--bg-secondary);
+    border-radius: 8px;
+    border: 1px solid var(--border-color);
+    padding: 16px;
+    margin-bottom: 16px;
+  }
+  .filter-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
+  .filter-group { display: flex; flex-direction: column; gap: 6px; }
+  .filter-label { color: var(--text-secondary); font-size: 0.85em; font-weight: 5
00; text-transform: uppercase; }
+  .filter-select { background: var(--bg-primary); color: var(--text-primary); bor
der: 1px solid var(--border-color); border-radius: 4px; padding: 8px 10px; }
+  @media (max-width: 768px) { .filter-row { grid-template-columns: 1fr; } }
+</style>
+

diff --git a/web/src/components/islands/Play
erFilterPanel.astro b/web/src/components/isl
ands/PlayerFilterPanel.astro
new file mode 100644
index 0000000..417247d
--- /dev/null
+++ b/web/src/components/islands/PlayerFilte
rPanel.astro
@@ -0,0 +1,98 @@
+---
+import { REALM_DATA } from "../../lib/realm
s";
+import { CLASS_COLORS } from "../../lib/wow
-constants";
+
+export interface Props {
+  initialRegion?: string;
+  initialRealm?: string;
+  initialClass?: string;
+  class?: string;
+}
+
+const {
+  initialRegion = "global",
+  initialRealm = "",
+  initialClass = "",
+  class: className,
+} = Astro.props;
+---
+
+<div
+  class={`player-filter-panel ${className |
| ""}`.trim()}
+  data-initial-region={initialRegion}
+  data-initial-realm={initialRealm}
+  data-initial-class={initialClass}
+>
+  <div class="filter-row">
+    <div class="filter-group">
+      <label for="player-region-select" cla
ss="filter-label">Region</label>
+      <select id="player-region-select" cla
ss="filter-select">
+        <option value="global" selected={in
itialRegion === "global"}>Global</option>
+        <option value="us" selected={initia
lRegion === "us"}>US</option>
+        <option value="eu" selected={initia
lRegion === "eu"}>EU</option>
+        <option value="kr" selected={initia
lRegion === "kr"}>KR</option>
+      </select>
+    </div>
+
+    <div class="filter-group">
+      <label for="player-realm-select" clas
s="filter-label">Realm</label>
+      <select id="player-realm-select" clas
s="filter-select" disabled={initialRegion ==
= "global"}>
+        <option value="all">All Realms</opt
ion>
+        {
+          Object.entries(REALM_DATA.us)
+            .sort((a, b) => a[1].localeComp
are(b[1]))
+            .map(([slug, name]) => (
+              <option value={slug} data-reg
ion="us" style="display: none;">{name}</opti
on>
+            ))
+        }
+        {
+          Object.entries(REALM_DATA.eu)
+            .sort((a, b) => a[1].localeComp
are(b[1]))
+            .map(([slug, name]) => (
+              <option value={slug} data-reg
ion="eu" style="display: none;">{name}</opti
on>
+            ))
+        }
+        {
+          Object.entries(REALM_DATA.kr)
+            .sort((a, b) => a[1].localeComp
are(b[1]))
+            .map(([slug, name]) => (
+              <option value={slug} data-reg
ion="kr" style="display: none;">{name}</opti
on>
+            ))
+        }
+      </select>
+    </div>
+
+    <div class="filter-group">
+      <label for="player-class-select" clas
s="filter-label">Class</label>
+      <select id="player-class-select" clas
s="filter-select">
+        <option value="">All Classes</optio
n>
+        {
+          Object.keys(CLASS_COLORS)
+            .sort()
+            .map((key) => {
+              const label = key.replace(/_/
g, " ").replace(/\b\w/g, (l) => l.toUpperCas
e());
+              return <option value={key} se
lected={initialClass === key}>{label}</optio
n>;
+            })
+        }
+      </select>
+    </div>
+  </div>
+</div>
+
+<script src="../../client/player-filter-pan
el.ts"></script>
+
+<style>
+  .player-filter-panel {
+    background-color: var(--bg-secondary);
+    border-radius: 8px;
+    border: 1px solid var(--border-color);
+    padding: 16px;
+    margin-bottom: 16px;
+  }
+  .filter-row { display: grid; grid-templat
e-columns: 1fr 1fr 1fr; gap: 12px; }
+  .filter-group { display: flex; flex-direc
tion: column; gap: 6px; }
+  .filter-label { color: var(--text-seconda
ry); font-size: 0.85em; font-weight: 500; te
xt-transform: uppercase; }
+  .filter-select { background: var(--bg-pri
mary); color: var(--text-primary); border: 1
px solid var(--border-color); border-radius:
 4px; padding: 8px 10px; }
+  @media (max-width: 768px) { .filter-row {
 grid-template-columns: 1fr; } }
+</style>
+
 

diff --git a/web/src/layouts/ModernChallengeModeLayout.astro b/web/src/layouts/Mod
ernChallengeModeLayout.astro
index 177da73..0e4adc0 100644
--- a/web/src/layouts/ModernChallengeModeLayout.astro
+++ b/web/src/layouts/ModernChallengeModeLayout.astro
@@ -6,6 +6,7 @@ import LeaderboardTable from "../components/islands/LeaderboardTab
le.astro";
 import { getDungeonName } from "../lib/wow-constants.ts";
 import { buildStaticLeaderboardPath } from "../lib/utils.ts";
 import PlayerLeaderboard from "../components/islands/PlayerLeaderboard.astro";
+import PlayerFilterPanel from "../components/islands/PlayerFilterPanel.astro";
 import "../styles/components.scss";
 
 export interface Props {
@@ -136,6 +137,7 @@ if (
         </div>
 
         <div class="player-leaderboard-wrapper">
+          <PlayerFilterPanel />
           <PlayerLeaderboard />
         </div>
       </section>
@@ -158,13 +160,13 @@ if (
 
         navTabs.forEach((tab) => {
           tab.addEventListener("click", (e) => {
-            const button = e.target;
-            const type =
-              button && button.dataset ? button.dataset.type : undefined;
+            e.preventDefault();
+            const el = e.currentTarget;
+            const type = el && el.getAttribute
+              ? el.getAttribute("data-type")
+              : undefined;
             console.log("[INFO] Tab clicked:", type);
-            if (type) {
-              this.switchTab(type);
-            }
+            if (type) this.switchTab(type);
           });
         });
       }
@@ -202,27 +204,43 @@ if (
         // update URL only if not responding to browser navigation
         if (updateHistory) {
           if (type === "players") {
-            // always update to players URL when switching to players tab
-            const newURL = "/challenge-mode/players";
-            console.log("[INFO] Updating URL to players:", newURL);
+            const current = window.location.pathname + window.location.search;
+            let newURL = current;
+            if (!current.startsWith("/challenge-mode/players")) {
+              // Try to restore last players URL from sessionStorage, else derive
 from current filter state
+              const last = sessionStorage.getItem("lastPlayersPath");
+              if (last && last.startsWith("/challenge-mode/players")) {
+                newURL = last;
+              } else {
+                // Build from client state if available
+                const w = window;
+                const pl = (w).playerLeaderboard;
+                if (pl) {
+                  const scope = pl["scope"] || "global";
+                  const region = (pl["region"] || "").toLowerCase();
+                  const realm = (pl["realmSlug"] || "").toLowerCase();
+                  const cls = (pl["classKey"] || "").toLowerCase();
+                  if (scope === "global") newURL = `/challenge-mode/players/globa
l${cls ? "/" + cls : ""}`;
+                  else if (scope === "regional") newURL = `/challenge-mode/player
s/${region}${cls ? "/" + cls : ""}`;
+                  else newURL = `/challenge-mode/players/${region}/${realm}${cls 
? "/" + cls : ""}`;
+                } else {
+                  newURL = "/challenge-mode/players";
+                }
+              }
+            }
+            console.log("[INFO] Switching to players URL:", newURL);
             window.history.pushState({}, "", newURL);
           } else if (type === "dungeon") {
-            // for dungeon tab, only update if we're on a generic URL
-            const currentPath = window.location.pathname;
-            if (currentPath === "/challenge-mode/players") {
-              // coming from players tab, go to generic dungeon URL
+            // If currently on any players path, remember it and go to base dunge
on URL
+            const currentPath = window.location.pathname + window.location.search
;
+            if (currentPath.startsWith("/challenge-mode/players")) {
+              try { sessionStorage.setItem("lastPlayersPath", currentPath); } cat
ch {}
               const newURL = "/challenge-mode";
-              console.log(
-                "[INFO] Updating URL from players to dungeon:",
-                newURL,
-              );
+              console.log("[INFO] Switching to dungeon URL:", newURL);
               window.history.pushState({}, "", newURL);
             } else {
-              // preserve specific dungeon URLs
-              console.log(
-                "[INFO] Preserving specific dungeon URL:",
-                currentPath,
-              );
+              // preserve specific dungeon URLs if already there
+              console.log("[INFO] Preserving specific dungeon URL:", currentPath)
;
             }
           }
         } else {

diff --git a/web/src/layouts/ModernChallenge
ModeLayout.astro b/web/src/layouts/ModernCha
llengeModeLayout.astro
index 177da73..0e4adc0 100644
--- a/web/src/layouts/ModernChallengeModeLay
out.astro
+++ b/web/src/layouts/ModernChallengeModeLay
out.astro
@@ -6,6 +6,7 @@ import LeaderboardTable from
 "../components/islands/LeaderboardTable.ast
ro";
 import { getDungeonName } from "../lib/wow-
constants.ts";
 import { buildStaticLeaderboardPath } from 
"../lib/utils.ts";
 import PlayerLeaderboard from "../component
s/islands/PlayerLeaderboard.astro";
+import PlayerFilterPanel from "../component
s/islands/PlayerFilterPanel.astro";
 import "../styles/components.scss";
 
 export interface Props {
@@ -136,6 +137,7 @@ if (
         </div>
 
         <div class="player-leaderboard-wrap
per">
+          <PlayerFilterPanel />
           <PlayerLeaderboard />
         </div>
       </section>
@@ -158,13 +160,13 @@ if (
 
         navTabs.forEach((tab) => {
           tab.addEventListener("click", (e)
 => {
-            const button = e.target;
-            const type =
-              button && button.dataset ? bu
tton.dataset.type : undefined;
+            e.preventDefault();
+            const el = e.currentTarget;
+            const type = el && el.getAttrib
ute
+              ? el.getAttribute("data-type"
)
+              : undefined;
             console.log("[INFO] Tab clicked
:", type);
-            if (type) {
-              this.switchTab(type);
-            }
+            if (type) this.switchTab(type);
           });
         });
       }
@@ -202,27 +204,43 @@ if (
         // update URL only if not respondin
g to browser navigation
         if (updateHistory) {
           if (type === "players") {
-            // always update to players URL
 when switching to players tab
-            const newURL = "/challenge-mode
/players";
-            console.log("[INFO] Updating UR
L to players:", newURL);
+            const current = window.location
.pathname + window.location.search;
+            let newURL = current;
+            if (!current.startsWith("/chall
enge-mode/players")) {
+              // Try to restore last player
s URL from sessionStorage, else derive from 
current filter state
+              const last = sessionStorage.g
etItem("lastPlayersPath");
+              if (last && last.startsWith("
/challenge-mode/players")) {
+                newURL = last;
+              } else {
+                // Build from client state 
if available
+                const w = window;
+                const pl = (w).playerLeader
board;
+                if (pl) {
+                  const scope = pl["scope"]
 || "global";
+                  const region = (pl["regio
n"] || "").toLowerCase();
+                  const realm = (pl["realmS
lug"] || "").toLowerCase();
+                  const cls = (pl["classKey
"] || "").toLowerCase();
+                  if (scope === "global") n
ewURL = `/challenge-mode/players/global${cls
 ? "/" + cls : ""}`;
+                  else if (scope === "regio
nal") newURL = `/challenge-mode/players/${re
gion}${cls ? "/" + cls : ""}`;
+                  else newURL = `/challenge
-mode/players/${region}/${realm}${cls ? "/" 
+ cls : ""}`;
+                } else {
+                  newURL = "/challenge-mode
/players";
+                }
+              }
+            }
+            console.log("[INFO] Switching t
o players URL:", newURL);
             window.history.pushState({}, ""
, newURL);
           } else if (type === "dungeon") {
-            // for dungeon tab, only update
 if we're on a generic URL
-            const currentPath = window.loca
tion.pathname;
-            if (currentPath === "/challenge
-mode/players") {
-              // coming from players tab, g
o to generic dungeon URL
+            // If currently on any players 
path, remember it and go to base dungeon URL
+            const currentPath = window.loca
tion.pathname + window.location.search;
+            if (currentPath.startsWith("/ch
allenge-mode/players")) {
+              try { sessionStorage.setItem(
"lastPlayersPath", currentPath); } catch {}
               const newURL = "/challenge-mo
de";
-              console.log(
-                "[INFO] Updating URL from p
layers to dungeon:",
-                newURL,
-              );
+              console.log("[INFO] Switching
 to dungeon URL:", newURL);
               window.history.pushState({}, 
"", newURL);
             } else {
-              // preserve specific dungeon 
URLs
-              console.log(
-                "[INFO] Preserving specific
 dungeon URL:",
-                currentPath,
-              );
+              // preserve specific dungeon 
URLs if already there
+              console.log("[INFO] Preservin
g specific dungeon URL:", currentPath);
             }
           }
         } else {
 

diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index 1622b61..5a7fae7 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -60,12 +60,13 @@ export async function fetchRealmLeaderboard(
 }
 
 export async function fetchPlayerLeaderboard(
-  scope: string = "global",
+  scope: "global" | "regional" | "realm" = "global",
   region?: string,
   page: number = 1,
   pageSize: number = 25,
+  opts?: { realmSlug?: string; classKey?: string },
 ): Promise<any> {
-  const url = `${API_BASE}${buildStaticPlayerLeaderboardPath(scope, region, page)
}`;
+  const url = `${API_BASE}${buildStaticPlayerLeaderboardPath(scope, region, page,
 opts)}`;
   console.log("Fetching player leaderboard:", url);
   return apiRequest(url);
 }

diff --git a/web/src/lib/api.ts b/web/src/li
b/api.ts
index 1622b61..5a7fae7 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -60,12 +60,13 @@ export async function fe
tchRealmLeaderboard(
 }
 
 export async function fetchPlayerLeaderboar
d(
-  scope: string = "global",
+  scope: "global" | "regional" | "realm" = 
"global",
   region?: string,
   page: number = 1,
   pageSize: number = 25,
+  opts?: { realmSlug?: string; classKey?: s
tring },
 ): Promise<any> {
-  const url = `${API_BASE}${buildStaticPlay
erLeaderboardPath(scope, region, page)}`;
+  const url = `${API_BASE}${buildStaticPlay
erLeaderboardPath(scope, region, page, opts)
}`;
   console.log("Fetching player leaderboard:
", url);
   return apiRequest(url);
 }
 

diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
index 37e1ed8..7b332ce 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -172,6 +172,30 @@ export function buildPlayerProfileURL(
   return `/player/${region}/${realmSlug}/${playerName.toLowerCase()}`;
 }
 
+// Build Players tab URL path from filters
+export function buildPlayersLeaderboardURL(
+  scope: "global" | "regional" | "realm",
+  opts?: { region?: string; realmSlug?: string; classKey?: string; page?: number 
},
+): string {
+  const cls = (opts?.classKey || "").toLowerCase();
+  const reg = (opts?.region || "").toLowerCase();
+  const realm = (opts?.realmSlug || "").toLowerCase();
+  let path = "/challenge-mode/players";
+  if (scope === "global") {
+    if (cls) path += `/global/${cls}`; else path += `/global`;
+  } else if (scope === "regional") {
+    if (reg) path += `/${reg}`; else path += `/us`;
+    if (cls) path += `/${cls}`;
+  } else if (scope === "realm") {
+    if (reg) path += `/${reg}`;
+    if (realm) path += `/${realm}`;
+    if (cls) path += `/${cls}`;
+  }
+  const page = opts?.page || 1;
+  if (page > 1) path += `?page=${page}`;
+  return path;
+}
+
 // Form utilities
 export function getFormValue(elementId: string): string {
   const element = document.getElementById(elementId) as
@@ -291,15 +315,39 @@ export function buildStaticLeaderboardPath(
 }
 
 export function buildStaticPlayerLeaderboardPath(
-  scope: string,
+  scope: "global" | "regional" | "realm",
   region?: string,
   page: number = 1,
+  opts?: { realmSlug?: string; classKey?: string },
 ): string {
+  const cls = (opts?.classKey || "").toLowerCase();
+  const realm = (opts?.realmSlug || "").toLowerCase();
+
+  // Class-filtered variants
+  if (cls) {
+    if (scope === "global") {
+      return `/api/leaderboard/players/class/${cls}/global/${page}.json`;
+    }
+    if (scope === "regional" && region) {
+      return `/api/leaderboard/players/class/${cls}/regional/${region}/${page}.js
on`;
+    }
+    if (scope === "realm" && region && realm) {
+      return `/api/leaderboard/players/class/${cls}/realm/${region}/${realm}/${pa
ge}.json`;
+    }
+  }
+
+  // Unfiltered
+  if (scope === "global") {
+    return `/api/leaderboard/players/global/${page}.json`;
+  }
   if (scope === "regional" && region) {
     return `/api/leaderboard/players/regional/${region}/${page}.json`;
-  } else {
-    return `/api/leaderboard/players/global/${page}.json`;
   }
+  if (scope === "realm" && region && realm) {
+    return `/api/leaderboard/players/realm/${region}/${realm}/${page}.json`;
+  }
+  // Fallback to global
+  return `/api/leaderboard/players/global/${page}.json`;
 }
 
 export function buildStaticPlayerProfilePath(

diff --git a/web/src/lib/utils.ts b/web/src/
lib/utils.ts
index 37e1ed8..7b332ce 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -172,6 +172,30 @@ export function buildPl
ayerProfileURL(
   return `/player/${region}/${realmSlug}/${
playerName.toLowerCase()}`;
 }
 
+// Build Players tab URL path from filters
+export function buildPlayersLeaderboardURL(
+  scope: "global" | "regional" | "realm",
+  opts?: { region?: string; realmSlug?: str
ing; classKey?: string; page?: number },
+): string {
+  const cls = (opts?.classKey || "").toLowe
rCase();
+  const reg = (opts?.region || "").toLowerC
ase();
+  const realm = (opts?.realmSlug || "").toL
owerCase();
+  let path = "/challenge-mode/players";
+  if (scope === "global") {
+    if (cls) path += `/global/${cls}`; else
 path += `/global`;
+  } else if (scope === "regional") {
+    if (reg) path += `/${reg}`; else path +
= `/us`;
+    if (cls) path += `/${cls}`;
+  } else if (scope === "realm") {
+    if (reg) path += `/${reg}`;
+    if (realm) path += `/${realm}`;
+    if (cls) path += `/${cls}`;
+  }
+  const page = opts?.page || 1;
+  if (page > 1) path += `?page=${page}`;
+  return path;
+}
+
 // Form utilities
 export function getFormValue(elementId: str
ing): string {
   const element = document.getElementById(e
lementId) as
@@ -291,15 +315,39 @@ export function buildS
taticLeaderboardPath(
 }
 
 export function buildStaticPlayerLeaderboar
dPath(
-  scope: string,
+  scope: "global" | "regional" | "realm",
   region?: string,
   page: number = 1,
+  opts?: { realmSlug?: string; classKey?: s
tring },
 ): string {
+  const cls = (opts?.classKey || "").toLowe
rCase();
+  const realm = (opts?.realmSlug || "").toL
owerCase();
+
+  // Class-filtered variants
+  if (cls) {
+    if (scope === "global") {
+      return `/api/leaderboard/players/clas
s/${cls}/global/${page}.json`;
+    }
+    if (scope === "regional" && region) {
+      return `/api/leaderboard/players/clas
s/${cls}/regional/${region}/${page}.json`;
+    }
+    if (scope === "realm" && region && real
m) {
+      return `/api/leaderboard/players/clas
s/${cls}/realm/${region}/${realm}/${page}.js
on`;
+    }
+  }
+
+  // Unfiltered
+  if (scope === "global") {
+    return `/api/leaderboard/players/global
/${page}.json`;
+  }
   if (scope === "regional" && region) {
     return `/api/leaderboard/players/region
al/${region}/${page}.json`;
-  } else {
-    return `/api/leaderboard/players/global
/${page}.json`;
   }
+  if (scope === "realm" && region && realm)
 {
+    return `/api/leaderboard/players/realm/
${region}/${realm}/${page}.json`;
+  }
+  // Fallback to global
+  return `/api/leaderboard/players/global/$
{page}.json`;
 }
 
 export function buildStaticPlayerProfilePat
h(
 

diff --git a/web/src/pages/challenge-mode/[...slug].astro b/web/src/pages/challeng
e-mode/[...slug].astro
index 4e09506..3b4ab8c 100644
--- a/web/src/pages/challenge-mode/[...slug].astro
+++ b/web/src/pages/challenge-mode/[...slug].astro
@@ -15,11 +15,9 @@ let initialRealm = "";
 let initialDungeon = "2"; // Default dungeon
 
 // Parse different URL patterns
-if (slugParts.length === 1) {
-  // /challenge-mode/players or /challenge-mode/teams
-  if (slugParts[0] === "players" || slugParts[0] === "teams") {
-    initialLeaderboardType = slugParts[0];
-  }
+if (slugParts.length >= 1 && (slugParts[0] === "players" || slugParts[0] === "tea
ms")) {
+  // Any /challenge-mode/players[...] or /challenge-mode/teams[...] path should l
and on that tab
+  initialLeaderboardType = slugParts[0];
 } else if (slugParts.length === 2) {
   // /challenge-mode/global/dungeon-slug
   if (slugParts[0] === "global") {

diff --git a/web/src/pages/challenge-mode/[.
..slug].astro b/web/src/pages/challenge-mode
/[...slug].astro
index 4e09506..3b4ab8c 100644
--- a/web/src/pages/challenge-mode/[...slug]
.astro
+++ b/web/src/pages/challenge-mode/[...slug]
.astro
@@ -15,11 +15,9 @@ let initialRealm = "";
 let initialDungeon = "2"; // Default dungeo
n
 
 // Parse different URL patterns
-if (slugParts.length === 1) {
-  // /challenge-mode/players or /challenge-
mode/teams
-  if (slugParts[0] === "players" || slugPar
ts[0] === "teams") {
-    initialLeaderboardType = slugParts[0];
-  }
+if (slugParts.length >= 1 && (slugParts[0] 
=== "players" || slugParts[0] === "teams")) 
{
+  // Any /challenge-mode/players[...] or /c
hallenge-mode/teams[...] path should land on
 that tab
+  initialLeaderboardType = slugParts[0];
 } else if (slugParts.length === 2) {
   // /challenge-mode/global/dungeon-slug
   if (slugParts[0] === "global") {
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET