diff --git a/get_data.py b/get_data.py
index 350a354..ae93666 100644
--- a/get_data.py
+++ b/get_data.py
@@ -1,6 +1,5 @@
from fastapi import FastAPI, HTTPException, Request
-from fastapi.responses import Response, HTMLResponse, StreamingResponse, JSONResponse
-from fastapi.staticfiles import StaticFiles
+from fastapi.responses import Response, HTMLResponse, StreamingResponse
from contextlib import asynccontextmanager
import requests, uvicorn, json
import threading, queue
@@ -16,7 +15,7 @@ import nasio
import io, os, platform, time
import xml.etree.ElementTree as ET
import re
-from PIL import Image, ImageDraw, ImageFont
+from PIL import Image, ImageDraw
from io import BytesIO
import warnings
@@ -2263,6 +2262,8 @@ async def team(who: str):
)
+ ".svg"
),
+ "country": (item.get("countryName") or "").strip(),
+ "photo_site": (item.get("photo") or "").strip(),
"photoGFX": (
os.path.join(
"D:\\Photos",
@@ -3766,6 +3767,7 @@ async def last_5_games():
return wl_list[::-1]
+ # ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
def find_next_game(team_id: int):
if not CALENDAR or "items" not in CALENDAR:
@@ -3806,6 +3808,11 @@ async def last_5_games():
if team_id not in (t1.get("teamId"), t2.get("teamId")):
continue
+ # 👇 пропускаем ТЕКУЩИЙ матч (тот, что в GAME_ID)
+ gid = game_info.get("id")
+ if gid is not None and GAME_ID is not None and str(gid) == str(GAME_ID):
+ continue
+
dt_str = game_info.get("defaultZoneDateTime") or ""
try:
dt = datetime.fromisoformat(dt_str)
@@ -3815,11 +3822,11 @@ async def last_5_games():
# убираем tzinfo, чтобы можно было сравнивать с now
if dt.tzinfo is not None:
dt = dt.replace(tzinfo=None)
- # только будущие матчи
+
+ # только матчи ПОСЛЕ текущего момента
if dt <= now:
- print(dt, now)
continue
- print(item)
+
# определяем соперника и место
if team_id == t1.get("teamId"):
opp_name = t2.get("name", "")
@@ -3839,8 +3846,8 @@ async def last_5_games():
# 🆕 формируем английскую строку
dt = best["dt"]
weekday_en = WEEKDAYS_EN[dt.weekday()] # monday..sunday
- month_en = MONTHS_EN[dt.month - 1] # january..december
- day = dt.day # 1..31
+ month_en = MONTHS_EN[dt.month - 1] # january..december
+ day = dt.day # 1..31
place_en = "home" if best["place"] == "home" else "away"
formatted = (
@@ -3850,9 +3857,9 @@ async def last_5_games():
return {
"opponent": best["opp"],
"date": best["dt"].strftime("%Y-%m-%d %H:%M"),
- "place": best["place"], # "home" / "away"
- "place_ru": place_ru, # "дома" / "в гостях"
- "formatted": formatted, # 🆕 "wednesday, march 26, at home against astana"
+ "place": best["place"], # "home" / "away"
+ "place_ru": place_ru, # "дома" / "в гостях"
+ "formatted": formatted, # "wednesday, march 26, at home against astana"
}
# последние 5 игр и результаты
@@ -4031,6 +4038,8 @@ async def commentary():
Игрок |
PTS |
REB |
+
+
AST |
STL |
BLK |
@@ -4069,9 +4078,11 @@ async def commentary():
# REB: если нет 'reb', то dreb + oreb
reb = p.get("reb")
+ dreb_raw = p.get("dreb")
+ oreb_raw = p.get("oreb")
if reb in ("", None):
- dreb = p.get("dreb") or 0
- oreb = p.get("oreb") or 0
+ dreb = dreb_raw or 0
+ oreb = oreb_raw or 0
reb_val = dreb + oreb
else:
reb_val = reb
@@ -4089,6 +4100,10 @@ async def commentary():
foul_raw = p.get("foul")
foul_str = "" if foul_raw is None else str(foul_raw)
+
+ # значения для отдельных колонок
+ oreb_val = "" if oreb_raw in (None, "") else oreb_raw
+ dreb_val = "" if dreb_raw in (None, "") else dreb_raw
plus_minus = p.get("plusMinus") or ""
kpi = p.get("kpi") or ""
@@ -4119,6 +4134,8 @@ async def commentary():
{pts} |
{reb_val} |
+
+
{ast} |
{stl} |
{blk} |
@@ -4139,7 +4156,7 @@ async def commentary():
# Если вдруг ни одного игрока не прошло фильтр – покажем заглушку
if not rows_html:
rows_html.append(
- '| Нет данных по игрокам. |
'
+ '| Нет данных по игрокам. |
'
)
table_footer = """
@@ -4324,6 +4341,19 @@ async def commentary():
cursor: pointer;
text-decoration: underline;
}}
+
+ .player-name.selected-player-left,
+ .player-name.selected-player-right {{
+ text-decoration: none;
+ font-weight: 600;
+ background: rgba(59,130,246,0.12);
+ border-radius: 4px;
+ padding: 0 4px;
+ }}
+
+ .player-name.selected-player-right {{
+ background: rgba(16,185,129,0.12); /* можно оставить одинаковый, если не хочется различать */
+ }}
.extra-col {{
display: none; /* по умолчанию скрыто */
}}
@@ -4337,6 +4367,75 @@ async def commentary():
#player-details table {{
margin-top: 5px;
}}
+ .compare-header {{
+ display: flex;
+ gap: 16px;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+ }}
+
+ /* делаем блок игрока flex-контейнером: слева текст, справа фото */
+ .compare-player-block {{
+ flex: 1 1 0;
+ padding: 8px 10px;
+ border-radius: 10px;
+ background: #141414;
+ border: 1px solid #333;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 8px;
+ }}
+
+ /* левая часть — текст (имя, амплуа и т.п.) */
+ .compare-player-info {{
+ flex: 1 1 auto;
+ min-width: 0;
+ }}
+
+ /* правая часть — фото */
+ .compare-player-photo {{
+ flex: 0 0 auto;
+ width: 72px;
+ height: 72px;
+ border-radius: 8px;
+ overflow: hidden;
+ background: #111;
+ border: 1px solid #333;
+ }}
+ .compare-player-photo img {{
+ width: 100%;
+ height: 100%;
+ display: block;
+ object-fit: cover;
+ }}
+
+ .compare-name-row {{
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 4px;
+ }}
+ .compare-name {{
+ font-size: 16px;
+ font-weight: 600;
+ }}
+ .compare-flag {{
+ width: 20px;
+ height: 14px;
+ border-radius: 2px;
+ object-fit: cover;
+ box-shadow: 0 0 0 1px rgba(0,0,0,0.4);
+ }}
+ .compare-meta-list {{
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ font-size: 12px;
+ color: #ccc;
+ line-height: 1.4;
+ }}
@@ -4528,16 +4627,39 @@ async def commentary():
["team1_table", "team2_table"].forEach(function(tableId) {{
var table = document.getElementById(tableId);
if (!table) return;
+
var tbody = table.querySelector("tbody");
if (!tbody) return;
+ // Находим индексы нужных колонок по заголовкам
+ var headers = table.querySelectorAll("thead th");
+ var ptsIndex = -1;
+ var foulIndex = -1;
+
+ headers.forEach(function(th, idx) {{
+ var text = (th.innerText || th.textContent || "").trim().toLowerCase();
+
+ if (text === "pts") {{
+ ptsIndex = idx;
+ }}
+ // если вдруг подпишешь по-русски — можно добавить варианты
+ if (text === "foul" || text === "фол" || text === "фолы") {{
+ foulIndex = idx;
+ }}
+ }});
+
+ if (ptsIndex < 0 || foulIndex < 0) {{
+ // не нашли нужные колонки – тихо выходим
+ return;
+ }}
+
Array.from(tbody.rows).forEach(function(row) {{
var cells = row.cells;
- if (!cells || cells.length < 14) return;
+ if (!cells) return;
+ if (cells.length <= ptsIndex || cells.length <= foulIndex) return;
- // индексы: PTS = 2, Foul = 13
- var ptsCell = cells[2];
- var foulCell = cells[13];
+ var ptsCell = cells[ptsIndex];
+ var foulCell = cells[foulIndex];
if (!ptsCell || !foulCell) return;
// --- 1) зелёная подсветка при изменении PTS / Foul ---
@@ -4609,7 +4731,7 @@ async def commentary():
}});
}})();
- // === клик по игроку + компактные таблицы сезон/карьера ===
+ // === клик по игроку + компактные таблицы сезон/карьера + сравнение левого/правого ===
(function() {{
var details = document.getElementById("player-details");
if (!details) return;
@@ -4620,6 +4742,8 @@ async def commentary():
function buildMetrics(p) {{
var seasonAvg = [
+ ["Игры", p.TGameCount],
+ ["В старте", p.TStartCount],
["Очки", p.AvgPoints],
["Передачи", p.AvgAssist],
["Подборы", p.AvgRebound],
@@ -4635,6 +4759,8 @@ async def commentary():
["С игры, %", p.Shot23Percent]
];
var seasonTot = [
+ ["Игры", p.TGameCount],
+ ["В старте", p.TStartCount],
["Очки", p.TPoints],
["Передачи", p.TAssist],
["Подборы", p.TRebound],
@@ -4650,6 +4776,8 @@ async def commentary():
["С игры (goal/shot)", p.TShots23]
];
var careerAvg = [
+ ["Игры", p.Career_TGameCount],
+ ["В старте", p.Career_TStartCount],
["Очки", p.Career_AvgPoints],
["Передачи", p.Career_AvgAssist],
["Подборы", p.Career_AvgRebound],
@@ -4665,6 +4793,8 @@ async def commentary():
["С игры, %", p.Career_Shot23Percent]
];
var careerTot = [
+ ["Игры", p.Career_TGameCount],
+ ["В старте", p.Career_TStartCount],
["Очки", p.Career_TPoints],
["Передачи", p.Career_TAssist],
["Подборы", p.Career_TRebound],
@@ -4694,12 +4824,32 @@ async def commentary():
return metrics;
}}
+ // выбранные игроки слева/справа
+ var selectedLeft = null;
+ var selectedRight = null;
+
+ // одиночное отображение (как было раньше) — НЕ трогаем таблицу, тут всё ок
function renderPlayerDetails(p) {{
if (!p) return "";
var metrics = buildMetrics(p);
var html = "";
- html += "" + safe(p.NameGFX || p.name) + "
";
+
+ var metaParts = [];
+ if (p.age) metaParts.push(p.age + " лет");
+ if (p.height) metaParts.push(p.height.height || p.height);
+ if (p.weight) metaParts.push(p.weight);
+
+ html += ""
+ + safe(p.NameGFX || p.name)
+ + (p.flag ? ("
") : "")
+ + "
";
+
+ if (metaParts.length) {{
+ html += "" + safe(metaParts.join(" · ")) + "
";
+ }}
+
html += "Сезон / карьера — компактно
";
+
html += "";
html += " | ";
for (var j = 0; j < metrics.length; j++) {{
@@ -4735,6 +4885,156 @@ async def commentary():
return html;
}}
+ // === Сравнение игроков: шапка + СТАРАЯ вертикальная таблица с отдельными колонками ===
+ function renderPlayersComparison(leftP, rightP) {{
+ // если вообще никого нет — ничего не показываем
+ if (!leftP && !rightP) {{
+ return "";
+ }}
+
+ // --- метрики ---
+ var LM = leftP ? buildMetrics(leftP) : null;
+ var RM = rightP ? buildMetrics(rightP) : null;
+
+ // базовый набор строк (под один из игроков)
+ var base = LM || RM || [];
+
+ // если одного игрока нет — делаем для него пустой набор метрик
+ function makeEmptyMetrics(from) {{
+ return from.map(function (m) {{
+ return {{
+ label: m.label,
+ sAvg: "",
+ sTot: "",
+ cAvg: "",
+ cTot: ""
+ }};
+ }});
+ }}
+
+ if (!LM) LM = makeEmptyMetrics(base);
+ if (!RM) RM = makeEmptyMetrics(base);
+
+ function metaList(p) {{
+ if (!p) return [];
+ var arr = [];
+ if (p.roleFull || p.role) arr.push("Амплуа: " + (p.roleFull || p.role));
+ if (p.age) arr.push("Возраст: " + p.age);
+ if (p.height) arr.push("Рост: " + p.height);
+ if (p.weight) arr.push("Вес: " + p.weight);
+ if (p.teamName) arr.push("Команда: " + p.teamName);
+ return arr;
+ }}
+
+function renderPlayerHeader(p) {{
+ var html = "";
+ html += "";
+
+ // левая часть: текст
+ html += "
";
+ html += "
";
+ html += "
" + safe((p && (p.NameGFX || p.name)) || "") + "";
+ if (p && p.flag) {{
+ var title = safe(p.country || "").replace(/\"/g, """);
+ html += "

";
+ }}
+ html += "
";
+ html += "
";
+ html += "
";
+
+ // правая часть: фото из p.photo_site (если есть)
+ if (p && p.photo_site) {{
+ html += "
";
+ html += "
 + ")
";
+ html += "
";
+ }}
+
+ html += "
";
+ return html;
+}}
+
+ var html = "";
+
+ html += "Сравнение игроков
";
+ html += "";
+
+ // --- СТАРАЯ таблица: 4 колонки слева + 4 справа ---
+ html += "";
+
+ // шапка таблицы
+ html += "";
+ html += "";
+ html += "| Левый игрок | ";
+ html += " | ";
+ html += "Правый игрок | ";
+ html += "
";
+
+ html += "";
+ html += "| Сезон (сред.) | ";
+ html += "Сезон (тотал) | ";
+ html += "Карьера (сред.) | ";
+ html += "Карьера (тотал) | ";
+
+ html += "Показатель | ";
+
+ html += "Сезон (сред.) | ";
+ html += "Сезон (тотал) | ";
+ html += "Карьера (сред.) | ";
+ html += "Карьера (тотал) | ";
+ html += "
";
+ html += "";
+
+ html += "";
+
+ // строки таблицы — одна строка = один показатель
+ for (var i = 0; i < base.length; i++) {{
+ var L = LM[i] || {{}};
+ var R = RM[i] || {{}};
+
+ html += "";
+
+ // левый
+ html += "| " + safe(L.sAvg) + " | ";
+ html += "" + safe(L.sTot) + " | ";
+ html += "" + safe(L.cAvg) + " | ";
+ html += "" + safe(L.cTot) + " | ";
+
+ // показатель
+ html += "" + safe(base[i].label || "") + " | ";
+
+ // правый
+ html += "" + safe(R.sAvg) + " | ";
+ html += "" + safe(R.sTot) + " | ";
+ html += "" + safe(R.cAvg) + " | ";
+ html += "" + safe(R.cTot) + " | ";
+
+ html += "
";
+ }}
+
+ html += "
";
+
+ return html;
+ }}
+
+ // переотрисовка блока с деталями/сравнением
+ function rerenderDetails() {{
+ if (selectedLeft || selectedRight) {{
+ // Всегда рисуем вертикальную сравнительную таблицу.
+ // Если один из игроков не выбран — его половина будет пустой.
+ details.innerHTML = renderPlayersComparison(selectedLeft, selectedRight);
+ }} else {{
+ details.innerHTML = 'Кликни по фамилии игрока, чтобы показать сезон/карьеру.
';
+ }}
+ }}
+
+ // выбор/снятие выбора игрока
function selectPlayer(teamKey, pid, noScroll, noSave) {{
var data = window.PLAYER_DATA || {{}};
var list = data[teamKey] || [];
@@ -4745,16 +5045,77 @@ async def commentary():
break;
}}
}}
+
+ // если игрок не найден в данных — просто выходим
if (!found) return;
- details.innerHTML = renderPlayerDetails(found);
+
+ var pidStr = String(pid || "");
+
+ if (teamKey === "team1") {{
+ // повторный клик по тому же игроку слева — снимаем выбор
+ if (selectedLeft && String(selectedLeft.id || "") === pidStr) {{
+ selectedLeft = null;
+ }} else {{
+ selectedLeft = found;
+ }}
+ }} else if (teamKey === "team2") {{
+ // повторный клик по тому же игроку справа — снимаем выбор
+ if (selectedRight && String(selectedRight.id || "") === pidStr) {{
+ selectedRight = null;
+ }} else {{
+ selectedRight = found;
+ }}
+ }} else {{
+ // запасной вариант — считаем как левую сторону
+ if (selectedLeft && String(selectedLeft.id || "") === pidStr) {{
+ selectedLeft = null;
+ }} else {{
+ selectedLeft = found;
+ }}
+ }}
+
+ // подсветка выбранных фамилий
+ var allCells = document.querySelectorAll(".player-name");
+ allCells.forEach(function (cell) {{
+ cell.classList.remove("selected-player-left", "selected-player-right");
+
+ var cPid = cell.getAttribute("data-player-id");
+ var cTeam = cell.getAttribute("data-team");
+
+ if (selectedLeft && cTeam === "team1" &&
+ String(selectedLeft.id || "") === String(cPid || "")) {{
+ cell.classList.add("selected-player-left");
+ }}
+
+ if (selectedRight && cTeam === "team2" &&
+ String(selectedRight.id || "") === String(cPid || "")) {{
+ cell.classList.add("selected-player-right");
+ }}
+ }});
+
+ rerenderDetails();
+
if (!noSave) {{
- localStorage.setItem("commentary_last_player", JSON.stringify({{ team: teamKey, id: pid }}));
+ try {{
+ if (selectedLeft || selectedRight) {{
+ // запоминаем последнего кликнутого
+ localStorage.setItem(
+ "commentary_last_player",
+ JSON.stringify({{ team: teamKey, id: pid }})
+ );
+ }} else {{
+ // если вообще никого не осталось выбранным — чистим
+ localStorage.removeItem("commentary_last_player");
+ }}
+ }} catch (e) {{}}
}}
if (!noScroll) {{
details.scrollIntoView({{ behavior: "smooth", block: "start" }});
}}
}}
+ window.selectPlayer = selectPlayer;
+
// клики по фамилиям
var cells = document.querySelectorAll(".player-name");
for (var i = 0; i < cells.length; i++) {{
@@ -4765,7 +5126,7 @@ async def commentary():
}});
}}
- // восстановить последнего выбранного
+ // восстановить последнего выбранного (если был)
var saved = localStorage.getItem("commentary_last_player");
if (saved) {{
try {{
@@ -4775,7 +5136,7 @@ async def commentary():
}}
}} catch (e) {{}}
}}
-
+
// === автообновление статистики без перезагрузки страницы ===
function updateTeamTable(teamKey, newPlayers) {{
var map = {{}};
@@ -4788,9 +5149,10 @@ async def commentary():
return (v === undefined || v === null) ? "" : v;
}}
+ // все ячейки с фамилиями для нужной команды
var cells = document.querySelectorAll('.player-name[data-team="' + teamKey + '"]');
- cells.forEach(function(cell) {{
+ cells.forEach(function (cell) {{
var pid = cell.getAttribute("data-player-id");
var newP = map[String(pid)];
if (!newP) return;
@@ -4798,42 +5160,64 @@ async def commentary():
var row = cell.parentElement;
var tds = row.children;
- // обновляем data-on-court
+ // обновляем data-on-court (для раскраски строк)
var onCourt = !!newP.isOnCourt;
row.dataset.onCourt = onCourt ? "true" : "false";
+ // Порядок колонок в таблице:
+ // 0 #
+ // 1 Игрок
+ // 2 PTS
+ // 3 REB
+ // 4 AST
+ // 5 STL
+ // 6 BLK
+ // 7 MIN
+ // 8 OREB (extra-col)
+ // 9 DREB (extra-col)
+ // 10 FG (extra-col)
+ // 11 2PT (extra-col)
+ // 12 3PT (extra-col)
+ // 13 1PT (extra-col)
+ // 14 TO (extra-col)
+ // 15 Foul
+ // 16 +/-
+ // 17 KPI
var fields = [
- [2, "pts"],
- [3, "reb"],
- [4, "ast"],
- [5, "stl"],
- [6, "blk"],
- [7, "time"],
- [8, "fg"],
- [9, "pt-2"],
- [10, "pt-3"],
- [11, "pt-1"],
- [12, "to"],
- [13, "foul"],
- [14, "plusMinus"],
- [15, "kpi"]
+ [2, "pts"],
+ [3, "reb"], // пересчёт ниже
+ [4, "oreb"],
+ [5, "dreb"],
+ [6, "ast"],
+ [7, "stl"],
+ [8, "blk"],
+ [9, "time"],
+ [10, "fg"],
+ [11, "pt-2"],
+ [12, "pt-3"],
+ [13, "pt-1"],
+ [14, "to"],
+ [15, "foul"],
+ [16, "plusMinus"],
+ [17, "kpi"]
];
+ // REB: если нет newP.reb, считаем как dreb + oreb
var newReb = newP.reb;
- if (newReb === undefined) {{
- newReb = (newP.dreb || 0) + (newP.oreb || 0);
+ if (newReb === undefined || newReb === null || newReb === "") {{
+ var dreb = newP.dreb || 0;
+ var oreb = newP.oreb || 0;
+ newReb = dreb + oreb;
}}
function applyUpdate(td, newVal) {{
- newVal = safe(newVal);
- td.textContent = newVal;
+ td.textContent = safe(newVal);
}}
- fields.forEach(function(item) {{
+ fields.forEach(function (item) {{
var index = item[0];
- var key = item[1];
- var td = tds[index];
-
+ var key = item[1];
+ var td = tds[index];
if (!td) return;
if (key === "reb") {{
@@ -4845,16 +5229,45 @@ async def commentary():
}});
}}
- function refreshSelected() {{
- var saved = localStorage.getItem("commentary_last_player");
- if (!saved) return;
- try {{
- var obj = JSON.parse(saved);
- if (obj && obj.team && obj.id) {{
- selectPlayer(obj.team, obj.id, true, true);
- }}
- }} catch (e) {{}}
- }}
+
+
+
+
+function refreshSelected() {{
+ // если никого не выбрали – нечего обновлять
+ if (!selectedLeft && !selectedRight) {{
+ return;
+ }}
+
+ var data = window.PLAYER_DATA || {{}};
+ var team1 = Array.isArray(data.team1) ? data.team1 : [];
+ var team2 = Array.isArray(data.team2) ? data.team2 : [];
+
+ // обновляем ссылку на объект для левого игрока (team1)
+ if (selectedLeft) {{
+ var leftId = String(selectedLeft.id || "");
+ var updatedLeft = team1.find(function (p) {{
+ return String(p.id || "") === leftId;
+ }});
+ if (updatedLeft) {{
+ selectedLeft = updatedLeft;
+ }}
+ }}
+
+ // обновляем ссылку на объект для правого игрока (team2)
+ if (selectedRight) {{
+ var rightId = String(selectedRight.id || "");
+ var updatedRight = team2.find(function (p) {{
+ return String(p.id || "") === rightId;
+ }});
+ if (updatedRight) {{
+ selectedRight = updatedRight;
+ }}
+ }}
+
+ // просто перерисовываем блок сравнения, без скролла и без записи в localStorage
+ rerenderDetails();
+}}
setInterval(function() {{
fetch('/team1').then(function(r) {{
@@ -4884,8 +5297,9 @@ async def commentary():
// 🔁 обновляем шапку и четверти
updateHeaderFromLiveStatus(); // счёт + фолы из live_status
updateQuartersFromScores(); // счёт по четвертям из /scores
+ updateTimeoutsFromTeamStats(); // тайм-ауты обновление
- }}, 1000);
+ }}, 1500);
updateTimeoutsFromTeamStats(); // тайм-ауты строго из team_stats.timeout_left
}})();
@@ -5002,7 +5416,7 @@ async def commentary():
}}
// запустим автообновление счёта и четвертей
updateScoreAndQuarters();
- setInterval(updateScoreAndQuarters, 1000);
+ setInterval(updateScoreAndQuarters, 1500);
makeTableSortable("team1_table");
makeTableSortable("team2_table");
@@ -5010,7 +5424,7 @@ async def commentary():
// первый старт стилей сразу
updateRowStyles();
// и периодически обновляем (на случай любых внешних изменений)
- setInterval(updateRowStyles, 1000);
+ setInterval(updateRowStyles, 1500);
}});
@@ -5563,6 +5977,18 @@ async def milestones_ui():
align-items: center;
gap: 6px;
}
+ .player-flag {
+ width: 22px;
+ height: 16px;
+ object-fit: cover;
+ border-radius: 3px;
+ }
+
+ .player-meta,
+ .player-meta-inline {
+ font-size: 11px;
+ color: var(--text-muted);
+ }
label {
font-size: 12px;
color: var(--text-muted);