Spaces:
Running
Running
Sonja Topf
commited on
Commit
·
d975fec
1
Parent(s):
14f39ac
added delta auc pr columns
Browse files- config/leaderboard.py +3 -2
- frontend/layout.py +65 -20
- frontend/leaderboard.py +34 -18
- frontend/styles.css +37 -55
config/leaderboard.py
CHANGED
|
@@ -2,6 +2,9 @@ from .tasks import get_all_task_keys
|
|
| 2 |
|
| 3 |
# column names
|
| 4 |
TASK_NAMES = get_all_task_keys()
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
METADATA_COLUMN_NAMES = [
|
| 7 |
"Model",
|
|
@@ -16,7 +19,6 @@ METADATA_COLUMN_NAMES = [
|
|
| 16 |
"N-shot", # only if few-shot is yes
|
| 17 |
"Few-shot (y/n)",
|
| 18 |
"ΔAUC-PR",
|
| 19 |
-
"Model Commit Hash",
|
| 20 |
"Organization",
|
| 21 |
"Publication Link",
|
| 22 |
"Pretraining Data", # only if Pretrained (y/n) is yes
|
|
@@ -35,7 +37,6 @@ METADATA_COLUMN_WIDTHS = {
|
|
| 35 |
"Avg. AUC": 130,
|
| 36 |
|
| 37 |
"Zero-shot (y/n)": 140,
|
| 38 |
-
"Model Commit Hash": 100,
|
| 39 |
"N-shot": 140,
|
| 40 |
"Few-shot (y/n)": 140,
|
| 41 |
"ΔAUC-PR": 130,
|
|
|
|
| 2 |
|
| 3 |
# column names
|
| 4 |
TASK_NAMES = get_all_task_keys()
|
| 5 |
+
AUC_TASK_NAMES = ["ROC_AUC_" + name for name in TASK_NAMES]
|
| 6 |
+
AUC_PR_TASK_NAMES = ["ΔAUC-PR_" + name for name in TASK_NAMES]
|
| 7 |
+
TASK_NAMES = AUC_TASK_NAMES + AUC_PR_TASK_NAMES
|
| 8 |
|
| 9 |
METADATA_COLUMN_NAMES = [
|
| 10 |
"Model",
|
|
|
|
| 19 |
"N-shot", # only if few-shot is yes
|
| 20 |
"Few-shot (y/n)",
|
| 21 |
"ΔAUC-PR",
|
|
|
|
| 22 |
"Organization",
|
| 23 |
"Publication Link",
|
| 24 |
"Pretraining Data", # only if Pretrained (y/n) is yes
|
|
|
|
| 37 |
"Avg. AUC": 130,
|
| 38 |
|
| 39 |
"Zero-shot (y/n)": 140,
|
|
|
|
| 40 |
"N-shot": 140,
|
| 41 |
"Few-shot (y/n)": 140,
|
| 42 |
"ΔAUC-PR": 130,
|
frontend/layout.py
CHANGED
|
@@ -20,11 +20,21 @@ def create_leaderboard_tab(refresh_callback: Callable = None) -> gr.TabItem:
|
|
| 20 |
|
| 21 |
with gr.TabItem("Leaderboard", elem_id="leaderboard-tab", id=0) as tab:
|
| 22 |
def apply_filters(search):
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
if search:
|
| 26 |
search = search.lower()
|
| 27 |
-
filtered = filtered[filtered["Model"].str.lower().str.contains(search)]
|
| 28 |
|
| 29 |
# if few_shot is not None:
|
| 30 |
# filtered = filtered[filtered["Few-shot (y/n)"] == few_shot]
|
|
@@ -35,20 +45,57 @@ def create_leaderboard_tab(refresh_callback: Callable = None) -> gr.TabItem:
|
|
| 35 |
return make_html_table(filtered)
|
| 36 |
|
| 37 |
def make_html_table(df: pd.DataFrame) -> str:
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
for _, row in df.iterrows():
|
| 48 |
-
html += "<tr class='leaderboard-row'>"
|
| 49 |
-
html += "".join(f"<td class='leaderboard-cell'>{val}</td>" for val in row)
|
| 50 |
html += "</tr>"
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
html += "</tbody></table></div>"
|
| 53 |
return html
|
| 54 |
|
|
@@ -60,15 +107,13 @@ def create_leaderboard_tab(refresh_callback: Callable = None) -> gr.TabItem:
|
|
| 60 |
|
| 61 |
# Load initial data
|
| 62 |
result_data = refresh_leaderboard() # df
|
| 63 |
-
result_data["Avg. AUC"] = pd.to_numeric(result_data["Avg. AUC"], errors="coerce")
|
| 64 |
-
|
| 65 |
-
# Sort leaderboard by Avg. AUC (descending)
|
| 66 |
-
result_data = result_data.sort_values(by="Avg. AUC", ascending=False, na_position="last")
|
| 67 |
|
| 68 |
-
#
|
| 69 |
-
result_data
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
result_data.columns = result_data.columns.map(str)
|
| 72 |
# Leaderboard at full width
|
| 73 |
search = gr.Textbox(show_label=False, placeholder="Search by model name…", elem_id="leaderboard-search", container=False, max_lines=1, type="text")
|
| 74 |
|
|
|
|
| 20 |
|
| 21 |
with gr.TabItem("Leaderboard", elem_id="leaderboard-tab", id=0) as tab:
|
| 22 |
def apply_filters(search):
|
| 23 |
+
new_data = (
|
| 24 |
+
refresh_callback().reset_index(drop=True)
|
| 25 |
+
if refresh_callback
|
| 26 |
+
else refresh_leaderboard().reset_index(drop=True)
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
new_data[("", "Avg. AUC")] = pd.to_numeric(new_data[("", "Avg. AUC")], errors="coerce")
|
| 30 |
+
new_data = new_data.sort_values(by=("", "Avg. AUC"), ascending=False, na_position="last")
|
| 31 |
+
new_data.insert(0, ("", "Rank"), range(1, len(new_data) + 1))
|
| 32 |
+
|
| 33 |
+
filtered = new_data.copy()
|
| 34 |
|
| 35 |
if search:
|
| 36 |
search = search.lower()
|
| 37 |
+
filtered = filtered[filtered[("", "Model")].str.lower().str.contains(search)]
|
| 38 |
|
| 39 |
# if few_shot is not None:
|
| 40 |
# filtered = filtered[filtered["Few-shot (y/n)"] == few_shot]
|
|
|
|
| 45 |
return make_html_table(filtered)
|
| 46 |
|
| 47 |
def make_html_table(df: pd.DataFrame) -> str:
|
| 48 |
+
html = "<div class='leaderboard-container'><table class='leaderboard-table'>"
|
| 49 |
+
|
| 50 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 51 |
+
html += "<thead>"
|
| 52 |
+
|
| 53 |
+
# --- Top header row ---
|
| 54 |
+
html += "<tr>"
|
| 55 |
+
prev_top = None
|
| 56 |
+
colspan = 0
|
| 57 |
+
|
| 58 |
+
for i, (top, sub) in enumerate(df.columns):
|
| 59 |
+
# Handle blank top headers directly (span both rows)
|
| 60 |
+
if top == "" or top is None:
|
| 61 |
+
# close any previous group first
|
| 62 |
+
if prev_top is not None:
|
| 63 |
+
html += f"<th colspan='{colspan}'>{prev_top}</th>"
|
| 64 |
+
prev_top, colspan = None, 0
|
| 65 |
+
html += f"<th rowspan='2'>{sub}</th>"
|
| 66 |
+
continue
|
| 67 |
+
|
| 68 |
+
# If same group continues, increment colspan
|
| 69 |
+
if top == prev_top:
|
| 70 |
+
colspan += 1
|
| 71 |
+
else:
|
| 72 |
+
# close previous group if exists
|
| 73 |
+
if prev_top is not None:
|
| 74 |
+
html += f"<th colspan='{colspan}'>{prev_top}</th>"
|
| 75 |
+
prev_top = top
|
| 76 |
+
colspan = 1
|
| 77 |
+
|
| 78 |
+
# close final group
|
| 79 |
+
if prev_top is not None:
|
| 80 |
+
html += f"<th colspan='{colspan}'>{prev_top}</th>"
|
| 81 |
|
|
|
|
|
|
|
|
|
|
| 82 |
html += "</tr>"
|
| 83 |
|
| 84 |
+
# --- Second header row ---
|
| 85 |
+
html += "<tr>"
|
| 86 |
+
for top, sub in df.columns:
|
| 87 |
+
if top not in ("", None):
|
| 88 |
+
html += f"<th>{sub}</th>"
|
| 89 |
+
html += "</tr>"
|
| 90 |
+
|
| 91 |
+
html += "</thead>"
|
| 92 |
+
else:
|
| 93 |
+
html += "<thead><tr>" + "".join(f"<th>{col}</th>" for col in df.columns) + "</tr></thead>"
|
| 94 |
+
|
| 95 |
+
# --- Body ---
|
| 96 |
+
html += "<tbody>"
|
| 97 |
+
for _, row in df.iterrows():
|
| 98 |
+
html += "<tr>" + "".join(f"<td>{val}</td>" for val in row) + "</tr>"
|
| 99 |
html += "</tbody></table></div>"
|
| 100 |
return html
|
| 101 |
|
|
|
|
| 107 |
|
| 108 |
# Load initial data
|
| 109 |
result_data = refresh_leaderboard() # df
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
# add rank column
|
| 112 |
+
result_data[("", "Avg. AUC")] = pd.to_numeric(result_data[("", "Avg. AUC")], errors="coerce")
|
| 113 |
+
result_data = result_data.sort_values(by=("", "Avg. AUC"), ascending=False, na_position="last")
|
| 114 |
+
result_data.insert(0, ("", "Rank"), range(1, len(result_data) + 1))
|
| 115 |
|
| 116 |
+
# result_data.columns = result_data.columns.map(str)
|
| 117 |
# Leaderboard at full width
|
| 118 |
search = gr.Textbox(show_label=False, placeholder="Search by model name…", elem_id="leaderboard-search", container=False, max_lines=1, type="text")
|
| 119 |
|
frontend/leaderboard.py
CHANGED
|
@@ -53,29 +53,44 @@ def format_leaderboard_data(raw_data: dict) -> pd.DataFrame:
|
|
| 53 |
|
| 54 |
# Create a row with all the data
|
| 55 |
row = {
|
| 56 |
-
"Model": config["model_name"],
|
| 57 |
-
"Organization": config.get("organization", ""),
|
| 58 |
-
"Avg. AUC": results["overall_score"]["roc_auc"],
|
| 59 |
-
"ΔAUC-PR": config.get("daucpr", ""),
|
| 60 |
}
|
| 61 |
|
| 62 |
# === Insert task columns immediately after ΔAUC-PR ===
|
| 63 |
for task_key, task_result in results.items():
|
| 64 |
-
if task_key != "overall_score":
|
| 65 |
-
row[task_key] = task_result
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
# === Then continue with the rest of the metadata columns ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
row.update({
|
| 69 |
-
"Model Description": config["model_description"],
|
| 70 |
-
"
|
| 71 |
-
"Publication": config.get("
|
| 72 |
-
"
|
| 73 |
-
"
|
| 74 |
-
"
|
| 75 |
-
"
|
| 76 |
-
"
|
| 77 |
-
"
|
| 78 |
-
"N-shot": config.get("n_shot", ""),
|
| 79 |
})
|
| 80 |
|
| 81 |
date_raw = config.get("date_approved", config.get("date_submitted", ""))
|
|
@@ -89,12 +104,13 @@ def format_leaderboard_data(raw_data: dict) -> pd.DataFrame:
|
|
| 89 |
|
| 90 |
row.update({
|
| 91 |
# ...
|
| 92 |
-
"Date Added": date_str,
|
| 93 |
})
|
| 94 |
|
| 95 |
rows.append(row)
|
| 96 |
|
| 97 |
df = pd.DataFrame(rows)
|
|
|
|
| 98 |
|
| 99 |
# Handle empty dataset case
|
| 100 |
if df.empty:
|
|
@@ -105,7 +121,7 @@ def format_leaderboard_data(raw_data: dict) -> pd.DataFrame:
|
|
| 105 |
df = pd.DataFrame(columns=COLUMN_NAMES)
|
| 106 |
else:
|
| 107 |
# rank according to overall score
|
| 108 |
-
df = df.sort_values(by="Avg. AUC", ascending=False).reset_index(
|
| 109 |
drop=True
|
| 110 |
)
|
| 111 |
# set different precision
|
|
|
|
| 53 |
|
| 54 |
# Create a row with all the data
|
| 55 |
row = {
|
| 56 |
+
("", "Model"): config["model_name"],
|
| 57 |
+
("", "Organization"): config.get("organization", ""),
|
| 58 |
+
("", "Avg. AUC"): results["overall_score"]["roc_auc"],
|
| 59 |
+
("", "ΔAUC-PR"): config.get("daucpr", ""),
|
| 60 |
}
|
| 61 |
|
| 62 |
# === Insert task columns immediately after ΔAUC-PR ===
|
| 63 |
for task_key, task_result in results.items():
|
| 64 |
+
if task_key != "overall_score":
|
| 65 |
+
row[("ROC-AUC", task_key)] = task_result.get("roc_auc", "")
|
| 66 |
+
for task_key, task_result in results.items():
|
| 67 |
+
if task_key != "overall_score":
|
| 68 |
+
row[("ΔAUC-PR", task_key)] = task_result.get("delta_auc_pr", "")
|
| 69 |
+
|
| 70 |
|
| 71 |
# === Then continue with the rest of the metadata columns ===
|
| 72 |
+
# row.update({
|
| 73 |
+
# "Model Description": config["model_description"],
|
| 74 |
+
# "Publication": config.get("publication_title", ""),
|
| 75 |
+
# "Publication Link": config.get("publication_link", ""),
|
| 76 |
+
# "# Parameters": config.get("model_size", ""),
|
| 77 |
+
# "Pretrained (y/n)": config.get("pretrained", ""),
|
| 78 |
+
# "Pretraining Data": config.get("pretraining_data", ""),
|
| 79 |
+
# "Zero-shot (y/n)": config.get("zero_shot", ""),
|
| 80 |
+
# "Few-shot (y/n)": config.get("few_shot", ""),
|
| 81 |
+
# "N-shot": config.get("n_shot", ""),
|
| 82 |
+
# })
|
| 83 |
+
|
| 84 |
row.update({
|
| 85 |
+
("", "Model Description"): config["model_description"],
|
| 86 |
+
("", "Publication"): config.get("publication_title", ""),
|
| 87 |
+
("", "Publication Link"): config.get("publication_link", ""),
|
| 88 |
+
("", "# Parameters"): config.get("model_size", ""),
|
| 89 |
+
("", "Pretrained (y/n)"): config.get("pretrained", ""),
|
| 90 |
+
("", "Pretraining Data"): config.get("pretraining_data", ""),
|
| 91 |
+
("", "Zero-shot (y/n)"): config.get("zero_shot", ""),
|
| 92 |
+
("", "Few-shot (y/n)"): config.get("few_shot", ""),
|
| 93 |
+
("", "N-shot"): config.get("n_shot", ""),
|
|
|
|
| 94 |
})
|
| 95 |
|
| 96 |
date_raw = config.get("date_approved", config.get("date_submitted", ""))
|
|
|
|
| 104 |
|
| 105 |
row.update({
|
| 106 |
# ...
|
| 107 |
+
("", "Date Added"): date_str,
|
| 108 |
})
|
| 109 |
|
| 110 |
rows.append(row)
|
| 111 |
|
| 112 |
df = pd.DataFrame(rows)
|
| 113 |
+
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
| 114 |
|
| 115 |
# Handle empty dataset case
|
| 116 |
if df.empty:
|
|
|
|
| 121 |
df = pd.DataFrame(columns=COLUMN_NAMES)
|
| 122 |
else:
|
| 123 |
# rank according to overall score
|
| 124 |
+
df = df.sort_values(by=("", "Avg. AUC"), ascending=False).reset_index(
|
| 125 |
drop=True
|
| 126 |
)
|
| 127 |
# set different precision
|
frontend/styles.css
CHANGED
|
@@ -48,86 +48,70 @@ body, .gradio-container {
|
|
| 48 |
|
| 49 |
|
| 50 |
|
| 51 |
-
|
| 52 |
-
/* ===
|
| 53 |
.leaderboard-container {
|
| 54 |
-
overflow
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
border: none;
|
| 58 |
border-radius: 10px;
|
| 59 |
-
background:
|
| 60 |
-
|
| 61 |
-
margin-top: 10px;
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
/* === Sticky header === */
|
| 65 |
-
.leaderboard-table thead th {
|
| 66 |
-
position: sticky;
|
| 67 |
-
top: 0;
|
| 68 |
-
z-index: 2;
|
| 69 |
-
background: #f9fafb; /* same as header background */
|
| 70 |
-
border-bottom: 1px solid #e5e7eb;
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
/* Keep nice clean row look */
|
| 74 |
-
.leaderboard-row:hover {
|
| 75 |
-
background-color: #f8fafc;
|
| 76 |
-
transition: background-color 0.15s ease-in-out;
|
| 77 |
}
|
| 78 |
|
| 79 |
/* === Table === */
|
| 80 |
.leaderboard-table {
|
| 81 |
-
border-collapse: collapse;
|
| 82 |
width: max-content;
|
| 83 |
min-width: 100%;
|
|
|
|
| 84 |
table-layout: fixed;
|
| 85 |
-
border: none; /* ✅ no outer border on table */
|
| 86 |
}
|
| 87 |
|
| 88 |
/* === Header cells === */
|
| 89 |
-
.leaderboard-
|
| 90 |
-
background: #f9fafb;
|
| 91 |
-
font-weight: 600;
|
| 92 |
-
font-size: 15px;
|
| 93 |
-
color: #374151;
|
| 94 |
text-align: center;
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
/* === Data cells === */
|
| 104 |
-
.leaderboard-
|
|
|
|
|
|
|
| 105 |
font-size: 14px;
|
| 106 |
color: #111827;
|
| 107 |
-
|
| 108 |
-
border: 1px solid #f3f4f6; /* ✅ light horizontal divider */
|
| 109 |
vertical-align: top;
|
| 110 |
-
white-space: normal;
|
| 111 |
word-break: break-word;
|
| 112 |
-
|
| 113 |
line-height: 1.5;
|
| 114 |
-
max-width: 260px;
|
| 115 |
}
|
| 116 |
|
| 117 |
-
|
| 118 |
.leaderboard-row:hover {
|
| 119 |
background-color: #f8fafc;
|
| 120 |
transition: background-color 0.15s ease-in-out;
|
| 121 |
}
|
| 122 |
|
| 123 |
-
/* === Optional subtle rounded header top === */
|
| 124 |
-
.leaderboard-table thead th:first-child {
|
| 125 |
-
border-top-left-radius: 8px;
|
| 126 |
-
}
|
| 127 |
-
.leaderboard-table thead th:last-child {
|
| 128 |
-
border-top-right-radius: 8px;
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
/* === Scrollbar === */
|
| 132 |
.leaderboard-container::-webkit-scrollbar {
|
| 133 |
height: 8px;
|
|
@@ -149,8 +133,6 @@ body, .gradio-container {
|
|
| 149 |
|
| 150 |
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
#leaderboard-tab,
|
| 155 |
#leaderboard-tab .gr-panel,
|
| 156 |
#leaderboard-tab .gr-block,
|
|
@@ -206,7 +188,7 @@ body, .gradio-container {
|
|
| 206 |
transition: all 0.2s ease-in-out !important;
|
| 207 |
box-shadow: 0 4px 10px rgba(99, 102, 241, 0.25) !important;
|
| 208 |
margin-top: 0px !important; /* tighter to search bar */
|
| 209 |
-
margin-bottom:
|
| 210 |
}
|
| 211 |
|
| 212 |
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
|
| 51 |
+
|
| 52 |
+
/* === Container === */
|
| 53 |
.leaderboard-container {
|
| 54 |
+
overflow: auto;
|
| 55 |
+
max-height: 600px; /* scroll vertically after ~10 rows */
|
| 56 |
+
border: 1px solid #e5e7eb;
|
|
|
|
| 57 |
border-radius: 10px;
|
| 58 |
+
background: #fff;
|
| 59 |
+
margin-top: 3px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
/* === Table === */
|
| 63 |
.leaderboard-table {
|
|
|
|
| 64 |
width: max-content;
|
| 65 |
min-width: 100%;
|
| 66 |
+
border-collapse: collapse;
|
| 67 |
table-layout: fixed;
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
/* === Header cells === */
|
| 71 |
+
.leaderboard-table th {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
text-align: center;
|
| 73 |
+
vertical-align: middle;
|
| 74 |
+
padding: 10px 14px;
|
| 75 |
+
font-weight: 600;
|
| 76 |
+
font-size: 14px;
|
| 77 |
+
background: #f9fafb;
|
| 78 |
+
border: 1px solid #e5e7eb;
|
| 79 |
+
position: sticky;
|
| 80 |
+
top: 0;
|
| 81 |
+
z-index: 2;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.leaderboard-table thead tr:first-child th {
|
| 85 |
+
background: #f1f5f9;
|
| 86 |
+
font-weight: 700;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.leaderboard-table thead th:first-child {
|
| 90 |
+
border-top-left-radius: 8px;
|
| 91 |
+
}
|
| 92 |
+
.leaderboard-table thead th:last-child {
|
| 93 |
+
border-top-right-radius: 8px;
|
| 94 |
}
|
| 95 |
|
| 96 |
/* === Data cells === */
|
| 97 |
+
.leaderboard-table td {
|
| 98 |
+
text-align: left;
|
| 99 |
+
padding: 10px 14px;
|
| 100 |
font-size: 14px;
|
| 101 |
color: #111827;
|
| 102 |
+
border: 1px solid #f3f4f6;
|
|
|
|
| 103 |
vertical-align: top;
|
|
|
|
| 104 |
word-break: break-word;
|
| 105 |
+
white-space: normal;
|
| 106 |
line-height: 1.5;
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
+
/* === Hover effect === */
|
| 110 |
.leaderboard-row:hover {
|
| 111 |
background-color: #f8fafc;
|
| 112 |
transition: background-color 0.15s ease-in-out;
|
| 113 |
}
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
/* === Scrollbar === */
|
| 116 |
.leaderboard-container::-webkit-scrollbar {
|
| 117 |
height: 8px;
|
|
|
|
| 133 |
|
| 134 |
|
| 135 |
|
|
|
|
|
|
|
| 136 |
#leaderboard-tab,
|
| 137 |
#leaderboard-tab .gr-panel,
|
| 138 |
#leaderboard-tab .gr-block,
|
|
|
|
| 188 |
transition: all 0.2s ease-in-out !important;
|
| 189 |
box-shadow: 0 4px 10px rgba(99, 102, 241, 0.25) !important;
|
| 190 |
margin-top: 0px !important; /* tighter to search bar */
|
| 191 |
+
margin-bottom: 20px !important; /* more breathing room before table */
|
| 192 |
}
|
| 193 |
|
| 194 |
|