在 GitHub 个人资料页与头像悬停卡片中显示 GitFut 球探信息
当前为
// ==UserScript==
// @name GitHub GitFut
// @name:ru GitHub GitFut
// @name:zh-CN GitHub GitFut
// @name:es GitHub GitFut
// @name:pt-BR GitHub GitFut
// @name:de GitHub GitFut
// @name:fr GitHub GitFut
// @name:ja GitHub GitFut
// @name:ko GitHub GitFut
// @name:pl GitHub GitFut
// @namespace https://github.com/NemoKing1210/github-gitfut
// @version 1.9.0
// @description Adds GitFut scouting cards on GitHub profiles and avatar hovercards
// @description:ru Добавляет карточки GitFut на профили GitHub и в поповеры аватаров
// @description:zh-CN 在 GitHub 个人资料页与头像悬停卡片中显示 GitFut 球探信息
// @description:es Añade cartas GitFut en perfiles y hovercards de avatar de GitHub
// @description:pt-BR Adiciona cartas GitFut nos perfis e hovercards de avatar do GitHub
// @description:de Zeigt GitFut-Karten auf GitHub-Profilen und Avatar-Hovercards
// @description:fr Ajoute les cartes GitFut sur les profils et hovercards d’avatar GitHub
// @description:ja GitHubプロフィールとアバターホバーカードにGitFut情報を表示
// @description:ko GitHub 프로필과 아바타 호버카드에 GitFut 스카우트 정보를 표시
// @description:pl Dodaje karty GitFut na profilach i hovercardach awatarów GitHub
// @author NemoKing1210
// @tag github
// @tag gitfut
// @homepageURL https://github.com/NemoKing1210/github-gitfut
// @supportURL https://github.com/NemoKing1210/github-gitfut/issues
// @license MIT
// @icon https://gitfut.com/favicon.ico
// @match https://github.com/*
// @match https://gist.github.com/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @connect gitfut.com
// @run-at document-idle
// @noframes
// ==/UserScript==
(function () {
'use strict';
const API_BASE = 'https://gitfut.com/api/card';
const SITE_BASE = 'https://gitfut.com';
const REPO_URL = 'https://github.com/NemoKing1210/github-gitfut';
const CACHE_KEY = 'gf_github_cache_v1';
const SETTINGS_KEY = 'gf_github_settings';
/** Soft budget for cache progress UI (GM storage itself may allow more). */
const CACHE_BUDGET_BYTES = 5 * 1024 * 1024;
/** Fallback if GM_info is unavailable — keep in sync with @version. */
const SCRIPT_VERSION =
(typeof GM_info !== 'undefined' && GM_info?.script?.version) || '1.9.0';
const CACHE_HOURS_MAX = 168;
const DEFAULT_SETTINGS = {
cacheHours: 12,
showHovercard: true,
cardTheme: 'standard',
locale: 'auto',
};
const CARD_THEMES = ['standard', 'github', 'fifa', 'neon'];
const MAX_CONCURRENT = 2;
const REQUEST_DELAY_MS = 100;
const CACHE_PERSIST_MS = 1000;
const SCAN_DEBOUNCE_MS = 400;
const PANEL_ID = 'gf-profile-panel';
const HOVERCARD_BLOCK_ID = 'gf-hovercard-block';
const RESERVED_PATHS = new Set([
'about', 'account', 'actions', 'apps', 'blog', 'cache', 'codespaces', 'collections',
'contact', 'copilot', 'customer', 'dashboard', 'developer', 'discussions', 'docs',
'education', 'enterprise', 'events', 'explore', 'features', 'files', 'funding',
'gist', 'home', 'issues', 'join', 'login', 'logout', 'marketplace', 'mcp',
'new', 'notifications', 'organizations', 'orgs', 'packages', 'pages', 'pricing',
'pulls', 'readme', 'search', 'security', 'sessions', 'settings', 'site', 'sponsors',
'stars', 'topics', 'trending', 'users', 'watching', 'workflows',
]);
const FINISH_COLORS = {
bronze: {
accent: '#CD7F32',
ink: '#2A1A0C',
soft: 'rgba(205,127,50,0.16)',
glow: 'rgba(205,127,50,0.28)',
deep: '#5a3412',
shine: 'rgba(255,198,120,0.45)',
tier: 1,
},
silver: {
accent: '#AAB2BD',
ink: '#1F242B',
soft: 'rgba(170,178,189,0.2)',
glow: 'rgba(180,190,205,0.4)',
deep: '#3a4250',
shine: 'rgba(255,255,255,0.55)',
tier: 2,
},
gold: {
accent: '#E6B422',
ink: '#3A2806',
soft: 'rgba(230,180,34,0.22)',
glow: 'rgba(230,180,34,0.55)',
deep: '#7a5608',
shine: 'rgba(255,236,160,0.7)',
tier: 3,
},
totw: {
accent: '#E03E52',
ink: '#4A0A14',
soft: 'rgba(224,62,82,0.2)',
glow: 'rgba(224,62,82,0.55)',
deep: '#6a1020',
shine: 'rgba(255,140,160,0.65)',
tier: 4,
},
toty: {
accent: '#3B7AFF',
ink: '#10254F',
soft: 'rgba(59,122,255,0.22)',
glow: 'rgba(59,122,255,0.6)',
deep: '#0a1f5c',
shine: 'rgba(160,200,255,0.75)',
tier: 5,
},
icon: {
accent: '#F3D688',
ink: '#2A1A45',
soft: 'rgba(243,214,136,0.24)',
glow: 'rgba(180,120,255,0.55)',
deep: '#3a1a70',
shine: 'rgba(255,240,200,0.85)',
tier: 6,
},
founder: {
accent: '#FF5A7A',
ink: '#3A0A18',
soft: 'rgba(255,90,122,0.22)',
glow: 'rgba(255,90,122,0.6)',
deep: '#5a1028',
shine: 'rgba(255,180,200,0.8)',
tier: 7,
},
};
const FINISH_KEYS = Object.keys(FINISH_COLORS);
const FINISH_CLASS_RE = /^gf-(?:hc|panel)-finish--/;
const STAT_ORDER = [
['pac', 'PAC'],
['sho', 'SHO'],
['pas', 'PAS'],
['dri', 'DRI'],
['def', 'DEF'],
['phy', 'PHY'],
];
const SUPPORTED_LOCALES = ['en', 'ru', 'zh', 'es', 'pt', 'de', 'fr', 'ja', 'ko', 'pl'];
/** Native labels for the language picker (not translated per UI locale). */
const LOCALE_NATIVE_NAMES = {
en: 'English',
ru: 'Русский',
zh: '中文',
es: 'Español',
pt: 'Português',
de: 'Deutsch',
fr: 'Français',
ja: '日本語',
ko: '한국어',
pl: 'Polski',
};
const TRANSLATIONS = {
en: {
loading: 'Scouting…',
loadError: 'Scout failed',
notFound: 'Not scouted',
overall: 'OVR',
position: 'POS',
finish: 'Finish',
archetype: 'Archetype',
language: 'Language',
playstyles: 'Playstyles',
metrics: 'Scouting metrics',
openReport: 'Open full scout report',
duel: 'Duel a rival',
menuSettings: 'GitHub GitFut — Settings',
navMenuItem: 'GitFut settings',
panelTitle: 'GitFut',
panelSubtitle: 'Scouting · cache · display',
close: 'Close',
cancel: 'Cancel',
save: 'Save',
saveReload: 'Save & Reload page',
sectionDisplay: 'Display',
uiLanguage: 'Language',
uiLanguageHint: 'Language for GitFut panels, hovercards, and settings. Auto follows the browser.',
uiLanguageAuto: 'Auto (browser)',
sectionCache: 'Cache',
showHovercard: 'Show GitFut in avatar hovercards',
showHovercardHint: 'Injects OVR, position, and stats into GitHub’s user popover on avatar hover.',
cardTheme: 'Card style',
cardThemeHint: 'Standard, GitHub (subtle Primer look), FIFA (metallic), or Neon (glow).',
cardThemeStandard: 'Standard',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: 'Neon',
on: 'ON',
off: 'OFF',
cacheHours: 'Cache duration (hours)',
cacheHoursHint: 'How long to reuse GitFut API results. 0 disables cache.',
clearCache: 'Clear cache',
cacheCleared: 'Cache cleared ({count})',
cacheEmpty: 'Cache is empty',
cacheClearHint: 'Removes all stored GitFut lookups from this browser profile.',
version: 'Version {version}',
cacheCards: '{count} cards cached',
cacheStorage: '{used} used · {free} free',
cacheStorageHint: 'Local card-cache size vs a soft {budget} budget.',
repoLink: 'GitHub',
repoAbout: 'Source code, updates, and issue reports',
skillMoves: 'Skill moves',
weakFoot: 'Weak foot',
workRate: 'Work rate',
style: 'Style',
},
ru: {
loading: 'Скаутинг…',
loadError: 'Ошибка скаутинга',
notFound: 'Нет карточки',
overall: 'OVR',
position: 'ПОЗ',
finish: 'Тир',
archetype: 'Архетип',
language: 'Язык',
playstyles: 'Стили',
metrics: 'Метрики скаутинга',
openReport: 'Открыть полный отчёт',
duel: 'Дуэль с соперником',
menuSettings: 'GitHub GitFut — Настройки',
navMenuItem: 'Настройки GitFut',
panelTitle: 'GitFut',
panelSubtitle: 'Скаутинг · кэш · отображение',
close: 'Закрыть',
cancel: 'Отмена',
save: 'Сохранить',
saveReload: 'Сохранить и перезагрузить',
sectionDisplay: 'Отображение',
uiLanguage: 'Язык',
uiLanguageHint: 'Язык панелей GitFut, hovercard и настроек. Авто — как в браузере.',
uiLanguageAuto: 'Авто (браузер)',
sectionCache: 'Кэш',
showHovercard: 'GitFut в поповере аватара',
showHovercardHint: 'Добавляет OVR, позицию и статы в нативный hovercard GitHub при наведении на аватар.',
cardTheme: 'Оформление карточек',
cardThemeHint: 'Стандартный, GitHub (спокойный Primer), FIFA (металл) или Неон (свечение).',
cardThemeStandard: 'Стандартный вид',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: 'Неон',
on: 'ВКЛ',
off: 'ВЫКЛ',
cacheHours: 'Время кэша (часы)',
cacheHoursHint: 'Как долго переиспользовать ответы API. 0 отключает кэш.',
clearCache: 'Очистить кэш',
cacheCleared: 'Кэш очищен ({count})',
cacheEmpty: 'Кэш пуст',
cacheClearHint: 'Удаляет все сохранённые запросы GitFut в этом профиле браузера.',
version: 'Версия {version}',
cacheCards: 'Карточек в кэше: {count}',
cacheStorage: '{used} занято · {free} свободно',
cacheStorageHint: 'Размер локального кэша карточек относительно лимита {budget}.',
repoLink: 'GitHub',
repoAbout: 'Исходники, обновления и баг-репорты',
skillMoves: 'Финты',
weakFoot: 'Слабая нога',
workRate: 'Работоспособность',
style: 'Стиль',
},
zh: {
loading: '球探中…',
loadError: '球探失败',
notFound: '无卡片',
overall: 'OVR',
position: '位置',
finish: '品质',
archetype: '原型',
language: '语言',
playstyles: '风格',
metrics: '球探指标',
openReport: '打开完整报告',
duel: '对决对手',
menuSettings: 'GitHub GitFut — 设置',
navMenuItem: 'GitFut 设置',
panelTitle: 'GitFut',
panelSubtitle: '球探 · 缓存 · 显示',
close: '关闭',
cancel: '取消',
save: '保存',
saveReload: '保存并刷新',
sectionDisplay: '显示',
uiLanguage: '语言',
uiLanguageHint: 'GitFut 面板、悬停卡片与设置的界面语言。自动跟随浏览器。',
uiLanguageAuto: '自动(浏览器)',
sectionCache: '缓存',
showHovercard: '在头像悬停卡片中显示 GitFut',
showHovercardHint: '悬停头像时在 GitHub 用户弹层中注入 OVR、位置与属性。',
cardTheme: '卡片样式',
cardThemeHint: '资料页与悬停卡片球探面板的视觉风格。',
cardThemeStandard: '标准',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: '霓虹',
on: '开',
off: '关',
cacheHours: '缓存时长(小时)',
cacheHoursHint: '复用 API 结果的时长。0 禁用缓存。',
clearCache: '清空缓存',
cacheCleared: '已清空 ({count})',
cacheEmpty: '缓存为空',
cacheClearHint: '删除本浏览器配置中保存的 GitFut 查询。',
version: '版本 {version}',
cacheCards: '已缓存 {count} 张',
cacheStorage: '已用 {used} · 剩余 {free}',
cacheStorageHint: '本地卡片缓存相对软上限 {budget}。',
repoLink: 'GitHub',
repoAbout: '源码、更新与问题反馈',
skillMoves: '花式',
weakFoot: '逆足',
workRate: '积极性',
style: '风格',
},
es: {
loading: 'Scouting…',
loadError: 'Scout fallido',
notFound: 'Sin carta',
overall: 'OVR',
position: 'POS',
finish: 'Acabado',
archetype: 'Arquetipo',
language: 'Lenguaje',
playstyles: 'Estilos',
metrics: 'Métricas',
openReport: 'Abrir informe completo',
duel: 'Duelo',
menuSettings: 'GitHub GitFut — Ajustes',
navMenuItem: 'Ajustes de GitFut',
panelTitle: 'GitFut',
panelSubtitle: 'Scouting · caché · pantalla',
close: 'Cerrar',
cancel: 'Cancelar',
save: 'Guardar',
saveReload: 'Guardar y recargar',
sectionDisplay: 'Pantalla',
uiLanguage: 'Idioma',
uiLanguageHint: 'Idioma de paneles GitFut, hovercards y ajustes. Auto sigue el navegador.',
uiLanguageAuto: 'Auto (navegador)',
sectionCache: 'Caché',
showHovercard: 'Mostrar GitFut en hovercards de avatar',
showHovercardHint: 'Inyecta OVR, posición y stats en el popover nativo de GitHub.',
cardTheme: 'Estilo de carta',
cardThemeHint: 'Acabado visual del panel en perfil y hovercard.',
cardThemeStandard: 'Estándar',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: 'Neón',
on: 'ON',
off: 'OFF',
cacheHours: 'Duración de caché (horas)',
cacheHoursHint: 'Cuánto reutilizar resultados de la API. 0 desactiva.',
clearCache: 'Vaciar caché',
cacheCleared: 'Caché vaciada ({count})',
cacheEmpty: 'Caché vacía',
cacheClearHint: 'Elimina búsquedas GitFut de este perfil.',
version: 'Versión {version}',
cacheCards: '{count} cartas en caché',
cacheStorage: '{used} usados · {free} libres',
cacheStorageHint: 'Tamaño de la caché local frente a un tope blando de {budget}.',
repoLink: 'GitHub',
repoAbout: 'Código, actualizaciones e issues',
skillMoves: 'Regates',
weakFoot: 'Pie malo',
workRate: 'Trabajo',
style: 'Estilo',
},
pt: {
loading: 'Scouting…',
loadError: 'Scout falhou',
notFound: 'Sem carta',
overall: 'OVR',
position: 'POS',
finish: 'Acabamento',
archetype: 'Arquétipo',
language: 'Linguagem',
playstyles: 'Estilos',
metrics: 'Métricas',
openReport: 'Abrir relatório completo',
duel: 'Duelo',
menuSettings: 'GitHub GitFut — Configurações',
navMenuItem: 'Configurações do GitFut',
panelTitle: 'GitFut',
panelSubtitle: 'Scouting · cache · exibição',
close: 'Fechar',
cancel: 'Cancelar',
save: 'Salvar',
saveReload: 'Salvar e recarregar',
sectionDisplay: 'Exibição',
uiLanguage: 'Idioma',
uiLanguageHint: 'Idioma dos painéis GitFut, hovercards e configurações. Auto segue o navegador.',
uiLanguageAuto: 'Auto (navegador)',
sectionCache: 'Cache',
showHovercard: 'Mostrar GitFut nos hovercards de avatar',
showHovercardHint: 'Injeta OVR, posição e stats no popover nativo do GitHub.',
cardTheme: 'Estilo da carta',
cardThemeHint: 'Visual do painel no perfil e no hovercard.',
cardThemeStandard: 'Padrão',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: 'Neon',
on: 'ON',
off: 'OFF',
cacheHours: 'Duração do cache (horas)',
cacheHoursHint: 'Por quanto tempo reutilizar a API. 0 desativa.',
clearCache: 'Limpar cache',
cacheCleared: 'Cache limpo ({count})',
cacheEmpty: 'Cache vazio',
cacheClearHint: 'Remove buscas GitFut deste perfil.',
version: 'Versão {version}',
cacheCards: '{count} cartas em cache',
cacheStorage: '{used} usados · {free} livres',
cacheStorageHint: 'Tamanho do cache local vs orçamento suave de {budget}.',
repoLink: 'GitHub',
repoAbout: 'Código, atualizações e issues',
skillMoves: 'Habilidades',
weakFoot: 'Pé fraco',
workRate: 'Dedicação',
style: 'Estilo',
},
de: {
loading: 'Scouting…',
loadError: 'Scout fehlgeschlagen',
notFound: 'Keine Karte',
overall: 'OVR',
position: 'POS',
finish: 'Finish',
archetype: 'Archetyp',
language: 'Sprache',
playstyles: 'Spielstile',
metrics: 'Scout-Metriken',
openReport: 'Vollen Bericht öffnen',
duel: 'Duell',
menuSettings: 'GitHub GitFut — Einstellungen',
navMenuItem: 'GitFut-Einstellungen',
panelTitle: 'GitFut',
panelSubtitle: 'Scouting · Cache · Anzeige',
close: 'Schließen',
cancel: 'Abbrechen',
save: 'Speichern',
saveReload: 'Speichern & neu laden',
sectionDisplay: 'Anzeige',
uiLanguage: 'Sprache',
uiLanguageHint: 'Sprache für GitFut-Panels, Hovercards und Einstellungen. Auto folgt dem Browser.',
uiLanguageAuto: 'Auto (Browser)',
sectionCache: 'Cache',
showHovercard: 'GitFut in Avatar-Hovercards zeigen',
showHovercardHint: 'Fügt OVR, Position und Stats in GitHubs User-Popover ein.',
cardTheme: 'Kartenstil',
cardThemeHint: 'Optik der Scout-Panels auf Profil und Hovercard.',
cardThemeStandard: 'Standard',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: 'Neon',
on: 'AN',
off: 'AUS',
cacheHours: 'Cache-Dauer (Stunden)',
cacheHoursHint: 'Wie lange API-Ergebnisse wiederverwendet werden. 0 = aus.',
clearCache: 'Cache leeren',
cacheCleared: 'Cache geleert ({count})',
cacheEmpty: 'Cache ist leer',
cacheClearHint: 'Löscht gespeicherte GitFut-Lookups.',
version: 'Version {version}',
cacheCards: '{count} Karten im Cache',
cacheStorage: '{used} belegt · {free} frei',
cacheStorageHint: 'Lokaler Karten-Cache gegenüber Soft-Limit {budget}.',
repoLink: 'GitHub',
repoAbout: 'Quellcode, Updates und Issues',
skillMoves: 'Tricks',
weakFoot: 'Schwachfuß',
workRate: 'Einsatz',
style: 'Stil',
},
fr: {
loading: 'Scouting…',
loadError: 'Échec du scout',
notFound: 'Pas de carte',
overall: 'OVR',
position: 'POS',
finish: 'Finition',
archetype: 'Archétype',
language: 'Langage',
playstyles: 'Styles',
metrics: 'Métriques',
openReport: 'Ouvrir le rapport complet',
duel: 'Duel',
menuSettings: 'GitHub GitFut — Réglages',
navMenuItem: 'Réglages GitFut',
panelTitle: 'GitFut',
panelSubtitle: 'Scouting · cache · affichage',
close: 'Fermer',
cancel: 'Annuler',
save: 'Enregistrer',
saveReload: 'Enregistrer et recharger',
sectionDisplay: 'Affichage',
uiLanguage: 'Langue',
uiLanguageHint: 'Langue des panneaux GitFut, hovercards et réglages. Auto suit le navigateur.',
uiLanguageAuto: 'Auto (navigateur)',
sectionCache: 'Cache',
showHovercard: 'Afficher GitFut dans les hovercards d’avatar',
showHovercardHint: 'Injecte OVR, poste et stats dans le popover natif de GitHub.',
cardTheme: 'Style de carte',
cardThemeHint: 'Apparence des panneaux scout sur profil et hovercard.',
cardThemeStandard: 'Standard',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: 'Néon',
on: 'ON',
off: 'OFF',
cacheHours: 'Durée du cache (heures)',
cacheHoursHint: 'Durée de réutilisation de l’API. 0 désactive.',
clearCache: 'Vider le cache',
cacheCleared: 'Cache vidé ({count})',
cacheEmpty: 'Cache vide',
cacheClearHint: 'Supprime les recherches GitFut de ce profil.',
version: 'Version {version}',
cacheCards: '{count} cartes en cache',
cacheStorage: '{used} utilisés · {free} libres',
cacheStorageHint: 'Taille du cache local vs budget souple de {budget}.',
repoLink: 'GitHub',
repoAbout: 'Code, mises à jour et issues',
skillMoves: 'Gestes',
weakFoot: 'Pied faible',
workRate: 'Intensité',
style: 'Style',
},
ja: {
loading: 'スカウティング…',
loadError: 'スカウト失敗',
notFound: 'カードなし',
overall: 'OVR',
position: 'ポジ',
finish: 'フィニッシュ',
archetype: 'アーキタイプ',
language: '言語',
playstyles: 'プレースタイル',
metrics: 'スカウト指標',
openReport: 'フルレポートを開く',
duel: 'デュエル',
menuSettings: 'GitHub GitFut — 設定',
navMenuItem: 'GitFutの設定',
panelTitle: 'GitFut',
panelSubtitle: 'スカウト · キャッシュ · 表示',
close: '閉じる',
cancel: 'キャンセル',
save: '保存',
saveReload: '保存して再読込',
sectionDisplay: '表示',
uiLanguage: '言語',
uiLanguageHint: 'GitFutパネル・ホバーカード・設定の表示言語。自動はブラウザに合わせます。',
uiLanguageAuto: '自動(ブラウザ)',
sectionCache: 'キャッシュ',
showHovercard: 'アバターホバーカードにGitFutを表示',
showHovercardHint: 'アバターホバー時にGitHubのユーザーポップオーバーへOVR等を挿入。',
cardTheme: 'カードスタイル',
cardThemeHint: 'プロフィールとホバーカードの見た目。',
cardThemeStandard: 'スタンダード',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: 'ネオン',
on: 'ON',
off: 'OFF',
cacheHours: 'キャッシュ時間(時間)',
cacheHoursHint: 'API結果の再利用時間。0で無効。',
clearCache: 'キャッシュ削除',
cacheCleared: '削除しました ({count})',
cacheEmpty: 'キャッシュは空です',
cacheClearHint: 'このブラウザのGitFut照会を削除します。',
version: 'バージョン {version}',
cacheCards: 'キャッシュ済み: {count} 件',
cacheStorage: '使用 {used} · 空き {free}',
cacheStorageHint: 'ローカルカードキャッシュとソフト上限 {budget}。',
repoLink: 'GitHub',
repoAbout: 'ソース・更新・Issue',
skillMoves: 'スキル',
weakFoot: 'ウィークフット',
workRate: 'ワークレート',
style: 'スタイル',
},
ko: {
loading: '스카우팅…',
loadError: '스카우트 실패',
notFound: '카드 없음',
overall: 'OVR',
position: '포지션',
finish: '피니시',
archetype: '아키타입',
language: '언어',
playstyles: '플레이스타일',
metrics: '스카우트 지표',
openReport: '전체 리포트 열기',
duel: '듀얼',
menuSettings: 'GitHub GitFut — 설정',
navMenuItem: 'GitFut 설정',
panelTitle: 'GitFut',
panelSubtitle: '스카우팅 · 캐시 · 표시',
close: '닫기',
cancel: '취소',
save: '저장',
saveReload: '저장 후 새로고침',
sectionDisplay: '표시',
uiLanguage: '언어',
uiLanguageHint: 'GitFut 패널·호버카드·설정의 표시 언어. 자동은 브라우저를 따릅니다.',
uiLanguageAuto: '자동 (브라우저)',
sectionCache: '캐시',
showHovercard: '아바타 호버카드에 GitFut 표시',
showHovercardHint: '아바타 호버 시 GitHub 사용자 팝오버에 OVR·포지션·스탯을 넣습니다.',
cardTheme: '카드 스타일',
cardThemeHint: '프로필·호버카드 스카우트 패널의 시각 스타일.',
cardThemeStandard: '기본',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: '네온',
on: 'ON',
off: 'OFF',
cacheHours: '캐시 시간(시간)',
cacheHoursHint: 'API 결과 재사용 시간. 0이면 비활성.',
clearCache: '캐시 비우기',
cacheCleared: '비움 ({count})',
cacheEmpty: '캐시가 비어 있음',
cacheClearHint: '이 브라우저의 GitFut 조회를 삭제합니다.',
version: '버전 {version}',
cacheCards: '캐시된 카드 {count}개',
cacheStorage: '사용 {used} · 남음 {free}',
cacheStorageHint: '로컬 카드 캐시와 소프트 한도 {budget}.',
repoLink: 'GitHub',
repoAbout: '소스, 업데이트, 이슈',
skillMoves: '스킬',
weakFoot: '약발',
workRate: '활동량',
style: '스타일',
},
pl: {
loading: 'Scouting…',
loadError: 'Scout nieudany',
notFound: 'Brak karty',
overall: 'OVR',
position: 'POZ',
finish: 'Wykończenie',
archetype: 'Archetyp',
language: 'Język',
playstyles: 'Style gry',
metrics: 'Metryki',
openReport: 'Otwórz pełny raport',
duel: 'Pojedynek',
menuSettings: 'GitHub GitFut — Ustawienia',
navMenuItem: 'Ustawienia GitFut',
panelTitle: 'GitFut',
panelSubtitle: 'Scouting · cache · wyświetlanie',
close: 'Zamknij',
cancel: 'Anuluj',
save: 'Zapisz',
saveReload: 'Zapisz i odśwież',
sectionDisplay: 'Wyświetlanie',
uiLanguage: 'Język',
uiLanguageHint: 'Język paneli GitFut, hovercard i ustawień. Auto — według przeglądarki.',
uiLanguageAuto: 'Auto (przeglądarka)',
sectionCache: 'Cache',
showHovercard: 'Pokaż GitFut w hovercardach awatara',
showHovercardHint: 'Wstawia OVR, pozycję i staty do natywnego popovera GitHub.',
cardTheme: 'Styl karty',
cardThemeHint: 'Wygląd paneli scout na profilu i w hovercard.',
cardThemeStandard: 'Standardowy',
cardThemeGithub: 'GitHub',
cardThemeFifa: 'FIFA',
cardThemeNeon: 'Neon',
on: 'WŁ',
off: 'WYŁ',
cacheHours: 'Czas cache (godziny)',
cacheHoursHint: 'Jak długo ponownie używać API. 0 wyłącza.',
clearCache: 'Wyczyść cache',
cacheCleared: 'Wyczyszczono ({count})',
cacheEmpty: 'Cache pusty',
cacheClearHint: 'Usuwa zapisane zapytania GitFut.',
version: 'Wersja {version}',
cacheCards: 'Karty w cache: {count}',
cacheStorage: '{used} zajęte · {free} wolne',
cacheStorageHint: 'Rozmiar lokalnego cache względem limitu {budget}.',
repoLink: 'GitHub',
repoAbout: 'Kod, aktualizacje i zgłoszenia',
skillMoves: 'Sztuczki',
weakFoot: 'Słaba noga',
workRate: 'Zaangażowanie',
style: 'Styl',
},
};
function detectBrowserLocale() {
const candidates = [navigator.language, ...(navigator.languages || [])];
for (const candidate of candidates) {
const raw = String(candidate).toLowerCase();
const primary = raw.split('-')[0];
if (SUPPORTED_LOCALES.includes(primary)) return primary;
if (raw.startsWith('zh')) return 'zh';
if (raw.startsWith('pt')) return 'pt';
}
return 'en';
}
function normalizeUiLocale(value) {
const raw = String(value ?? 'auto').toLowerCase().trim();
if (!raw || raw === 'auto') return 'auto';
const primary = raw.split('-')[0];
if (SUPPORTED_LOCALES.includes(primary)) return primary;
if (raw.startsWith('zh')) return 'zh';
if (raw.startsWith('pt')) return 'pt';
return 'auto';
}
function resolveActiveLocale(pref) {
const normalized = normalizeUiLocale(pref);
return normalized === 'auto' ? detectBrowserLocale() : normalized;
}
/** @type {typeof DEFAULT_SETTINGS} */
let settings = loadSettings();
let locale = resolveActiveLocale(settings.locale);
let panelOpen = false;
function t(key, vars) {
let text = TRANSLATIONS[locale]?.[key] ?? TRANSLATIONS.en[key] ?? key;
if (vars && typeof vars === 'object') {
for (const [name, value] of Object.entries(vars)) {
text = text.replace(new RegExp(`\\{${name}\\}`, 'g'), String(value));
}
}
return text;
}
function normalizeCacheHours(value) {
const n = Math.round(Number(value));
if (!Number.isFinite(n) || n < 0) return DEFAULT_SETTINGS.cacheHours;
return Math.min(n, CACHE_HOURS_MAX);
}
function normalizeCardTheme(value) {
const key = String(value || '').toLowerCase();
return CARD_THEMES.includes(key) ? key : DEFAULT_SETTINGS.cardTheme;
}
function applyCardTheme() {
const theme = normalizeCardTheme(settings.cardTheme);
document.documentElement.dataset.gfCardTheme = theme;
document.documentElement.classList.toggle('gf-card-theme-github', theme === 'github');
document.documentElement.classList.toggle('gf-card-theme-fifa', theme === 'fifa');
document.documentElement.classList.toggle('gf-card-theme-neon', theme === 'neon');
}
function loadSettings() {
const raw = GM_getValue(SETTINGS_KEY, null);
if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS };
return {
...DEFAULT_SETTINGS,
...raw,
cacheHours: normalizeCacheHours(raw.cacheHours),
showHovercard:
typeof raw.showHovercard === 'boolean'
? raw.showHovercard
: raw.showInlineBadges !== false,
cardTheme: normalizeCardTheme(raw.cardTheme),
locale: normalizeUiLocale(raw.locale),
};
}
function saveSettings(next) {
settings = {
...settings,
...next,
cacheHours: normalizeCacheHours(next.cacheHours ?? settings.cacheHours),
showHovercard:
typeof next.showHovercard === 'boolean'
? next.showHovercard
: settings.showHovercard !== false,
cardTheme: normalizeCardTheme(
next.cardTheme !== undefined ? next.cardTheme : settings.cardTheme
),
locale: normalizeUiLocale(
next.locale !== undefined ? next.locale : settings.locale
),
};
GM_setValue(SETTINGS_KEY, settings);
locale = resolveActiveLocale(settings.locale);
applyCardTheme();
updateSettingsMenuState();
}
function getCacheTtlMs() {
const hours = normalizeCacheHours(settings.cacheHours);
return hours > 0 ? hours * 60 * 60 * 1000 : 0;
}
/** @type {Map<string, Promise<object|null>>} */
const inflight = new Map();
/** @type {Array<{ task: () => Promise<unknown>, resolve: Function, reject: Function }>} */
const queue = [];
let activeRequests = 0;
/** @type {Record<string, unknown>|null} */
let memoryCache = null;
let cacheDirty = false;
let cachePersistTimer = null;
let scanTimer = null;
let mutationObserver = null;
let hovercardObserver = null;
/** @type {number} */
let hovercardSeq = 0;
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function finishMeta(finish) {
return FINISH_COLORS[finish] || FINISH_COLORS.bronze;
}
function isValidUsername(login) {
if (!login || typeof login !== 'string') return false;
const name = login.trim();
if (name.length < 1 || name.length > 39) return false;
if (RESERVED_PATHS.has(name.toLowerCase())) return false;
return /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(name);
}
function getLoggedInUsername() {
const meta = document.querySelector('meta[name="user-login"]');
const login = meta?.getAttribute('content')?.trim();
return login && isValidUsername(login) ? login : null;
}
/** @param {string} opponentLogin */
function buildDuelUrl(opponentLogin) {
const opponent = String(opponentLogin || '').trim();
if (!opponent || !isValidUsername(opponent)) {
return `${SITE_BASE}`;
}
const me = getLoggedInUsername();
if (me && me.toLowerCase() !== opponent.toLowerCase()) {
return `${SITE_BASE}/${encodeURIComponent(me)}/vs/${encodeURIComponent(opponent)}`;
}
return `${SITE_BASE}/${encodeURIComponent(opponent)}/vs`;
}
function getProfileUsername() {
if (location.hostname === 'gist.github.com') {
const parts = location.pathname.split('/').filter(Boolean);
return parts[0] && isValidUsername(parts[0]) ? parts[0] : null;
}
const parts = location.pathname.split('/').filter(Boolean);
if (parts.length !== 1) return null;
return isValidUsername(parts[0]) ? parts[0] : null;
}
function extractUsernameFromHref(href) {
try {
const url = new URL(href, location.origin);
if (!/^(?:www\.)?github\.com$/.test(url.hostname) && url.hostname !== 'gist.github.com') {
return null;
}
const parts = url.pathname.split('/').filter(Boolean);
if (!parts.length) return null;
if (parts.length === 1 || (url.hostname === 'gist.github.com' && parts.length >= 1)) {
return isValidUsername(parts[0]) ? parts[0] : null;
}
return null;
} catch {
return null;
}
}
function loadMemoryCache() {
if (memoryCache) return memoryCache;
const raw = GM_getValue(CACHE_KEY, null);
memoryCache = raw && typeof raw === 'object' ? raw : {};
return memoryCache;
}
function persistCacheNow() {
if (!cacheDirty || !memoryCache) return;
GM_setValue(CACHE_KEY, memoryCache);
cacheDirty = false;
}
function schedulePersistCache() {
cacheDirty = true;
if (cachePersistTimer) clearTimeout(cachePersistTimer);
cachePersistTimer = setTimeout(() => {
cachePersistTimer = null;
persistCacheNow();
}, CACHE_PERSIST_MS);
}
function getCached(login) {
const cache = loadMemoryCache();
const key = login.toLowerCase();
const entry = cache[key];
if (!entry || typeof entry !== 'object') return null;
const ttl = getCacheTtlMs();
if (ttl <= 0) return null;
if (Date.now() - (entry.ts || 0) > ttl) {
delete cache[key];
schedulePersistCache();
return null;
}
return entry.data ?? null;
}
function setCached(login, data) {
const cache = loadMemoryCache();
cache[login.toLowerCase()] = { ts: Date.now(), data };
schedulePersistCache();
}
function clearCardCache() {
const cache = loadMemoryCache();
const count = Object.keys(cache).length;
memoryCache = {};
cacheDirty = true;
persistCacheNow();
return count;
}
function formatBytes(bytes) {
const n = Math.max(0, Number(bytes) || 0);
if (n < 1024) return `${Math.round(n)} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(n < 10 * 1024 ? 1 : 0)} KB`;
return `${(n / (1024 * 1024)).toFixed(2)} MB`;
}
function getCacheStats() {
const cache = loadMemoryCache();
const count = Object.keys(cache).length;
let used = 0;
try {
used = new TextEncoder().encode(JSON.stringify(cache)).length;
} catch {
used = JSON.stringify(cache).length;
}
const budget = CACHE_BUDGET_BYTES;
const free = Math.max(0, budget - used);
const usedRatio = budget > 0 ? Math.min(1, used / budget) : 0;
const freeRatio = budget > 0 ? Math.max(0, 1 - usedRatio) : 0;
return { count, used, free, budget, usedRatio, freeRatio };
}
function updateCacheStatsUI() {
const panel = document.getElementById('gf-panel');
if (!panel) return;
const stats = getCacheStats();
const countEl = panel.querySelector('#gf-cache-count');
const storageEl = panel.querySelector('#gf-cache-storage');
const bar = panel.querySelector('#gf-cache-bar');
const fill = panel.querySelector('#gf-cache-bar-fill');
if (countEl) countEl.textContent = t('cacheCards', { count: stats.count });
if (storageEl) {
storageEl.textContent = t('cacheStorage', {
used: formatBytes(stats.used),
free: formatBytes(stats.free),
});
}
if (bar && fill) {
const pct = Math.round(stats.freeRatio * 100);
fill.style.width = `${pct}%`;
bar.setAttribute('aria-valuenow', String(pct));
bar.setAttribute('aria-valuetext', t('cacheStorage', {
used: formatBytes(stats.used),
free: formatBytes(stats.free),
}));
bar.classList.toggle('is-warn', stats.usedRatio >= 0.7 && stats.usedRatio < 0.9);
bar.classList.toggle('is-full', stats.usedRatio >= 0.9);
}
}
function enqueue(task) {
return new Promise((resolve, reject) => {
queue.push({ task, resolve, reject });
pumpQueue();
});
}
function pumpQueue() {
while (activeRequests < MAX_CONCURRENT && queue.length) {
const item = queue.shift();
activeRequests += 1;
Promise.resolve()
.then(() => new Promise((r) => setTimeout(r, REQUEST_DELAY_MS)))
.then(() => item.task())
.then(item.resolve, item.reject)
.finally(() => {
activeRequests -= 1;
pumpQueue();
});
}
}
function gmGetJson(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
headers: { Accept: 'application/json' },
onload(res) {
if (res.status === 404) {
resolve(null);
return;
}
if (res.status < 200 || res.status >= 300) {
reject(new Error(`HTTP ${res.status}`));
return;
}
try {
resolve(JSON.parse(res.responseText));
} catch (err) {
reject(err);
}
},
onerror: () => reject(new Error('Network error')),
ontimeout: () => reject(new Error('Timeout')),
});
});
}
function fetchCard(login) {
const key = login.toLowerCase();
const cached = getCached(login);
if (cached !== null && cached !== undefined) {
return Promise.resolve(cached);
}
if (inflight.has(key)) return inflight.get(key);
const promise = enqueue(() => gmGetJson(`${API_BASE}/${encodeURIComponent(login)}`))
.then((data) => {
setCached(login, data);
return data;
})
.finally(() => inflight.delete(key));
inflight.set(key, promise);
return promise;
}
GM_addStyle(`
:root {
--gf-radius: 16px;
--gf-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
--gf-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}
#${PANEL_ID} {
position: relative;
isolation: isolate;
font-family: var(--gf-font);
margin: 0 0 16px;
border: 1px solid color-mix(
in srgb,
var(--gf-finish-accent, #CD7F32) 48%,
var(--borderColor-default, var(--color-border-default, #d0d7de))
);
border-radius: var(--gf-radius);
background:
radial-gradient(
120% 80% at 0% 0%,
var(--gf-finish-soft, rgba(205,127,50,0.16)),
transparent 55%
),
radial-gradient(
90% 70% at 100% 100%,
color-mix(in srgb, var(--gf-finish-glow, rgba(205,127,50,0.28)) 32%, transparent),
transparent 60%
),
var(--bgColor-default, var(--color-canvas-default, #fff));
overflow: hidden;
box-shadow:
var(--gf-panel-outer-glow, 0 0 0 transparent),
var(--gf-panel-ring, 0 0 0 1px transparent),
0 8px 24px rgba(1, 4, 9, 0.1);
transition: box-shadow 0.25s ease, border-color 0.25s ease, background 0.25s ease;
}
#${PANEL_ID}.is-expanding {
transition:
height 0.42s cubic-bezier(0.22, 1, 0.36, 1),
margin-bottom 0.42s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.3s ease,
box-shadow 0.25s ease,
border-color 0.25s ease,
background 0.25s ease;
will-change: height, opacity, margin-bottom;
}
#${PANEL_ID} > .gf-panel-theme-fx {
position: absolute;
inset: 0;
border-radius: inherit;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
#${PANEL_ID} > .gf-panel-theme-fx + * {
position: relative;
z-index: 1;
}
.gf-panel-theme-fx__shine {
position: absolute;
inset: 0;
background: linear-gradient(
115deg,
transparent 35%,
var(--gf-finish-shine, rgba(255,255,255,0.35)) 48%,
transparent 62%
);
background-size: 220% 100%;
background-position: 120% 0;
opacity: 0;
}
#${PANEL_ID}.gf-panel-finish--bronze {
--gf-hc-shine-opacity: 0;
--gf-panel-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 24%, transparent);
--gf-panel-outer-glow: 0 0 0 transparent;
}
#${PANEL_ID}.gf-panel-finish--silver {
--gf-hc-shine-opacity: 0.16;
--gf-panel-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 36%, transparent);
--gf-panel-outer-glow: 0 0 18px color-mix(in srgb, var(--gf-finish-glow) 40%, transparent);
}
#${PANEL_ID}.gf-panel-finish--gold {
--gf-hc-shine-opacity: 0.3;
--gf-panel-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 50%, transparent);
--gf-panel-outer-glow:
0 0 22px color-mix(in srgb, var(--gf-finish-glow) 65%, transparent),
0 0 40px color-mix(in srgb, var(--gf-finish-glow) 24%, transparent);
}
#${PANEL_ID}.gf-panel-finish--totw {
--gf-hc-shine-opacity: 0.26;
--gf-panel-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 55%, transparent);
--gf-panel-outer-glow:
0 0 24px color-mix(in srgb, var(--gf-finish-glow) 70%, transparent),
0 0 48px color-mix(in srgb, var(--gf-finish-glow) 28%, transparent);
}
#${PANEL_ID}.gf-panel-finish--toty {
--gf-hc-shine-opacity: 0.36;
--gf-panel-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 60%, transparent);
--gf-panel-outer-glow:
0 0 28px color-mix(in srgb, var(--gf-finish-glow) 75%, transparent),
0 0 56px color-mix(in srgb, var(--gf-finish-glow) 32%, transparent);
}
#${PANEL_ID}.gf-panel-finish--icon,
#${PANEL_ID}.gf-panel-finish--founder {
--gf-hc-shine-opacity: 0.42;
--gf-panel-ring:
inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 68%, transparent),
inset 0 0 28px color-mix(in srgb, var(--gf-finish-soft) 50%, transparent);
--gf-panel-outer-glow:
0 0 30px color-mix(in srgb, var(--gf-finish-glow) 80%, transparent),
0 0 60px color-mix(in srgb, var(--gf-finish-glow) 36%, transparent);
}
#${PANEL_ID}.gf-panel-finish--gold .gf-panel-theme-fx__shine,
#${PANEL_ID}.gf-panel-finish--totw .gf-panel-theme-fx__shine,
#${PANEL_ID}.gf-panel-finish--toty .gf-panel-theme-fx__shine,
#${PANEL_ID}.gf-panel-finish--icon .gf-panel-theme-fx__shine,
#${PANEL_ID}.gf-panel-finish--founder .gf-panel-theme-fx__shine {
animation: gf-hc-shine-sweep 1.8s ease-in-out 1 forwards;
}
#${PANEL_ID}.gf-panel-finish--totw {
animation: gf-panel-pulse-totw 2.4s ease-in-out infinite;
}
#${PANEL_ID}.gf-panel-finish--icon {
animation: gf-panel-pulse-icon 2.8s ease-in-out infinite;
}
#${PANEL_ID}.gf-panel-finish--founder {
animation: gf-panel-pulse-founder 2.2s ease-in-out infinite;
}
@keyframes gf-panel-pulse-totw {
0%, 100% {
box-shadow:
0 0 20px color-mix(in srgb, var(--gf-finish-glow) 50%, transparent),
0 0 36px color-mix(in srgb, var(--gf-finish-glow) 20%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 50%, transparent),
0 8px 24px rgba(1, 4, 9, 0.1);
}
50% {
box-shadow:
0 0 32px color-mix(in srgb, var(--gf-finish-glow) 80%, transparent),
0 0 56px color-mix(in srgb, var(--gf-finish-glow) 36%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 72%, transparent),
0 10px 28px rgba(1, 4, 9, 0.14);
}
}
@keyframes gf-panel-pulse-icon {
0%, 100% {
box-shadow:
0 0 26px color-mix(in srgb, var(--gf-finish-glow) 55%, transparent),
0 0 46px color-mix(in srgb, var(--gf-finish-accent) 24%, transparent),
inset 0 0 22px color-mix(in srgb, var(--gf-finish-soft) 38%, transparent),
0 8px 24px rgba(1, 4, 9, 0.12);
}
50% {
box-shadow:
0 0 40px color-mix(in srgb, var(--gf-finish-glow) 88%, transparent),
0 0 68px color-mix(in srgb, var(--gf-finish-accent) 42%, transparent),
inset 0 0 34px color-mix(in srgb, var(--gf-finish-soft) 52%, transparent),
0 12px 30px rgba(1, 4, 9, 0.16);
}
}
@keyframes gf-panel-pulse-founder {
0%, 100% {
box-shadow:
0 0 28px color-mix(in srgb, var(--gf-finish-glow) 60%, transparent),
0 0 50px color-mix(in srgb, var(--gf-finish-accent) 26%, transparent),
inset 0 0 24px color-mix(in srgb, var(--gf-finish-soft) 42%, transparent),
0 8px 24px rgba(1, 4, 9, 0.12);
}
50% {
box-shadow:
0 0 44px color-mix(in srgb, var(--gf-finish-glow) 92%, transparent),
0 0 74px color-mix(in srgb, var(--gf-finish-accent) 46%, transparent),
inset 0 0 38px color-mix(in srgb, var(--gf-finish-soft) 58%, transparent),
0 12px 32px rgba(1, 4, 9, 0.18);
}
}
.gf-panel__inner {
position: relative;
z-index: 1;
}
.gf-panel__head {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 14px 12px;
border-bottom: 1px solid color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 22%, transparent);
background: linear-gradient(
180deg,
color-mix(in srgb, var(--gf-finish-soft, rgba(205,127,50,0.16)) 55%, transparent),
transparent
);
}
.gf-panel__ovr {
flex: 0 0 auto;
min-width: 56px;
padding: 9px 11px;
border-radius: 12px;
background:
linear-gradient(
145deg,
var(--gf-finish-shine, rgba(255,255,255,0.35)),
var(--gf-finish-accent, #CD7F32) 40%,
var(--gf-finish-deep, #5a3412)
);
color: var(--gf-finish-ink, #2A1A0C);
text-align: center;
line-height: 1.05;
box-shadow:
inset 0 1px 0 color-mix(in srgb, var(--gf-finish-shine) 55%, transparent),
0 0 0 1px color-mix(in srgb, var(--gf-finish-deep) 35%, transparent),
var(--gf-panel-ovr-glow, none);
}
#${PANEL_ID}.gf-panel-finish--silver .gf-panel__ovr,
#${PANEL_ID}.gf-panel-finish--gold .gf-panel__ovr {
--gf-panel-ovr-glow: 0 0 14px color-mix(in srgb, var(--gf-finish-glow) 55%, transparent);
}
#${PANEL_ID}.gf-panel-finish--totw .gf-panel__ovr,
#${PANEL_ID}.gf-panel-finish--toty .gf-panel__ovr,
#${PANEL_ID}.gf-panel-finish--icon .gf-panel__ovr,
#${PANEL_ID}.gf-panel-finish--founder .gf-panel__ovr {
--gf-panel-ovr-glow:
0 0 18px color-mix(in srgb, var(--gf-finish-glow) 75%, transparent),
0 0 30px color-mix(in srgb, var(--gf-finish-glow) 35%, transparent);
}
.gf-panel__ovr-value {
display: block;
font-size: 24px;
font-weight: 800;
letter-spacing: -0.03em;
font-variant-numeric: tabular-nums;
text-shadow: 0 1px 0 color-mix(in srgb, var(--gf-finish-shine) 40%, transparent);
}
.gf-panel__ovr-label {
display: block;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
opacity: 0.8;
text-transform: uppercase;
}
.gf-panel__meta {
min-width: 0;
flex: 1;
}
.gf-panel__title-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
}
.gf-panel__name {
font-size: 15px;
font-weight: 700;
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gf-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
border: 1px solid color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 50%, transparent);
background: color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 16%, transparent);
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
text-transform: uppercase;
}
#${PANEL_ID}.gf-panel-finish--gold .gf-chip,
#${PANEL_ID}.gf-panel-finish--totw .gf-chip,
#${PANEL_ID}.gf-panel-finish--toty .gf-chip,
#${PANEL_ID}.gf-panel-finish--icon .gf-chip,
#${PANEL_ID}.gf-panel-finish--founder .gf-chip {
box-shadow: 0 0 8px color-mix(in srgb, var(--gf-finish-glow) 28%, transparent);
}
.gf-panel__blurb {
margin: 5px 0 0;
font-size: 12px;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
line-height: 1.4;
}
.gf-panel__body {
padding: 12px 14px 14px;
}
.gf-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
margin: 0 0 12px;
}
.gf-stat {
border-radius: 10px;
padding: 8px 8px;
text-align: center;
background: color-mix(
in srgb,
var(--gf-finish-soft, rgba(205,127,50,0.16)) 32%,
var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa))
);
border: 1px solid color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 20%, transparent);
}
#${PANEL_ID}.gf-panel-finish--toty .gf-stat,
#${PANEL_ID}.gf-panel-finish--icon .gf-stat,
#${PANEL_ID}.gf-panel-finish--founder .gf-stat {
border-color: color-mix(in srgb, var(--gf-finish-accent) 38%, transparent);
box-shadow: inset 0 0 12px color-mix(in srgb, var(--gf-finish-soft) 38%, transparent);
}
.gf-stat__value {
display: block;
font-size: 16px;
font-weight: 800;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
}
.gf-stat__key {
display: block;
margin-top: 1px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
.gf-attrs {
display: grid;
gap: 6px;
margin: 0 0 12px;
padding: 10px 11px;
border-radius: 10px;
font-size: 12px;
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
background: color-mix(
in srgb,
var(--gf-finish-soft, rgba(205,127,50,0.16)) 18%,
var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa))
);
border: 1px solid color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 14%, transparent);
}
.gf-attrs__row {
display: flex;
justify-content: space-between;
gap: 10px;
}
.gf-attrs__label {
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
.gf-attrs__value {
font-weight: 600;
text-align: right;
font-variant-numeric: tabular-nums;
}
.gf-playstyles {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin: 0 0 12px;
}
.gf-playstyle {
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 999px;
background: var(--bgColor-default, var(--color-canvas-default, #fff));
border: 1px solid var(--borderColor-default, var(--color-border-default, #d0d7de));
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
}
.gf-playstyle.is-plus {
border-color: color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 55%, transparent);
background: color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 14%, transparent);
box-shadow: 0 0 8px color-mix(in srgb, var(--gf-finish-glow) 22%, transparent);
}
.gf-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.gf-btn-link {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 7px 12px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
text-decoration: none !important;
border: 1px solid var(--borderColor-default, var(--color-border-default, #d0d7de));
background: var(--bgColor-default, var(--color-canvas-default, #fff));
color: var(--fgColor-default, var(--color-fg-default, #1f2328)) !important;
transition: background 0.15s ease, box-shadow 0.15s ease;
}
.gf-btn-link:hover {
background: var(--control-transparent-bgColor-hover, rgba(208,215,222,0.32));
}
.gf-btn-link--primary {
border-color: transparent;
background:
linear-gradient(
145deg,
var(--gf-finish-shine, rgba(255,255,255,0.35)),
var(--gf-finish-accent, #CD7F32) 45%,
var(--gf-finish-deep, #5a3412)
);
color: var(--gf-finish-ink, #2A1A0C) !important;
box-shadow: 0 0 14px color-mix(in srgb, var(--gf-finish-glow) 38%, transparent);
}
.gf-btn-link--primary:hover {
filter: brightness(1.06);
background:
linear-gradient(
145deg,
var(--gf-finish-shine, rgba(255,255,255,0.35)),
var(--gf-finish-accent, #CD7F32) 45%,
var(--gf-finish-deep, #5a3412)
);
}
.gf-panel__status {
padding: 16px 14px;
font-size: 13px;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
.gf-panel__status.is-error {
color: var(--fgColor-danger, var(--color-danger-fg, #d1242f));
}
#${PANEL_ID} .gf-panel-skel .gf-skel {
display: block;
border-radius: 6px;
background: linear-gradient(
90deg,
color-mix(in srgb, var(--fgColor-muted, #656d76) 10%, transparent) 0%,
color-mix(in srgb, var(--fgColor-muted, #656d76) 22%, transparent) 45%,
color-mix(in srgb, var(--fgColor-muted, #656d76) 10%, transparent) 90%
);
background-size: 220% 100%;
animation: gf-skel-shimmer 1.35s ease-in-out infinite;
}
#${PANEL_ID} .gf-skel--panel-ovr {
flex: 0 0 auto;
width: 56px;
height: 50px;
border-radius: 12px;
}
#${PANEL_ID} .gf-skel--panel-name {
width: 42%;
height: 14px;
border-radius: 4px;
}
#${PANEL_ID} .gf-skel--panel-chip {
width: 48px;
height: 18px;
border-radius: 999px;
}
#${PANEL_ID} .gf-skel--panel-blurb {
width: 78%;
height: 11px;
margin-top: 8px;
}
#${PANEL_ID} .gf-skel--panel-stat {
height: 48px;
border-radius: 10px;
}
#${PANEL_ID} .gf-skel--panel-attr {
height: 12px;
border-radius: 4px;
}
#${PANEL_ID} .gf-skel--panel-btn {
width: 108px;
height: 30px;
border-radius: 8px;
}
#${PANEL_ID} .gf-skel--panel-btn-sm {
width: 64px;
}
.Popover-message.gf-hc-themed {
position: relative;
isolation: isolate;
border-radius: 16px !important;
border-color: color-mix(
in srgb,
var(--gf-finish-accent, #CD7F32) 58%,
var(--borderColor-default, #d0d7de)
) !important;
background:
radial-gradient(
120% 80% at 0% 0%,
var(--gf-finish-soft, rgba(205,127,50,0.16)),
transparent 55%
),
radial-gradient(
90% 70% at 100% 100%,
color-mix(in srgb, var(--gf-finish-glow, rgba(205,127,50,0.28)) 35%, transparent),
transparent 60%
),
var(--bgColor-default, var(--color-canvas-default, #fff)) !important;
box-shadow:
var(--gf-hc-outer-glow, 0 0 0 transparent),
var(--gf-hc-ring, 0 0 0 1px transparent),
0 10px 28px rgba(1, 4, 9, 0.2) !important;
transition: box-shadow 0.25s ease, border-color 0.25s ease, background 0.25s ease;
}
.Popover-message.gf-hc-themed > .gf-hc-theme-fx {
position: absolute;
inset: 0;
border-radius: inherit;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.Popover-message.gf-hc-themed > .gf-hc-theme-fx + * {
position: relative;
z-index: 1;
}
.gf-hc-theme-fx__shine {
position: absolute;
inset: 0;
background: linear-gradient(
115deg,
transparent 35%,
var(--gf-finish-shine, rgba(255,255,255,0.35)) 48%,
transparent 62%
);
background-size: 220% 100%;
background-position: 120% 0;
opacity: 0;
}
.Popover-message.gf-hc-finish--bronze {
--gf-hc-shine-opacity: 0;
--gf-hc-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 28%, transparent);
--gf-hc-outer-glow: 0 0 0 transparent;
}
.Popover-message.gf-hc-finish--silver {
--gf-hc-shine-opacity: 0.18;
--gf-hc-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 40%, transparent);
--gf-hc-outer-glow: 0 0 16px color-mix(in srgb, var(--gf-finish-glow) 45%, transparent);
}
.Popover-message.gf-hc-finish--gold {
--gf-hc-shine-opacity: 0.32;
--gf-hc-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 55%, transparent);
--gf-hc-outer-glow:
0 0 22px color-mix(in srgb, var(--gf-finish-glow) 70%, transparent),
0 0 40px color-mix(in srgb, var(--gf-finish-glow) 28%, transparent);
}
.Popover-message.gf-hc-finish--totw {
--gf-hc-shine-opacity: 0.28;
--gf-hc-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 60%, transparent);
--gf-hc-outer-glow:
0 0 24px color-mix(in srgb, var(--gf-finish-glow) 75%, transparent),
0 0 48px color-mix(in srgb, var(--gf-finish-glow) 30%, transparent);
}
.Popover-message.gf-hc-finish--toty {
--gf-hc-shine-opacity: 0.38;
--gf-hc-ring: inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 65%, transparent);
--gf-hc-outer-glow:
0 0 28px color-mix(in srgb, var(--gf-finish-glow) 80%, transparent),
0 0 56px color-mix(in srgb, var(--gf-finish-glow) 35%, transparent);
}
.Popover-message.gf-hc-finish--icon,
.Popover-message.gf-hc-finish--founder {
--gf-hc-shine-opacity: 0.45;
--gf-hc-ring:
inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 70%, transparent),
inset 0 0 28px color-mix(in srgb, var(--gf-finish-soft) 55%, transparent);
--gf-hc-outer-glow:
0 0 32px color-mix(in srgb, var(--gf-finish-glow) 85%, transparent),
0 0 64px color-mix(in srgb, var(--gf-finish-glow) 40%, transparent);
}
.Popover-message.gf-hc-finish--gold .gf-hc-theme-fx__shine,
.Popover-message.gf-hc-finish--totw .gf-hc-theme-fx__shine,
.Popover-message.gf-hc-finish--toty .gf-hc-theme-fx__shine,
.Popover-message.gf-hc-finish--icon .gf-hc-theme-fx__shine,
.Popover-message.gf-hc-finish--founder .gf-hc-theme-fx__shine {
animation: gf-hc-shine-sweep 1.8s ease-in-out 1 forwards;
}
.Popover-message.gf-hc-finish--totw {
animation: gf-hc-pulse-totw 2.4s ease-in-out infinite;
}
.Popover-message.gf-hc-finish--icon {
animation: gf-hc-pulse-icon 2.8s ease-in-out infinite;
}
.Popover-message.gf-hc-finish--founder {
animation: gf-hc-pulse-founder 2.2s ease-in-out infinite;
}
@keyframes gf-hc-shine-sweep {
0% { background-position: 130% 0; opacity: 0; }
12% { opacity: var(--gf-hc-shine-opacity, 0.3); }
55% {
background-position: -30% 0;
opacity: calc(var(--gf-hc-shine-opacity, 0.3) * 1.15);
}
100% { background-position: -30% 0; opacity: 0; }
}
@keyframes gf-hc-pulse-totw {
0%, 100% {
box-shadow:
0 0 22px color-mix(in srgb, var(--gf-finish-glow) 55%, transparent),
0 0 40px color-mix(in srgb, var(--gf-finish-glow) 22%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 55%, transparent),
0 10px 28px rgba(1, 4, 9, 0.2);
}
50% {
box-shadow:
0 0 34px color-mix(in srgb, var(--gf-finish-glow) 85%, transparent),
0 0 60px color-mix(in srgb, var(--gf-finish-glow) 40%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--gf-finish-accent) 75%, transparent),
0 12px 32px rgba(1, 4, 9, 0.24);
}
}
@keyframes gf-hc-pulse-icon {
0%, 100% {
box-shadow:
0 0 28px color-mix(in srgb, var(--gf-finish-glow) 60%, transparent),
0 0 50px color-mix(in srgb, var(--gf-finish-accent) 28%, transparent),
inset 0 0 24px color-mix(in srgb, var(--gf-finish-soft) 40%, transparent),
0 10px 28px rgba(1, 4, 9, 0.22);
}
50% {
box-shadow:
0 0 42px color-mix(in srgb, var(--gf-finish-glow) 90%, transparent),
0 0 72px color-mix(in srgb, var(--gf-finish-accent) 45%, transparent),
inset 0 0 36px color-mix(in srgb, var(--gf-finish-soft) 55%, transparent),
0 14px 34px rgba(1, 4, 9, 0.26);
}
}
@keyframes gf-hc-pulse-founder {
0%, 100% {
box-shadow:
0 0 30px color-mix(in srgb, var(--gf-finish-glow) 65%, transparent),
0 0 54px color-mix(in srgb, var(--gf-finish-accent) 30%, transparent),
inset 0 0 26px color-mix(in srgb, var(--gf-finish-soft) 45%, transparent),
0 10px 28px rgba(1, 4, 9, 0.22);
}
50% {
box-shadow:
0 0 46px color-mix(in srgb, var(--gf-finish-glow) 95%, transparent),
0 0 78px color-mix(in srgb, var(--gf-finish-accent) 48%, transparent),
inset 0 0 40px color-mix(in srgb, var(--gf-finish-soft) 60%, transparent),
0 14px 36px rgba(1, 4, 9, 0.28);
}
}
#${HOVERCARD_BLOCK_ID} {
position: relative;
z-index: 1;
margin-top: 12px;
padding: 12px 10px 10px;
margin-left: -4px;
margin-right: -4px;
border-radius: 14px;
border-top: 1px solid color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 35%, transparent);
background:
linear-gradient(
180deg,
color-mix(in srgb, var(--gf-finish-soft, rgba(205,127,50,0.16)) 70%, transparent),
transparent 85%
);
font-family: var(--gf-font);
box-shadow: var(--gf-hc-block-glow, none);
}
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--gold,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--totw,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--toty {
--gf-hc-block-glow: inset 0 0 20px color-mix(in srgb, var(--gf-finish-soft) 45%, transparent);
}
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--icon,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--founder {
--gf-hc-block-glow:
inset 0 0 28px color-mix(in srgb, var(--gf-finish-soft) 60%, transparent),
0 0 18px color-mix(in srgb, var(--gf-finish-glow) 25%, transparent);
}
#${HOVERCARD_BLOCK_ID} .gf-hc__head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
#${HOVERCARD_BLOCK_ID} .gf-hc__ovr {
flex: 0 0 auto;
min-width: 44px;
padding: 6px 8px;
border-radius: 8px;
background:
linear-gradient(
145deg,
var(--gf-finish-shine, rgba(255,255,255,0.35)),
var(--gf-finish-accent, #CD7F32) 40%,
var(--gf-finish-deep, #5a3412)
);
color: var(--gf-finish-ink, #2A1A0C);
text-align: center;
line-height: 1.05;
box-shadow:
inset 0 1px 0 color-mix(in srgb, var(--gf-finish-shine) 55%, transparent),
0 0 0 1px color-mix(in srgb, var(--gf-finish-deep) 35%, transparent),
var(--gf-hc-ovr-glow, none);
}
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--silver .gf-hc__ovr,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--gold .gf-hc__ovr {
--gf-hc-ovr-glow: 0 0 12px color-mix(in srgb, var(--gf-finish-glow) 55%, transparent);
}
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--totw .gf-hc__ovr,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--toty .gf-hc__ovr,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--icon .gf-hc__ovr,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--founder .gf-hc__ovr {
--gf-hc-ovr-glow:
0 0 16px color-mix(in srgb, var(--gf-finish-glow) 75%, transparent),
0 0 28px color-mix(in srgb, var(--gf-finish-glow) 35%, transparent);
}
#${HOVERCARD_BLOCK_ID} .gf-hc__ovr-value {
display: block;
font-size: 18px;
font-weight: 800;
letter-spacing: -0.03em;
font-variant-numeric: tabular-nums;
text-shadow: 0 1px 0 color-mix(in srgb, var(--gf-finish-shine) 40%, transparent);
}
#${HOVERCARD_BLOCK_ID} .gf-hc__ovr-label {
display: block;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.8;
}
#${HOVERCARD_BLOCK_ID} .gf-hc__meta {
min-width: 0;
flex: 1;
}
#${HOVERCARD_BLOCK_ID} .gf-hc__chips {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 2px;
}
#${HOVERCARD_BLOCK_ID} .gf-chip {
border-color: color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 50%, transparent);
background: color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 16%, transparent);
}
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--gold .gf-chip,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--totw .gf-chip,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--toty .gf-chip,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--icon .gf-chip,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--founder .gf-chip {
box-shadow: 0 0 8px color-mix(in srgb, var(--gf-finish-glow) 30%, transparent);
}
#${HOVERCARD_BLOCK_ID} .gf-hc__blurb {
margin: 0;
font-size: 11px;
line-height: 1.35;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
#${HOVERCARD_BLOCK_ID} .gf-hc__stats {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 4px;
margin: 0 0 8px;
}
#${HOVERCARD_BLOCK_ID} .gf-hc__stat {
text-align: center;
padding: 5px 2px;
border-radius: 6px;
background: color-mix(
in srgb,
var(--gf-finish-soft, rgba(205,127,50,0.16)) 35%,
var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa))
);
border: 1px solid color-mix(in srgb, var(--gf-finish-accent, #CD7F32) 22%, transparent);
}
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--toty .gf-hc__stat,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--icon .gf-hc__stat,
#${HOVERCARD_BLOCK_ID}.gf-hc-finish--founder .gf-hc__stat {
border-color: color-mix(in srgb, var(--gf-finish-accent) 40%, transparent);
box-shadow: inset 0 0 10px color-mix(in srgb, var(--gf-finish-soft) 40%, transparent);
}
#${HOVERCARD_BLOCK_ID} .gf-hc__stat-value {
display: block;
font-size: 13px;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
}
#${HOVERCARD_BLOCK_ID} .gf-hc__stat-key {
display: block;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
#${HOVERCARD_BLOCK_ID} .gf-hc__actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#${HOVERCARD_BLOCK_ID} .gf-hc__link {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
text-decoration: none !important;
border: 1px solid var(--borderColor-default, var(--color-border-default, #d0d7de));
background: var(--bgColor-default, var(--color-canvas-default, #fff));
color: var(--fgColor-default, var(--color-fg-default, #1f2328)) !important;
}
#${HOVERCARD_BLOCK_ID} .gf-hc__link--primary {
border-color: transparent;
background:
linear-gradient(
145deg,
var(--gf-finish-shine, rgba(255,255,255,0.35)),
var(--gf-finish-accent, #CD7F32) 45%,
var(--gf-finish-deep, #5a3412)
);
color: var(--gf-finish-ink, #2A1A0C) !important;
box-shadow: 0 0 12px color-mix(in srgb, var(--gf-finish-glow) 40%, transparent);
}
#${HOVERCARD_BLOCK_ID} .gf-hc__status {
font-size: 12px;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
#${HOVERCARD_BLOCK_ID} .gf-hc__status.is-error {
color: var(--fgColor-danger, var(--color-danger-fg, #d1242f));
}
#${HOVERCARD_BLOCK_ID} .gf-skel {
display: block;
border-radius: 6px;
background: linear-gradient(
90deg,
color-mix(in srgb, var(--fgColor-muted, #656d76) 10%, transparent) 0%,
color-mix(in srgb, var(--fgColor-muted, #656d76) 22%, transparent) 45%,
color-mix(in srgb, var(--fgColor-muted, #656d76) 10%, transparent) 90%
);
background-size: 220% 100%;
animation: gf-skel-shimmer 1.35s ease-in-out infinite;
}
#${HOVERCARD_BLOCK_ID} .gf-skel--ovr {
flex: 0 0 auto;
width: 44px;
height: 42px;
border-radius: 8px;
}
#${HOVERCARD_BLOCK_ID} .gf-skel--chip {
width: 52px;
height: 18px;
border-radius: 999px;
}
#${HOVERCARD_BLOCK_ID} .gf-skel--blurb {
width: 88%;
height: 10px;
margin-top: 6px;
}
#${HOVERCARD_BLOCK_ID} .gf-skel--blurb-short {
width: 62%;
margin-top: 5px;
}
#${HOVERCARD_BLOCK_ID} .gf-skel--stat {
height: 36px;
border-radius: 6px;
}
#${HOVERCARD_BLOCK_ID} .gf-skel--btn {
width: 92px;
height: 24px;
border-radius: 6px;
}
#${HOVERCARD_BLOCK_ID} .gf-skel--btn-sm {
width: 48px;
}
@keyframes gf-skel-shimmer {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
@media (prefers-reduced-motion: reduce) {
.Popover-message.gf-hc-themed,
.Popover-message.gf-hc-themed .gf-hc-theme-fx__shine {
animation: none !important;
}
#${PANEL_ID},
#${PANEL_ID} .gf-panel-theme-fx__shine {
animation: none !important;
}
#${HOVERCARD_BLOCK_ID} .gf-skel,
#${PANEL_ID} .gf-skel {
animation: none !important;
background: color-mix(in srgb, var(--fgColor-muted, #656d76) 14%, transparent);
}
}
/* FIFA theme: metallic finish colors like FUT cards, no glow / shine / pulse */
html.gf-card-theme-fifa #${PANEL_ID},
html.gf-card-theme-fifa .Popover-message.gf-hc-themed {
--gf-panel-outer-glow: 0 0 0 transparent !important;
--gf-panel-ring: 0 0 0 transparent !important;
--gf-panel-ovr-glow: none !important;
--gf-hc-outer-glow: 0 0 0 transparent !important;
--gf-hc-ring: 0 0 0 transparent !important;
--gf-hc-ovr-glow: none !important;
--gf-hc-block-glow: none !important;
--gf-hc-shine-opacity: 0 !important;
animation: none !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-panel-theme-fx,
html.gf-card-theme-fifa .Popover-message.gf-hc-themed > .gf-hc-theme-fx,
html.gf-card-theme-fifa .gf-panel-theme-fx__shine,
html.gf-card-theme-fifa .gf-hc-theme-fx__shine {
display: none !important;
}
html.gf-card-theme-fifa #${PANEL_ID}.gf-panel-finish--bronze,
html.gf-card-theme-fifa .Popover-message.gf-hc-finish--bronze {
--gf-fifa-ink: #2A1810;
--gf-fifa-muted: #6B4A32;
--gf-fifa-border: #8B5A2B;
--gf-fifa-ovr-top: #D4A574;
--gf-fifa-ovr-bot: #8B5A2B;
--gf-fifa-ovr-ink: #1F120A;
--gf-fifa-stat: rgba(139, 90, 43, 0.18);
--gf-fifa-chip: rgba(139, 90, 43, 0.22);
background: linear-gradient(165deg, #E8C9A0 0%, #C9956C 42%, #A67C52 100%) !important;
border-color: var(--gf-fifa-border) !important;
color: var(--gf-fifa-ink);
box-shadow: inset 0 1px 0 rgba(255, 240, 220, 0.45), 0 2px 8px rgba(90, 50, 20, 0.18) !important;
}
html.gf-card-theme-fifa #${PANEL_ID}.gf-panel-finish--silver,
html.gf-card-theme-fifa .Popover-message.gf-hc-finish--silver {
--gf-fifa-ink: #1A1E24;
--gf-fifa-muted: #5A6270;
--gf-fifa-border: #8A93A0;
--gf-fifa-ovr-top: #E8ECF0;
--gf-fifa-ovr-bot: #8A93A0;
--gf-fifa-ovr-ink: #1A1E24;
--gf-fifa-stat: rgba(90, 98, 112, 0.16);
--gf-fifa-chip: rgba(90, 98, 112, 0.2);
background: linear-gradient(165deg, #F2F4F7 0%, #C5CBD4 45%, #9AA3B0 100%) !important;
border-color: var(--gf-fifa-border) !important;
color: var(--gf-fifa-ink);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7), 0 2px 8px rgba(40, 50, 70, 0.14) !important;
}
html.gf-card-theme-fifa #${PANEL_ID}.gf-panel-finish--gold,
html.gf-card-theme-fifa .Popover-message.gf-hc-finish--gold {
--gf-fifa-ink: #2A2008;
--gf-fifa-muted: #7A6520;
--gf-fifa-border: #C9A227;
--gf-fifa-ovr-top: #FFE28A;
--gf-fifa-ovr-bot: #C9A227;
--gf-fifa-ovr-ink: #2A2008;
--gf-fifa-stat: rgba(201, 162, 39, 0.22);
--gf-fifa-chip: rgba(201, 162, 39, 0.28);
background: linear-gradient(165deg, #FFE9A8 0%, #E8C547 42%, #C9A227 100%) !important;
border-color: var(--gf-fifa-border) !important;
color: var(--gf-fifa-ink);
box-shadow: inset 0 1px 0 rgba(255, 248, 220, 0.65), 0 2px 8px rgba(120, 90, 20, 0.16) !important;
}
html.gf-card-theme-fifa #${PANEL_ID}.gf-panel-finish--totw,
html.gf-card-theme-fifa .Popover-message.gf-hc-finish--totw {
--gf-fifa-ink: #F5F5F5;
--gf-fifa-muted: #B0B0B0;
--gf-fifa-border: #E03E52;
--gf-fifa-ovr-top: #F0D060;
--gf-fifa-ovr-bot: #C9A227;
--gf-fifa-ovr-ink: #1A1405;
--gf-fifa-stat: rgba(224, 62, 82, 0.22);
--gf-fifa-chip: rgba(224, 62, 82, 0.28);
background: linear-gradient(165deg, #3A3A3A 0%, #1E1E1E 50%, #121212 100%) !important;
border-color: var(--gf-fifa-border) !important;
color: var(--gf-fifa-ink);
box-shadow: inset 0 0 0 1px rgba(224, 62, 82, 0.35), 0 2px 10px rgba(0, 0, 0, 0.35) !important;
}
html.gf-card-theme-fifa #${PANEL_ID}.gf-panel-finish--toty,
html.gf-card-theme-fifa .Popover-message.gf-hc-finish--toty {
--gf-fifa-ink: #E8F0FF;
--gf-fifa-muted: #9BB4DE;
--gf-fifa-border: #3B7AFF;
--gf-fifa-ovr-top: #7EB0FF;
--gf-fifa-ovr-bot: #2A5FD4;
--gf-fifa-ovr-ink: #0A1630;
--gf-fifa-stat: rgba(59, 122, 255, 0.28);
--gf-fifa-chip: rgba(59, 122, 255, 0.32);
background: linear-gradient(165deg, #1A3A6E 0%, #0B1B3D 55%, #071228 100%) !important;
border-color: var(--gf-fifa-border) !important;
color: var(--gf-fifa-ink);
box-shadow: inset 0 0 0 1px rgba(59, 122, 255, 0.4), 0 2px 10px rgba(8, 20, 50, 0.4) !important;
}
html.gf-card-theme-fifa #${PANEL_ID}.gf-panel-finish--icon,
html.gf-card-theme-fifa .Popover-message.gf-hc-finish--icon {
--gf-fifa-ink: #F5E6C8;
--gf-fifa-muted: #C4B08A;
--gf-fifa-border: #F3D688;
--gf-fifa-ovr-top: #F3D688;
--gf-fifa-ovr-bot: #B8944A;
--gf-fifa-ovr-ink: #2A1A45;
--gf-fifa-stat: rgba(180, 120, 255, 0.22);
--gf-fifa-chip: rgba(243, 214, 136, 0.28);
background: linear-gradient(165deg, #4A2A7A 0%, #2A1A4A 50%, #1A0F30 100%) !important;
border-color: var(--gf-fifa-border) !important;
color: var(--gf-fifa-ink);
box-shadow: inset 0 0 0 1px rgba(243, 214, 136, 0.35), 0 2px 10px rgba(30, 15, 60, 0.4) !important;
}
html.gf-card-theme-fifa #${PANEL_ID}.gf-panel-finish--founder,
html.gf-card-theme-fifa .Popover-message.gf-hc-finish--founder {
--gf-fifa-ink: #FFE4EC;
--gf-fifa-muted: #E0A0B0;
--gf-fifa-border: #FF5A7A;
--gf-fifa-ovr-top: #FF8AA0;
--gf-fifa-ovr-bot: #D43055;
--gf-fifa-ovr-ink: #3A0A18;
--gf-fifa-stat: rgba(255, 90, 122, 0.28);
--gf-fifa-chip: rgba(255, 90, 122, 0.32);
background: linear-gradient(165deg, #5A2030 0%, #3A0A18 55%, #250610 100%) !important;
border-color: var(--gf-fifa-border) !important;
color: var(--gf-fifa-ink);
box-shadow: inset 0 0 0 1px rgba(255, 90, 122, 0.4), 0 2px 10px rgba(40, 8, 18, 0.4) !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-panel__head {
border-bottom-color: color-mix(in srgb, var(--gf-fifa-border, #8B5A2B) 45%, transparent) !important;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--gf-fifa-ovr-top, #D4A574) 22%, transparent),
transparent
) !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-panel__name,
html.gf-card-theme-fifa #${PANEL_ID} .gf-stat__value,
html.gf-card-theme-fifa #${PANEL_ID} .gf-attrs,
html.gf-card-theme-fifa #${PANEL_ID} .gf-attrs__value,
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-hc__stat-value {
color: var(--gf-fifa-ink, inherit) !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-panel__blurb,
html.gf-card-theme-fifa #${PANEL_ID} .gf-stat__key,
html.gf-card-theme-fifa #${PANEL_ID} .gf-attrs__label,
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-hc__blurb,
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-hc__stat-key {
color: var(--gf-fifa-muted, inherit) !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-panel__ovr,
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-hc__ovr {
background: linear-gradient(180deg, var(--gf-fifa-ovr-top), var(--gf-fifa-ovr-bot)) !important;
color: var(--gf-fifa-ovr-ink) !important;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.35),
0 0 0 1px color-mix(in srgb, var(--gf-fifa-border) 55%, #000) !important;
text-shadow: none !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-panel__ovr-value,
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-hc__ovr-value {
text-shadow: none !important;
color: inherit !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-chip,
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-chip,
html.gf-card-theme-fifa .gf-playstyle.is-plus {
box-shadow: none !important;
border-color: color-mix(in srgb, var(--gf-fifa-border) 70%, transparent) !important;
background: var(--gf-fifa-chip) !important;
color: var(--gf-fifa-ink) !important;
}
html.gf-card-theme-fifa .gf-playstyle:not(.is-plus) {
border-color: color-mix(in srgb, var(--gf-fifa-border) 40%, transparent) !important;
background: color-mix(in srgb, var(--gf-fifa-ink) 6%, transparent) !important;
color: var(--gf-fifa-ink) !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-stat,
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-hc__stat {
box-shadow: none !important;
border-color: color-mix(in srgb, var(--gf-fifa-border) 40%, transparent) !important;
background: var(--gf-fifa-stat) !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-attrs {
box-shadow: none !important;
border-color: color-mix(in srgb, var(--gf-fifa-border) 35%, transparent) !important;
background: var(--gf-fifa-stat) !important;
}
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} {
border-top-color: color-mix(in srgb, var(--gf-fifa-border) 50%, transparent) !important;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--gf-fifa-ovr-top, transparent) 18%, transparent),
transparent 80%
) !important;
box-shadow: none !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-btn-link {
border-color: color-mix(in srgb, var(--gf-fifa-border) 45%, transparent) !important;
background: color-mix(in srgb, var(--gf-fifa-ink) 8%, transparent) !important;
color: var(--gf-fifa-ink) !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-btn-link:hover {
background: color-mix(in srgb, var(--gf-fifa-ink) 14%, transparent) !important;
}
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-hc__link {
border-color: color-mix(in srgb, var(--gf-fifa-border) 45%, transparent) !important;
background: color-mix(in srgb, var(--gf-fifa-ink) 8%, transparent) !important;
color: var(--gf-fifa-ink) !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-btn-link--primary,
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-hc__link--primary {
border-color: transparent !important;
background: linear-gradient(180deg, var(--gf-fifa-ovr-top), var(--gf-fifa-ovr-bot)) !important;
color: var(--gf-fifa-ovr-ink) !important;
box-shadow: none !important;
filter: none !important;
}
html.gf-card-theme-fifa #${PANEL_ID} .gf-btn-link--primary:hover,
html.gf-card-theme-fifa #${HOVERCARD_BLOCK_ID} .gf-hc__link--primary:hover {
filter: brightness(1.05) !important;
background: linear-gradient(180deg, var(--gf-fifa-ovr-top), var(--gf-fifa-ovr-bot)) !important;
}
/* Neon theme: dark glass + finish-colored neon glow */
html.gf-card-theme-neon #${PANEL_ID},
html.gf-card-theme-neon .Popover-message.gf-hc-themed {
--gf-neon: #FF9F43;
--gf-neon-ink: #FFE8D0;
--gf-neon-muted: #B89878;
--gf-panel-outer-glow: 0 0 0 transparent !important;
--gf-panel-ring: 0 0 0 transparent !important;
--gf-panel-ovr-glow: none !important;
--gf-hc-outer-glow: 0 0 0 transparent !important;
--gf-hc-ring: 0 0 0 transparent !important;
--gf-hc-ovr-glow: none !important;
--gf-hc-block-glow: none !important;
--gf-hc-shine-opacity: 0 !important;
animation: none !important;
color: var(--gf-neon-ink);
border-color: color-mix(in srgb, var(--gf-neon) 75%, #fff) !important;
background:
radial-gradient(120% 80% at 0% 0%, color-mix(in srgb, var(--gf-neon) 28%, transparent), transparent 55%),
radial-gradient(90% 70% at 100% 100%, color-mix(in srgb, var(--gf-neon) 18%, transparent), transparent 60%),
linear-gradient(165deg, #161622 0%, #0B0B14 100%) !important;
box-shadow:
0 0 0 1px color-mix(in srgb, var(--gf-neon) 55%, transparent),
0 0 18px color-mix(in srgb, var(--gf-neon) 45%, transparent),
0 0 42px color-mix(in srgb, var(--gf-neon) 22%, transparent),
inset 0 0 28px color-mix(in srgb, var(--gf-neon) 10%, transparent) !important;
}
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--bronze,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--bronze {
--gf-neon: #FF9F43;
--gf-neon-ink: #FFE8D0;
--gf-neon-muted: #C4A07A;
}
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--silver,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--silver {
--gf-neon: #B8C4FF;
--gf-neon-ink: #EEF1FF;
--gf-neon-muted: #9AA3C8;
}
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--gold,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--gold {
--gf-neon: #FFD60A;
--gf-neon-ink: #FFF6C8;
--gf-neon-muted: #C9B45A;
}
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--totw,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--totw {
--gf-neon: #FF2E63;
--gf-neon-ink: #FFD6E0;
--gf-neon-muted: #D08098;
}
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--toty,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--toty {
--gf-neon: #00F0FF;
--gf-neon-ink: #D7FBFF;
--gf-neon-muted: #74B8C4;
}
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--icon,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--icon {
--gf-neon: #C77DFF;
--gf-neon-ink: #F3E6FF;
--gf-neon-muted: #B090D0;
}
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--founder,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--founder {
--gf-neon: #FF6BCB;
--gf-neon-ink: #FFE3F4;
--gf-neon-muted: #D090B8;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-panel-theme-fx,
html.gf-card-theme-neon .Popover-message.gf-hc-themed > .gf-hc-theme-fx {
display: block !important;
}
html.gf-card-theme-neon .gf-panel-theme-fx__shine,
html.gf-card-theme-neon .gf-hc-theme-fx__shine {
display: block !important;
animation: none !important;
background: radial-gradient(
60% 50% at 50% 0%,
color-mix(in srgb, var(--gf-neon) 35%, transparent),
transparent 70%
) !important;
background-size: 100% 100% !important;
background-position: 0 0 !important;
opacity: 0.7 !important;
}
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--totw,
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--toty,
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--icon,
html.gf-card-theme-neon #${PANEL_ID}.gf-panel-finish--founder,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--totw,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--toty,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--icon,
html.gf-card-theme-neon .Popover-message.gf-hc-finish--founder {
animation: gf-neon-pulse 2.6s ease-in-out infinite !important;
}
@keyframes gf-neon-pulse {
0%, 100% {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--gf-neon) 55%, transparent),
0 0 16px color-mix(in srgb, var(--gf-neon) 40%, transparent),
0 0 36px color-mix(in srgb, var(--gf-neon) 18%, transparent),
inset 0 0 24px color-mix(in srgb, var(--gf-neon) 8%, transparent);
}
50% {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--gf-neon) 80%, transparent),
0 0 26px color-mix(in srgb, var(--gf-neon) 65%, transparent),
0 0 56px color-mix(in srgb, var(--gf-neon) 32%, transparent),
inset 0 0 34px color-mix(in srgb, var(--gf-neon) 14%, transparent);
}
}
html.gf-card-theme-neon #${PANEL_ID} .gf-panel__head {
border-bottom-color: color-mix(in srgb, var(--gf-neon) 35%, transparent) !important;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--gf-neon) 16%, transparent),
transparent
) !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-panel__name,
html.gf-card-theme-neon #${PANEL_ID} .gf-stat__value,
html.gf-card-theme-neon #${PANEL_ID} .gf-attrs,
html.gf-card-theme-neon #${PANEL_ID} .gf-attrs__value,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__stat-value {
color: var(--gf-neon-ink) !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-panel__blurb,
html.gf-card-theme-neon #${PANEL_ID} .gf-stat__key,
html.gf-card-theme-neon #${PANEL_ID} .gf-attrs__label,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__blurb,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__stat-key {
color: var(--gf-neon-muted) !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-panel__ovr,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__ovr {
background: color-mix(in srgb, var(--gf-neon) 18%, #0A0A12) !important;
color: var(--gf-neon) !important;
box-shadow:
0 0 0 1px color-mix(in srgb, var(--gf-neon) 70%, transparent),
0 0 14px color-mix(in srgb, var(--gf-neon) 55%, transparent),
inset 0 0 12px color-mix(in srgb, var(--gf-neon) 18%, transparent) !important;
text-shadow: 0 0 10px color-mix(in srgb, var(--gf-neon) 70%, transparent) !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-panel__ovr-value,
html.gf-card-theme-neon #${PANEL_ID} .gf-panel__ovr-label,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__ovr-value,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__ovr-label {
color: inherit !important;
text-shadow: inherit !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-chip,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-chip,
html.gf-card-theme-neon .gf-playstyle.is-plus {
border-color: color-mix(in srgb, var(--gf-neon) 65%, transparent) !important;
background: color-mix(in srgb, var(--gf-neon) 14%, transparent) !important;
color: var(--gf-neon-ink) !important;
box-shadow: 0 0 10px color-mix(in srgb, var(--gf-neon) 28%, transparent) !important;
}
html.gf-card-theme-neon .gf-playstyle:not(.is-plus) {
border-color: color-mix(in srgb, var(--gf-neon) 35%, transparent) !important;
background: color-mix(in srgb, var(--gf-neon) 8%, transparent) !important;
color: var(--gf-neon-ink) !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-stat,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__stat,
html.gf-card-theme-neon #${PANEL_ID} .gf-attrs {
border-color: color-mix(in srgb, var(--gf-neon) 35%, transparent) !important;
background: color-mix(in srgb, var(--gf-neon) 10%, #0A0A12) !important;
box-shadow: inset 0 0 12px color-mix(in srgb, var(--gf-neon) 8%, transparent) !important;
}
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} {
border-top-color: color-mix(in srgb, var(--gf-neon) 40%, transparent) !important;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--gf-neon) 14%, transparent),
transparent 85%
) !important;
box-shadow: none !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-btn-link,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__link {
border-color: color-mix(in srgb, var(--gf-neon) 45%, transparent) !important;
background: color-mix(in srgb, var(--gf-neon) 10%, transparent) !important;
color: var(--gf-neon-ink) !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-btn-link:hover,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__link:hover {
background: color-mix(in srgb, var(--gf-neon) 18%, transparent) !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-btn-link--primary,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__link--primary {
border-color: transparent !important;
background: color-mix(in srgb, var(--gf-neon) 22%, #0A0A12) !important;
color: var(--gf-neon) !important;
box-shadow:
0 0 0 1px color-mix(in srgb, var(--gf-neon) 60%, transparent),
0 0 16px color-mix(in srgb, var(--gf-neon) 40%, transparent) !important;
text-shadow: 0 0 8px color-mix(in srgb, var(--gf-neon) 50%, transparent);
filter: none !important;
}
html.gf-card-theme-neon #${PANEL_ID} .gf-btn-link--primary:hover,
html.gf-card-theme-neon #${HOVERCARD_BLOCK_ID} .gf-hc__link--primary:hover {
filter: brightness(1.08) !important;
background: color-mix(in srgb, var(--gf-neon) 28%, #0A0A12) !important;
}
@media (prefers-reduced-motion: reduce) {
html.gf-card-theme-neon #${PANEL_ID},
html.gf-card-theme-neon .Popover-message.gf-hc-themed {
animation: none !important;
}
}
/* GitHub theme: Primer tokens, no glow / shine / finish flash */
html.gf-card-theme-github #${PANEL_ID},
html.gf-card-theme-github .Popover-message.gf-hc-themed {
--gf-panel-outer-glow: 0 0 0 transparent !important;
--gf-panel-ring: 0 0 0 transparent !important;
--gf-panel-ovr-glow: none !important;
--gf-hc-outer-glow: 0 0 0 transparent !important;
--gf-hc-ring: 0 0 0 transparent !important;
--gf-hc-ovr-glow: none !important;
--gf-hc-block-glow: none !important;
--gf-hc-shine-opacity: 0 !important;
animation: none !important;
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
border-color: var(--borderColor-default, var(--color-border-default, #d0d7de)) !important;
border-radius: 6px !important;
background: var(--bgColor-default, var(--color-canvas-default, #fff)) !important;
box-shadow: none !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-panel-theme-fx,
html.gf-card-theme-github .Popover-message.gf-hc-themed > .gf-hc-theme-fx,
html.gf-card-theme-github .gf-panel-theme-fx__shine,
html.gf-card-theme-github .gf-hc-theme-fx__shine {
display: none !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-panel__head {
background: transparent !important;
border-bottom-color: var(--borderColor-muted, var(--color-border-muted, rgba(27,31,36,0.15))) !important;
padding: 12px;
}
html.gf-card-theme-github #${PANEL_ID} .gf-panel__name,
html.gf-card-theme-github #${PANEL_ID} .gf-stat__value,
html.gf-card-theme-github #${PANEL_ID} .gf-attrs,
html.gf-card-theme-github #${PANEL_ID} .gf-attrs__value,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__stat-value {
color: var(--fgColor-default, var(--color-fg-default, #1f2328)) !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-panel__blurb,
html.gf-card-theme-github #${PANEL_ID} .gf-stat__key,
html.gf-card-theme-github #${PANEL_ID} .gf-attrs__label,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__blurb,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__stat-key {
color: var(--fgColor-muted, var(--color-fg-muted, #656d76)) !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-panel__ovr,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__ovr {
background: color-mix(
in srgb,
var(--gf-finish-accent, #656d76) 20%,
var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa))
) !important;
color: var(--gf-finish-accent, var(--fgColor-default, #1f2328)) !important;
border: 1px solid color-mix(
in srgb,
var(--gf-finish-accent, #656d76) 70%,
var(--borderColor-default, var(--color-border-default, #d0d7de))
) !important;
border-radius: 6px !important;
box-shadow: none !important;
min-width: 48px;
padding: 6px 8px;
}
html.gf-card-theme-github #${PANEL_ID} .gf-panel__ovr-value,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__ovr-value {
text-shadow: none !important;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--gf-finish-accent, var(--fgColor-default, #1f2328)) !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-panel__ovr-label,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__ovr-label {
opacity: 0.9;
color: var(--gf-finish-accent, var(--fgColor-muted, #656d76)) !important;
font-weight: 600;
}
html.gf-card-theme-github #${PANEL_ID} .gf-chip,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-chip,
html.gf-card-theme-github .gf-playstyle.is-plus {
box-shadow: none !important;
border-radius: 2em !important;
border-color: var(--borderColor-default, var(--color-border-default, #d0d7de)) !important;
background: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa)) !important;
color: var(--fgColor-default, var(--color-fg-default, #1f2328)) !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-chip--finish,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-chip--finish {
border-color: color-mix(
in srgb,
var(--gf-finish-accent, #656d76) 28%,
var(--borderColor-default, #d0d7de)
) !important;
background: color-mix(
in srgb,
var(--gf-finish-accent, #656d76) 8%,
var(--bgColor-muted, #f6f8fa)
) !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-stat,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__stat {
box-shadow: none !important;
border-color: var(--borderColor-muted, var(--color-border-muted, rgba(27,31,36,0.15))) !important;
background: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa)) !important;
border-radius: 6px !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-attrs {
box-shadow: none !important;
border-color: var(--borderColor-muted, var(--color-border-muted, rgba(27,31,36,0.15))) !important;
background: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa)) !important;
border-radius: 6px !important;
}
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} {
border-top-color: var(--borderColor-muted, var(--color-border-muted, rgba(27,31,36,0.15))) !important;
background: transparent !important;
box-shadow: none !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-btn-link,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__link {
border-color: var(--borderColor-default, var(--color-border-default, #d0d7de)) !important;
background: var(--button-default-bgColor-rest, var(--color-btn-bg, #f6f8fa)) !important;
color: var(--fgColor-default, var(--color-fg-default, #1f2328)) !important;
border-radius: 6px !important;
box-shadow: none !important;
filter: none !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-btn-link:hover,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__link:hover {
background: var(--button-default-bgColor-hover, var(--color-btn-hover-bg, #f3f4f6)) !important;
border-color: var(--button-default-borderColor-hover, var(--color-btn-hover-border, #d0d7de)) !important;
filter: none !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-btn-link--primary,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__link--primary {
border-color: var(--button-primary-borderColor-rest, rgba(31, 35, 40, 0.15)) !important;
background: var(--button-primary-bgColor-rest, var(--color-btn-primary-bg, #1f883d)) !important;
color: var(--button-primary-fgColor-rest, var(--color-btn-primary-text, #fff)) !important;
box-shadow: none !important;
filter: none !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-btn-link--primary:hover,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-hc__link--primary:hover {
background: var(--button-primary-bgColor-hover, var(--color-btn-primary-hover-bg, #1a7f37)) !important;
border-color: var(--button-primary-borderColor-hover, rgba(31, 35, 40, 0.15)) !important;
filter: none !important;
}
html.gf-card-theme-github #${PANEL_ID} .gf-playstyle,
html.gf-card-theme-github #${HOVERCARD_BLOCK_ID} .gf-playstyle {
border-color: var(--borderColor-muted, rgba(27,31,36,0.15)) !important;
background: var(--bgColor-muted, #f6f8fa) !important;
color: var(--fgColor-default, #1f2328) !important;
box-shadow: none !important;
}
#gf-user-menu-item .gf-nav-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--fgColor-muted, #8b949e);
display: inline-block;
flex-shrink: 0;
}
#gf-user-menu-item .gf-nav-dot.is-on {
background: #3fb950;
}
.gf-settings-panel {
position: fixed;
z-index: 10000;
width: 360px;
max-width: calc(100vw - 16px);
border-radius: 12px;
border: 1px solid var(--borderColor-default, var(--color-border-default, #d0d7de));
background: var(--bgColor-default, var(--color-canvas-default, #fff));
box-shadow: 0 16px 40px rgba(1,4,9,0.28);
font-family: var(--gf-font);
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
}
.gf-settings-panel[hidden] { display: none !important; }
.gf-settings-panel__header {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 14px 14px 10px;
border-bottom: 1px solid var(--borderColor-muted, rgba(27,31,36,0.08));
}
.gf-settings-panel__title {
font-size: 15px;
font-weight: 700;
}
.gf-settings-panel__subtitle {
margin-top: 2px;
font-size: 12px;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
.gf-settings-panel__version {
margin-top: 4px;
font-size: 11px;
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
.gf-cache-stats {
margin: 0 0 12px;
padding: 10px 11px;
border-radius: 8px;
border: 1px solid var(--borderColor-muted, rgba(27,31,36,0.08));
background: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa));
}
.gf-cache-stats__count {
font-size: 13px;
font-weight: 600;
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
}
.gf-cache-stats__storage {
margin: 4px 0 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--fgColor-muted, var(--color-fg-muted, #656d76));
}
.gf-cache-stats__bar {
margin-top: 8px;
height: 8px;
border-radius: 999px;
overflow: hidden;
background: color-mix(in srgb, var(--fgColor-muted, #656d76) 16%, transparent);
}
.gf-cache-stats__bar-fill {
height: 100%;
width: 100%;
border-radius: inherit;
background: #3fb950;
transition: width 0.25s ease, background 0.25s ease;
}
.gf-cache-stats__bar.is-warn .gf-cache-stats__bar-fill {
background: #d29922;
}
.gf-cache-stats__bar.is-full .gf-cache-stats__bar-fill {
background: #cf222e;
}
.gf-cache-stats__hint {
margin: 6px 0 0;
}
.gf-settings-panel__close {
border: 0;
background: transparent;
font-size: 20px;
line-height: 1;
cursor: pointer;
color: var(--fgColor-muted, #656d76);
}
.gf-settings-panel__section {
padding: 12px 14px;
}
.gf-settings-panel__section-title {
margin-bottom: 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--fgColor-muted, #656d76);
}
.gf-field {
display: grid;
gap: 6px;
margin-bottom: 8px;
}
.gf-field__label {
font-size: 12px;
font-weight: 600;
}
.gf-field input[type="number"],
.gf-field select {
width: 100%;
padding: 7px 9px;
border-radius: 6px;
border: 1px solid var(--borderColor-default, #d0d7de);
background: var(--bgColor-default, #fff);
color: inherit;
}
.gf-hint {
margin: 0 0 8px;
font-size: 12px;
color: var(--fgColor-muted, #656d76);
line-height: 1.35;
}
.gf-switch {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
.gf-switch input { accent-color: #238636; }
.gf-pill {
margin-left: auto;
font-size: 11px;
font-weight: 700;
padding: 2px 7px;
border-radius: 999px;
background: var(--bgColor-muted, #f6f8fa);
color: var(--fgColor-muted, #656d76);
}
.gf-pill.is-on {
background: rgba(46,160,67,0.15);
color: #1a7f37;
}
.gf-row {
display: flex;
align-items: center;
gap: 10px;
}
.gf-settings-panel__divider {
height: 1px;
background: var(--borderColor-muted, rgba(27,31,36,0.08));
margin: 0 14px;
}
.gf-settings-panel__footer {
padding: 12px 14px 14px;
}
.gf-settings-panel__footer-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.gf-btn {
appearance: none;
border: 1px solid var(--borderColor-default, #d0d7de);
background: var(--bgColor-muted, #f6f8fa);
color: inherit;
border-radius: 6px;
padding: 7px 10px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.gf-btn--ghost { background: transparent; }
.gf-btn--danger {
border-color: rgba(209,36,47,0.35);
color: #cf222e;
background: rgba(209,36,47,0.06);
}
.gf-btn--green {
border-color: transparent;
background: #238636;
color: #fff;
}
.gf-cache-status {
min-height: 1.2em;
margin: 6px 0 0;
font-size: 12px;
color: var(--fgColor-muted, #656d76);
}
.gf-settings-panel__footer-divider {
height: 1px;
margin: 12px 0;
background: var(--borderColor-muted, rgba(27,31,36,0.08));
}
.gf-settings-panel__repo {
display: grid;
gap: 2px;
text-decoration: none !important;
color: inherit !important;
}
.gf-settings-panel__repo-title { font-size: 13px; font-weight: 700; }
.gf-settings-panel__repo-desc {
font-size: 12px;
color: var(--fgColor-muted, #656d76);
}
@media (prefers-reduced-motion: reduce) {
#${PANEL_ID}, #${HOVERCARD_BLOCK_ID}, .gf-btn-link { transition: none !important; }
}
`);
function normalizeFinish(finish) {
const key = String(finish || 'bronze').toLowerCase();
return FINISH_KEYS.includes(key) ? key : 'bronze';
}
function applyFinishVars(el, finish) {
const meta = finishMeta(finish);
el.style.setProperty('--gf-finish-accent', meta.accent);
el.style.setProperty('--gf-finish-ink', meta.ink);
el.style.setProperty('--gf-finish-soft', meta.soft);
el.style.setProperty('--gf-finish-glow', meta.glow);
el.style.setProperty('--gf-finish-deep', meta.deep);
el.style.setProperty('--gf-finish-shine', meta.shine);
}
function clearFinishClasses(el) {
if (!el?.classList) return;
el.classList.remove('gf-hc-themed');
for (const cls of [...el.classList]) {
if (FINISH_CLASS_RE.test(cls)) el.classList.remove(cls);
}
}
function clearHovercardTheme(popover) {
const message = popover?.querySelector?.('.Popover-message') || popover;
if (!(message instanceof HTMLElement)) return;
clearFinishClasses(message);
delete message.dataset.gfFinish;
message.querySelector('.gf-hc-theme-fx')?.remove();
for (const key of [
'--gf-finish-accent',
'--gf-finish-ink',
'--gf-finish-soft',
'--gf-finish-glow',
'--gf-finish-deep',
'--gf-finish-shine',
]) {
message.style.removeProperty(key);
}
}
function ensureHovercardThemeFx(message) {
let fx = message.querySelector(':scope > .gf-hc-theme-fx');
if (!fx) {
fx = document.createElement('div');
fx.className = 'gf-hc-theme-fx';
fx.setAttribute('aria-hidden', 'true');
message.insertBefore(fx, message.firstChild);
}
fx.innerHTML = '<div class="gf-hc-theme-fx__shine"></div>';
return fx;
}
function applyHovercardTheme(popover, finish) {
const message = popover?.querySelector?.('.Popover-message');
if (!(message instanceof HTMLElement)) return;
const key = normalizeFinish(finish);
clearFinishClasses(message);
message.classList.add('gf-hc-themed', `gf-hc-finish--${key}`);
message.dataset.gfFinish = key;
applyFinishVars(message, key);
ensureHovercardThemeFx(message);
}
function renderStars(count) {
const n = Math.max(0, Math.min(5, Number(count) || 0));
return '★'.repeat(n) + '☆'.repeat(5 - n);
}
function buildProfilePanel(card) {
const root = document.createElement('div');
root.id = PANEL_ID;
const finish = normalizeFinish(card.finish);
root.classList.add(`gf-panel-finish--${finish}`);
applyFinishVars(root, finish);
root.dataset.gfFinish = finish;
const report = card.report || {};
const work = report.workRate || {};
const playstyles = Array.isArray(report.playstyles) ? report.playstyles : [];
const finishLabel = card.finishLabel || String(card.finish || '').toUpperCase();
const cardUrl = `${SITE_BASE}/${encodeURIComponent(card.login)}`;
const duelUrl = buildDuelUrl(card.login);
const statsHtml = STAT_ORDER.map(
([key, label]) => `
<div class="gf-stat">
<span class="gf-stat__value">${escapeHtml(card.stats?.[key] ?? '—')}</span>
<span class="gf-stat__key">${label}</span>
</div>`
).join('');
const playstylesHtml = playstyles.length
? `<div class="gf-playstyles" aria-label="${escapeHtml(t('playstyles'))}">
${playstyles
.map(
(ps) =>
`<span class="gf-playstyle${ps.plus ? ' is-plus' : ''}" title="${escapeHtml(ps.reason || '')}">${escapeHtml(ps.name)}${ps.plus ? '+' : ''}</span>`
)
.join('')}
</div>`
: '';
root.innerHTML = `
<div class="gf-panel-theme-fx" aria-hidden="true"><div class="gf-panel-theme-fx__shine"></div></div>
<div class="gf-panel__inner">
<div class="gf-panel__head">
<div class="gf-panel__ovr" title="${escapeHtml(t('overall'))}">
<span class="gf-panel__ovr-value">${escapeHtml(card.overall)}</span>
<span class="gf-panel__ovr-label">${escapeHtml(t('overall'))}</span>
</div>
<div class="gf-panel__meta">
<div class="gf-panel__title-row">
<span class="gf-panel__name">${escapeHtml(card.name || card.login)}</span>
<span class="gf-chip">${escapeHtml(card.position || '—')}</span>
<span class="gf-chip gf-chip--finish">${escapeHtml(finishLabel)}</span>
</div>
<p class="gf-panel__blurb">${escapeHtml(card.archetype || '')}${card.archetypeBlurb ? ` — ${escapeHtml(card.archetypeBlurb)}` : ''}</p>
</div>
</div>
<div class="gf-panel__body">
<div class="gf-stats">${statsHtml}</div>
<div class="gf-attrs">
<div class="gf-attrs__row"><span class="gf-attrs__label">${escapeHtml(t('skillMoves'))}</span><span class="gf-attrs__value">${escapeHtml(renderStars(report.skillMoves))}</span></div>
<div class="gf-attrs__row"><span class="gf-attrs__label">${escapeHtml(t('weakFoot'))}</span><span class="gf-attrs__value">${escapeHtml(renderStars(report.weakFoot))}</span></div>
<div class="gf-attrs__row"><span class="gf-attrs__label">${escapeHtml(t('workRate'))}</span><span class="gf-attrs__value">${escapeHtml(`${work.attack || '—'} / ${work.defense || '—'}`)}</span></div>
<div class="gf-attrs__row"><span class="gf-attrs__label">${escapeHtml(t('style'))}</span><span class="gf-attrs__value">${escapeHtml(report.style || '—')}</span></div>
${
card.topLanguage
? `<div class="gf-attrs__row"><span class="gf-attrs__label">${escapeHtml(t('language'))}</span><span class="gf-attrs__value">${escapeHtml(card.topLanguage)}</span></div>`
: ''
}
</div>
${playstylesHtml}
<div class="gf-actions">
<a class="gf-btn-link gf-btn-link--primary" href="${escapeHtml(cardUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(t('openReport'))}</a>
<a class="gf-btn-link" href="${escapeHtml(duelUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(t('duel'))}</a>
</div>
</div>
</div>
`;
return root;
}
function buildLoadingPanel() {
const root = document.createElement('div');
root.id = PANEL_ID;
root.dataset.gfState = 'loading';
root.innerHTML = `
<div class="gf-panel__inner gf-panel-skel" role="status" aria-busy="true" aria-label="${escapeHtml(t('loading'))}">
<div class="gf-panel__head">
<div class="gf-skel gf-skel--panel-ovr" aria-hidden="true"></div>
<div class="gf-panel__meta">
<div class="gf-panel__title-row">
<span class="gf-skel gf-skel--panel-name" aria-hidden="true"></span>
<span class="gf-skel gf-skel--panel-chip" aria-hidden="true"></span>
<span class="gf-skel gf-skel--panel-chip" aria-hidden="true"></span>
</div>
<div class="gf-skel gf-skel--panel-blurb" aria-hidden="true"></div>
</div>
</div>
<div class="gf-panel__body">
<div class="gf-stats">
<div class="gf-skel gf-skel--panel-stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--panel-stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--panel-stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--panel-stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--panel-stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--panel-stat" aria-hidden="true"></div>
</div>
<div class="gf-attrs">
<div class="gf-skel gf-skel--panel-attr" aria-hidden="true" style="width:100%"></div>
<div class="gf-skel gf-skel--panel-attr" aria-hidden="true" style="width:92%"></div>
<div class="gf-skel gf-skel--panel-attr" aria-hidden="true" style="width:86%"></div>
<div class="gf-skel gf-skel--panel-attr" aria-hidden="true" style="width:78%"></div>
</div>
<div class="gf-actions">
<span class="gf-skel gf-skel--panel-btn" aria-hidden="true"></span>
<span class="gf-skel gf-skel--panel-btn gf-skel--panel-btn-sm" aria-hidden="true"></span>
</div>
</div>
</div>
`;
return root;
}
function buildStatusPanel(text, isError) {
const root = document.createElement('div');
root.id = PANEL_ID;
root.innerHTML = `<div class="gf-panel__status${isError ? ' is-error' : ''}">${escapeHtml(text)}</div>`;
return root;
}
function findProfileMount() {
const area = document.querySelector('.js-profile-editable-area');
if (!area) return null;
const followBlock = area.querySelector('.flex-order-1.flex-md-order-none');
if (followBlock) return { parent: area, before: followBlock.nextSibling };
const details = area.querySelector('.vcard-details');
if (details) return { parent: area, before: details };
return { parent: area, before: null };
}
function prefersReducedMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
function revealProfilePanel(node, fromHeight) {
if (!(node instanceof HTMLElement)) return;
if (prefersReducedMotion()) return;
const startH = Math.max(0, Number(fromHeight) || 0);
node.style.overflow = 'hidden';
node.style.height = 'auto';
const endH = Math.round(node.getBoundingClientRect().height);
const startMargin = startH < 1 ? 0 : 16;
const endMargin = 16;
if (Math.abs(endH - startH) < 2 && startMargin === endMargin) {
node.style.height = '';
node.style.overflow = '';
node.style.marginBottom = '';
node.style.opacity = '';
return;
}
node.classList.add('is-expanding');
node.style.height = `${startH}px`;
node.style.marginBottom = `${startMargin}px`;
if (startH < 1) node.style.opacity = '0';
void node.offsetHeight;
node.style.height = `${endH}px`;
node.style.marginBottom = `${endMargin}px`;
node.style.opacity = '1';
let settled = false;
const settle = () => {
if (settled) return;
settled = true;
node.classList.remove('is-expanding');
node.style.height = '';
node.style.overflow = '';
node.style.marginBottom = '';
node.style.opacity = '';
node.removeEventListener('transitionend', onEnd);
};
const onEnd = (event) => {
if (event.target !== node) return;
if (event.propertyName !== 'height' && event.propertyName !== 'margin-bottom') return;
settle();
};
node.addEventListener('transitionend', onEnd);
window.setTimeout(settle, 550);
}
function mountProfilePanel(node) {
const existing = document.getElementById(PANEL_ID);
const fromHeight = existing ? existing.getBoundingClientRect().height : 0;
if (existing) existing.remove();
const mount = findProfileMount();
if (!mount) return false;
mount.parent.insertBefore(node, mount.before);
requestAnimationFrame(() => {
requestAnimationFrame(() => revealProfilePanel(node, fromHeight));
});
return true;
}
async function hydrateProfile(username) {
if (!document.getElementById(PANEL_ID)) {
mountProfilePanel(buildLoadingPanel());
}
try {
const card = await fetchCard(username);
if (getProfileUsername()?.toLowerCase() !== username.toLowerCase()) return;
if (!card) {
mountProfilePanel(buildStatusPanel(t('notFound'), false));
return;
}
mountProfilePanel(buildProfilePanel(card));
} catch {
if (getProfileUsername()?.toLowerCase() === username.toLowerCase()) {
mountProfilePanel(buildStatusPanel(t('loadError'), true));
}
}
}
function getHovercardLogin(popover) {
const hydro = popover.querySelector('[data-hydro-view]');
if (hydro) {
try {
const data = JSON.parse(hydro.getAttribute('data-hydro-view') || '');
const login = data?.payload?.card_user_login;
if (login && isValidUsername(login)) return login;
} catch {
/* ignore */
}
}
const avatar = popover.querySelector('a.user-hovercard-avatar, img.user-hovercard-avatar-image');
if (avatar) {
const href = avatar.getAttribute('href') || avatar.closest('a')?.getAttribute('href');
const fromHref = href ? extractUsernameFromHref(href) : null;
if (fromHref) return fromHref;
const alt = avatar.getAttribute('alt') || '';
const m = alt.match(/^@?([A-Za-z0-9-]{1,39})$/);
if (m && isValidUsername(m[1])) return m[1];
}
const loginLink = popover.querySelector('section[aria-label="User login and name"] a.Link--primary');
if (loginLink) {
const fromHref = extractUsernameFromHref(loginLink.href);
if (fromHref) return fromHref;
}
return null;
}
function findHovercardMount(popover) {
const hydro = popover.querySelector('[data-hydro-view]');
if (hydro) return hydro;
return popover.querySelector('.Popover-message > div > div') || popover.querySelector('.Popover-message');
}
function isVisibleHovercard(popover) {
if (!popover || !(popover instanceof HTMLElement)) return false;
if (popover.style.display === 'none') return false;
const style = window.getComputedStyle(popover);
return style.display !== 'none' && style.visibility !== 'hidden';
}
function renderHovercardContent(block, card) {
const finish = normalizeFinish(card.finish);
clearFinishClasses(block);
block.classList.add(`gf-hc-finish--${finish}`);
applyFinishVars(block, finish);
block.dataset.gfFinish = finish;
const finishLabel = card.finishLabel || finish.toUpperCase();
const cardUrl = `${SITE_BASE}/${encodeURIComponent(card.login)}`;
const duelUrl = buildDuelUrl(card.login);
const statsHtml = STAT_ORDER.map(
([key, label]) => `
<div class="gf-hc__stat">
<span class="gf-hc__stat-value">${escapeHtml(card.stats?.[key] ?? '—')}</span>
<span class="gf-hc__stat-key">${label}</span>
</div>`
).join('');
block.innerHTML = `
<div class="gf-hc__head">
<div class="gf-hc__ovr" title="${escapeHtml(t('overall'))}">
<span class="gf-hc__ovr-value">${escapeHtml(card.overall)}</span>
<span class="gf-hc__ovr-label">${escapeHtml(t('overall'))}</span>
</div>
<div class="gf-hc__meta">
<div class="gf-hc__chips">
<span class="gf-chip">${escapeHtml(card.position || '—')}</span>
<span class="gf-chip gf-chip--finish">${escapeHtml(finishLabel)}</span>
${card.topLanguage ? `<span class="gf-chip">${escapeHtml(card.topLanguage)}</span>` : ''}
</div>
<p class="gf-hc__blurb">${escapeHtml(card.archetype || '')}${
card.archetypeBlurb ? ` — ${escapeHtml(card.archetypeBlurb)}` : ''
}</p>
</div>
</div>
<div class="gf-hc__stats">${statsHtml}</div>
<div class="gf-hc__actions">
<a class="gf-hc__link gf-hc__link--primary" href="${escapeHtml(cardUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(t('openReport'))}</a>
<a class="gf-hc__link" href="${escapeHtml(duelUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(t('duel'))}</a>
</div>
`;
const popover = block.closest('.Popover.js-hovercard-content');
if (popover) applyHovercardTheme(popover, finish);
}
function renderHovercardSkeleton() {
return `
<div class="gf-hc__skel" role="status" aria-busy="true" aria-label="${escapeHtml(t('loading'))}">
<div class="gf-hc__head">
<div class="gf-skel gf-skel--ovr" aria-hidden="true"></div>
<div class="gf-hc__meta">
<div class="gf-hc__chips">
<span class="gf-skel gf-skel--chip" aria-hidden="true"></span>
<span class="gf-skel gf-skel--chip" aria-hidden="true"></span>
</div>
<div class="gf-skel gf-skel--blurb" aria-hidden="true"></div>
<div class="gf-skel gf-skel--blurb gf-skel--blurb-short" aria-hidden="true"></div>
</div>
</div>
<div class="gf-hc__stats">
<div class="gf-skel gf-skel--stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--stat" aria-hidden="true"></div>
<div class="gf-skel gf-skel--stat" aria-hidden="true"></div>
</div>
<div class="gf-hc__actions">
<span class="gf-skel gf-skel--btn" aria-hidden="true"></span>
<span class="gf-skel gf-skel--btn gf-skel--btn-sm" aria-hidden="true"></span>
</div>
</div>
`;
}
async function hydrateHovercardBlock(block, login) {
const seq = ++hovercardSeq;
block.dataset.gfLogin = login.toLowerCase();
block.dataset.gfState = 'loading';
block.innerHTML = renderHovercardSkeleton();
try {
const card = await fetchCard(login);
if (seq !== hovercardSeq) return;
if (block.dataset.gfLogin !== login.toLowerCase()) return;
if (!document.contains(block)) return;
if (!card) {
block.dataset.gfState = 'done';
block.innerHTML = `<div class="gf-hc__status">${escapeHtml(t('notFound'))}</div>`;
return;
}
renderHovercardContent(block, card);
block.dataset.gfState = 'ready';
} catch {
if (seq !== hovercardSeq) return;
if (!document.contains(block)) return;
block.dataset.gfState = 'done';
block.innerHTML = `<div class="gf-hc__status is-error">${escapeHtml(t('loadError'))}</div>`;
}
}
function injectIntoHovercard(popover) {
if (!settings.showHovercard) {
popover.querySelector(`#${HOVERCARD_BLOCK_ID}`)?.remove();
clearHovercardTheme(popover);
return;
}
if (!isVisibleHovercard(popover)) {
clearHovercardTheme(popover);
return;
}
if (!popover.querySelector('.user-hovercard-avatar, [data-hydro-view*="user-hovercard"]')) {
clearHovercardTheme(popover);
return;
}
const login = getHovercardLogin(popover);
if (!login) return;
const mount = findHovercardMount(popover);
if (!mount) return;
let block = mount.querySelector(`#${HOVERCARD_BLOCK_ID}`);
if (
block &&
block.dataset.gfLogin === login.toLowerCase() &&
(block.dataset.gfState === 'loading' ||
block.dataset.gfState === 'ready' ||
block.dataset.gfState === 'done')
) {
// Re-apply theme if GitHub rebuilt the Popover-message chrome
if (block.dataset.gfState === 'ready' && block.dataset.gfFinish) {
const message = popover.querySelector('.Popover-message');
if (message && message.dataset.gfFinish !== block.dataset.gfFinish) {
applyHovercardTheme(popover, block.dataset.gfFinish);
}
}
return;
}
if (!block) {
block = document.createElement('div');
block.id = HOVERCARD_BLOCK_ID;
mount.appendChild(block);
}
hydrateHovercardBlock(block, login);
}
function scanHovercards() {
document.querySelectorAll('.Popover.js-hovercard-content').forEach((popover) => {
injectIntoHovercard(popover);
});
}
function ensureHovercardObserver() {
if (hovercardObserver) return;
let timer = null;
hovercardObserver = new MutationObserver((mutations) => {
let relevant = false;
for (const mutation of mutations) {
const target = mutation.target;
if (target instanceof Element) {
if (
target.classList?.contains('js-hovercard-content') ||
target.classList?.contains('Popover-message') ||
target.closest?.('.Popover.js-hovercard-content')
) {
relevant = true;
break;
}
}
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) continue;
if (
node.classList?.contains('js-hovercard-content') ||
node.querySelector?.('.js-hovercard-content, .user-hovercard-avatar')
) {
relevant = true;
break;
}
}
if (relevant) break;
}
if (!relevant) return;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
scanHovercards();
}, 50);
});
hovercardObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class'],
});
}
function scanPage() {
const username = getProfileUsername();
if (username) {
const existing = document.getElementById(PANEL_ID);
const currentLogin = existing?.querySelector('.gf-panel__name')?.textContent?.trim();
if (!existing || (existing.dataset.gfUser || '').toLowerCase() !== username.toLowerCase()) {
if (existing) existing.remove();
const loading = buildLoadingPanel();
loading.dataset.gfUser = username;
mountProfilePanel(loading);
hydrateProfile(username).then(() => {
const panel = document.getElementById(PANEL_ID);
if (panel) panel.dataset.gfUser = username;
});
} else if (!currentLogin && existing.dataset.gfState !== 'loading' && !existing.querySelector('.gf-panel__status')) {
hydrateProfile(username);
}
} else {
document.getElementById(PANEL_ID)?.remove();
}
scanHovercards();
ensureUserMenuItem();
}
function scheduleScan() {
if (scanTimer) clearTimeout(scanTimer);
scanTimer = setTimeout(() => {
scanTimer = null;
scanPage();
}, SCAN_DEBOUNCE_MS);
}
function getGlobalUserMenu() {
const header = document.getElementById('global-nav-user-menu-header');
if (header) {
const dialog = header.closest('[role="dialog"]');
if (dialog) return dialog;
}
return document.querySelector(
'[role="dialog"][aria-labelledby="global-nav-user-menu-header"]'
);
}
function closeGlobalUserMenu() {
const menu = getGlobalUserMenu();
if (!menu) return;
const closeBtn = menu.querySelector('[data-component="Dialog.CloseButton"]');
if (closeBtn instanceof HTMLElement && closeBtn.offsetParent !== null) {
closeBtn.click();
return;
}
const triggers = document.querySelectorAll('header button[aria-expanded="true"], .AppHeader button[aria-expanded="true"]');
for (const trigger of triggers) {
if (!(trigger instanceof HTMLElement)) continue;
trigger.click();
if (!getGlobalUserMenu()) return;
}
menu.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
);
}
function settingsAreCustomized() {
return (
settings.cacheHours !== DEFAULT_SETTINGS.cacheHours ||
settings.showHovercard !== DEFAULT_SETTINGS.showHovercard ||
normalizeCardTheme(settings.cardTheme) !== DEFAULT_SETTINGS.cardTheme ||
normalizeUiLocale(settings.locale) !== DEFAULT_SETTINGS.locale
);
}
function ensureUserMenuItem() {
const menu = getGlobalUserMenu();
if (!menu) return null;
if (menu.querySelector('#gf-user-menu-item')) {
updateSettingsMenuState();
return menu.querySelector('#gf-user-menu-item');
}
const list = menu.querySelector('ul[data-component="ActionList"]');
if (!list) return null;
const buttonItems = [...list.querySelectorAll('li[data-component="ActionList.Item"]')].filter(
(li) => li.querySelector(':scope > button')
);
// Prefer a later action button (e.g. Feature preview) over the status row.
const sampleLi = buttonItems[buttonItems.length - 1] || buttonItems[0];
if (!sampleLi) return null;
const li = /** @type {HTMLElement} */ (sampleLi.cloneNode(true));
li.id = 'gf-user-menu-item';
li.removeAttribute('hidden');
li.classList.remove('hide-whenRegular', 'd-md-none', 'd-lg-none');
const btn = li.querySelector('button');
if (!(btn instanceof HTMLButtonElement)) return null;
btn.removeAttribute('id');
btn.removeAttribute('aria-labelledby');
btn.removeAttribute('aria-describedby');
btn.removeAttribute('aria-expanded');
btn.removeAttribute('aria-haspopup');
btn.type = 'button';
const label = li.querySelector('[data-component="ActionList.Item.Label"]');
if (label) {
label.textContent = t('navMenuItem');
label.removeAttribute('id');
}
let visual = li.querySelector('[data-component="ActionList.LeadingVisual"]');
if (!visual) {
visual = document.createElement('span');
visual.setAttribute('data-component', 'ActionList.LeadingVisual');
visual.className =
sampleLi.querySelector('[data-component="ActionList.LeadingVisual"]')?.className ||
'prc-ActionList-LeadingVisual-NBr28 prc-ActionList-VisualWrap-bdCsS';
const spacer = btn.querySelector('[class*="Spacer"]');
if (spacer?.nextSibling) btn.insertBefore(visual, spacer.nextSibling);
else btn.insertBefore(visual, btn.firstChild);
}
visual.innerHTML =
'<svg data-component="Octicon" aria-hidden="true" focusable="false" class="octicon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom">' +
'<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Z"/>' +
'<path d="M8 3.25a.75.75 0 0 1 .67.415l.9 1.8 1.99.29a.75.75 0 0 1 .416 1.279l-1.44 1.404.34 1.982a.75.75 0 0 1-1.088.791L8 10.347l-1.788.94a.75.75 0 0 1-1.088-.79l.34-1.983-1.44-1.404a.75.75 0 0 1 .416-1.28l1.99-.289.9-1.8A.75.75 0 0 1 8 3.25Z"/>' +
'</svg>';
let trailing = li.querySelector('[data-component="ActionList.TrailingVisual"]');
const sub = li.querySelector('[data-component="ActionList.Item--DividerContainer"]');
if (!trailing && sub) {
trailing = document.createElement('span');
trailing.setAttribute('data-component', 'ActionList.TrailingVisual');
trailing.className =
sampleLi.querySelector('[data-component="ActionList.TrailingVisual"]')?.className ||
'prc-ActionList-TrailingVisual-jwT9C prc-ActionList-VisualWrap-bdCsS';
sub.appendChild(trailing);
}
if (trailing) {
trailing.innerHTML =
'<span class="gf-nav-dot" id="gf-settings-dot" aria-hidden="true"></span>';
}
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
closeGlobalUserMenu();
window.setTimeout(() => togglePanel(true), 50);
});
const settingsLink = [...list.querySelectorAll('a[href]')].find((a) => {
try {
return new URL(a.href, location.origin).pathname === '/settings/profile';
} catch {
return a.getAttribute('href') === '/settings/profile';
}
});
const settingsLi = settingsLink?.closest('li');
const signOutLink = [...list.querySelectorAll('a[href]')].find((a) => {
try {
return new URL(a.href, location.origin).pathname === '/logout';
} catch {
return a.getAttribute('href') === '/logout';
}
});
const signOutLi = signOutLink?.closest('li');
if (settingsLi) {
settingsLi.after(li);
} else if (signOutLi) {
signOutLi.before(li);
} else {
list.appendChild(li);
}
ensurePanel();
updateSettingsMenuState();
return li;
}
function updateSettingsMenuState() {
const dot = document.getElementById('gf-settings-dot');
if (!dot) return;
const customized = settingsAreCustomized();
dot.classList.toggle('is-on', customized);
dot.title = customized ? t('on') : t('off');
}
function ensurePanel() {
if (document.getElementById('gf-panel')) return;
const panel = document.createElement('div');
panel.id = 'gf-panel';
panel.className = 'gf-settings-panel';
panel.hidden = true;
panel.innerHTML = `
<div class="gf-settings-panel__header">
<div>
<div class="gf-settings-panel__title">${escapeHtml(t('panelTitle'))}</div>
<div class="gf-settings-panel__subtitle">${escapeHtml(t('panelSubtitle'))}</div>
<div class="gf-settings-panel__version">${escapeHtml(t('version', { version: SCRIPT_VERSION }))}</div>
</div>
<button type="button" class="gf-settings-panel__close" data-gf="close" aria-label="${escapeHtml(t('close'))}">×</button>
</div>
<div class="gf-settings-panel__section">
<div class="gf-settings-panel__section-title">${escapeHtml(t('sectionDisplay'))}</div>
<label class="gf-field">
<span class="gf-field__label">${escapeHtml(t('uiLanguage'))}</span>
<select id="gf-ui-locale">
<option value="auto">${escapeHtml(t('uiLanguageAuto'))}</option>
${SUPPORTED_LOCALES.map(
(code) =>
`<option value="${code}">${escapeHtml(LOCALE_NATIVE_NAMES[code] || code)}</option>`
).join('')}
</select>
</label>
<p class="gf-hint">${escapeHtml(t('uiLanguageHint'))}</p>
<label class="gf-field">
<span class="gf-field__label">${escapeHtml(t('cardTheme'))}</span>
<select id="gf-card-theme">
<option value="standard">${escapeHtml(t('cardThemeStandard'))}</option>
<option value="github">${escapeHtml(t('cardThemeGithub'))}</option>
<option value="fifa">${escapeHtml(t('cardThemeFifa'))}</option>
<option value="neon">${escapeHtml(t('cardThemeNeon'))}</option>
</select>
</label>
<p class="gf-hint">${escapeHtml(t('cardThemeHint'))}</p>
<div class="gf-row" style="margin-bottom:10px">
<label class="gf-switch">
<input type="checkbox" id="gf-show-hovercard" />
<span>${escapeHtml(t('showHovercard'))}</span>
</label>
<span class="gf-pill" id="gf-show-hovercard-pill">${escapeHtml(t('off'))}</span>
</div>
<p class="gf-hint">${escapeHtml(t('showHovercardHint'))}</p>
</div>
<div class="gf-settings-panel__divider"></div>
<div class="gf-settings-panel__section">
<div class="gf-settings-panel__section-title">${escapeHtml(t('sectionCache'))}</div>
<div class="gf-cache-stats" id="gf-cache-stats">
<div class="gf-cache-stats__count" id="gf-cache-count"></div>
<p class="gf-cache-stats__storage" id="gf-cache-storage"></p>
<div
class="gf-cache-stats__bar"
id="gf-cache-bar"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="100"
aria-label="${escapeHtml(t('cacheStorageHint', { budget: formatBytes(CACHE_BUDGET_BYTES) }))}"
>
<div class="gf-cache-stats__bar-fill" id="gf-cache-bar-fill"></div>
</div>
<p class="gf-hint gf-cache-stats__hint">${escapeHtml(t('cacheStorageHint', { budget: formatBytes(CACHE_BUDGET_BYTES) }))}</p>
</div>
<label class="gf-field">
<span class="gf-field__label">${escapeHtml(t('cacheHours'))}</span>
<input type="number" id="gf-cache-hours" min="0" max="${CACHE_HOURS_MAX}" step="1" placeholder="12" inputmode="numeric" />
</label>
<p class="gf-hint">${escapeHtml(t('cacheHoursHint'))}</p>
<button type="button" class="gf-btn gf-btn--danger" data-gf="clear-cache">${escapeHtml(t('clearCache'))}</button>
<p class="gf-hint">${escapeHtml(t('cacheClearHint'))}</p>
<p class="gf-cache-status" id="gf-cache-status" aria-live="polite"></p>
</div>
<div class="gf-settings-panel__footer">
<div class="gf-settings-panel__footer-actions">
<button type="button" class="gf-btn gf-btn--ghost" data-gf="close">${escapeHtml(t('cancel'))}</button>
<button type="button" class="gf-btn" data-gf="save">${escapeHtml(t('save'))}</button>
<button type="button" class="gf-btn gf-btn--green" data-gf="save-run">${escapeHtml(t('saveReload'))}</button>
</div>
<div class="gf-settings-panel__footer-divider" role="separator"></div>
<a class="gf-settings-panel__repo" href="${REPO_URL}" target="_blank" rel="noopener noreferrer">
<span class="gf-settings-panel__repo-title">${escapeHtml(t('repoLink'))}</span>
<span class="gf-settings-panel__repo-desc">${escapeHtml(t('repoAbout'))}</span>
</a>
</div>
`;
document.body.appendChild(panel);
panel.addEventListener('click', (e) => e.stopPropagation());
panel.querySelectorAll('[data-gf="close"]').forEach((el) =>
el.addEventListener('click', () => togglePanel(false))
);
panel.querySelector('[data-gf="save"]').addEventListener('click', () => {
const localeChanged = persistPanelForm();
if (localeChanged) {
location.reload();
return;
}
togglePanel(false);
});
panel.querySelector('[data-gf="save-run"]').addEventListener('click', () => {
persistPanelForm();
togglePanel(false);
location.reload();
});
panel.querySelector('[data-gf="clear-cache"]').addEventListener('click', () => {
const count = clearCardCache();
const status = panel.querySelector('#gf-cache-status');
if (status) status.textContent = count > 0 ? t('cacheCleared', { count }) : t('cacheEmpty');
updateCacheStatsUI();
});
panel.querySelector('#gf-show-hovercard').addEventListener('change', syncDisplayPills);
document.addEventListener('click', (e) => {
if (!panelOpen) return;
const menuItem = document.getElementById('gf-user-menu-item');
if (
panel.contains(/** @type {Node} */ (e.target)) ||
menuItem?.contains(/** @type {Node} */ (e.target))
) {
return;
}
togglePanel(false);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && panelOpen) togglePanel(false);
});
}
function syncDisplayPills() {
const panel = document.getElementById('gf-panel');
if (!panel) return;
const hoverOn = panel.querySelector('#gf-show-hovercard').checked;
const hoverPill = panel.querySelector('#gf-show-hovercard-pill');
hoverPill.textContent = hoverOn ? t('on') : t('off');
hoverPill.classList.toggle('is-on', hoverOn);
}
function fillPanelForm() {
const panel = document.getElementById('gf-panel');
if (!panel) return;
panel.querySelector('#gf-ui-locale').value = normalizeUiLocale(settings.locale);
panel.querySelector('#gf-show-hovercard').checked = settings.showHovercard !== false;
panel.querySelector('#gf-card-theme').value = normalizeCardTheme(settings.cardTheme);
panel.querySelector('#gf-cache-hours').value = String(normalizeCacheHours(settings.cacheHours));
const status = panel.querySelector('#gf-cache-status');
if (status) status.textContent = '';
syncDisplayPills();
updateCacheStatsUI();
}
/** @returns {boolean} true if UI locale preference changed (caller should reload) */
function persistPanelForm() {
const panel = document.getElementById('gf-panel');
if (!panel) return false;
const prevLocale = normalizeUiLocale(settings.locale);
saveSettings({
locale: panel.querySelector('#gf-ui-locale').value,
showHovercard: panel.querySelector('#gf-show-hovercard').checked,
cardTheme: panel.querySelector('#gf-card-theme').value,
cacheHours: normalizeCacheHours(panel.querySelector('#gf-cache-hours').value),
});
return normalizeUiLocale(settings.locale) !== prevLocale;
}
function togglePanel(force) {
ensurePanel();
const panel = document.getElementById('gf-panel');
if (!panel) return;
panelOpen = typeof force === 'boolean' ? force : !panelOpen;
panel.hidden = !panelOpen;
if (panelOpen) {
fillPanelForm();
positionPanel();
}
}
function positionPanel() {
const panel = document.getElementById('gf-panel');
if (!panel) return;
const width = 360;
const anchor =
document.getElementById('gf-user-menu-item') ||
getGlobalUserMenu() ||
document.querySelector('header .AppHeader-user, header [data-component="Avatar"], header img.avatar');
let left;
let top;
if (anchor) {
const rect = anchor.getBoundingClientRect();
left = rect.right - width;
top = rect.bottom + 8;
if (top < 56) top = 56;
} else {
left = window.innerWidth - width - 16;
top = 56;
}
if (left < 8) left = 8;
if (left + width > window.innerWidth - 8) left = window.innerWidth - width - 8;
if (top + 40 > window.innerHeight - 8) top = Math.max(8, window.innerHeight - 40);
panel.style.top = `${Math.round(top)}px`;
panel.style.left = `${Math.round(left)}px`;
}
function onSoftNavigation() {
scheduleScan();
ensureUserMenuItem();
}
function init() {
loadMemoryCache();
applyCardTheme();
window.addEventListener('pagehide', persistCacheNow);
ensurePanel();
ensureUserMenuItem();
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand(t('menuSettings'), () => {
togglePanel(true);
});
}
scheduleScan();
ensureHovercardObserver();
mutationObserver = new MutationObserver((mutations) => {
let relevant = false;
for (const mutation of mutations) {
if (mutation.type !== 'childList') continue;
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) continue;
if (
node.id === PANEL_ID ||
node.id === 'gf-panel' ||
node.id === 'gf-user-menu-item' ||
node.id === HOVERCARD_BLOCK_ID
) {
continue;
}
if (
node.id === 'global-nav-user-menu-header' ||
node.getAttribute?.('aria-labelledby') === 'global-nav-user-menu-header' ||
(node.matches?.('ul[data-component="ActionList"]') && getGlobalUserMenu()?.contains(node)) ||
node.querySelector?.(
'#global-nav-user-menu-header, [aria-labelledby="global-nav-user-menu-header"]'
)
) {
ensureUserMenuItem();
continue;
}
if (
node.matches?.(
'.js-profile-editable-area, .Layout-sidebar, .Popover.js-hovercard-content, main'
) ||
node.querySelector?.(
'.js-profile-editable-area, .Popover.js-hovercard-content'
)
) {
relevant = true;
break;
}
}
if (relevant) break;
}
if (!relevant) return;
scheduleScan();
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
document.addEventListener('turbo:load', onSoftNavigation);
document.addEventListener('turbo:render', onSoftNavigation);
document.addEventListener('pjax:end', onSoftNavigation);
document.addEventListener('soft-nav:success', onSoftNavigation);
window.addEventListener('popstate', onSoftNavigation);
}
init();
})();