// ==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 // @updateURL https://raw.githubusercontent.com/NemoKing1210/github-gitfut/main/github-gitfut.user.js // @downloadURL https://raw.githubusercontent.com/NemoKing1210/github-gitfut/main/github-gitfut.user.js // @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>} */ const inflight = new Map(); /** @type {Array<{ task: () => Promise, resolve: Function, reject: Function }>} */ const queue = []; let activeRequests = 0; /** @type {Record|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, '''); } 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 = '
'; 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]) => `
${escapeHtml(card.stats?.[key] ?? '—')} ${label}
` ).join(''); const playstylesHtml = playstyles.length ? `
${playstyles .map( (ps) => `${escapeHtml(ps.name)}${ps.plus ? '+' : ''}` ) .join('')}
` : ''; root.innerHTML = `
${escapeHtml(card.overall)} ${escapeHtml(t('overall'))}
${escapeHtml(card.name || card.login)} ${escapeHtml(card.position || '—')} ${escapeHtml(finishLabel)}

${escapeHtml(card.archetype || '')}${card.archetypeBlurb ? ` — ${escapeHtml(card.archetypeBlurb)}` : ''}

${statsHtml}
${escapeHtml(t('skillMoves'))}${escapeHtml(renderStars(report.skillMoves))}
${escapeHtml(t('weakFoot'))}${escapeHtml(renderStars(report.weakFoot))}
${escapeHtml(t('workRate'))}${escapeHtml(`${work.attack || '—'} / ${work.defense || '—'}`)}
${escapeHtml(t('style'))}${escapeHtml(report.style || '—')}
${ card.topLanguage ? `
${escapeHtml(t('language'))}${escapeHtml(card.topLanguage)}
` : '' }
${playstylesHtml}
`; return root; } function buildLoadingPanel() { const root = document.createElement('div'); root.id = PANEL_ID; root.dataset.gfState = 'loading'; root.innerHTML = `
`; return root; } function buildStatusPanel(text, isError) { const root = document.createElement('div'); root.id = PANEL_ID; root.innerHTML = `
${escapeHtml(text)}
`; 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]) => `
${escapeHtml(card.stats?.[key] ?? '—')} ${label}
` ).join(''); block.innerHTML = `
${escapeHtml(card.overall)} ${escapeHtml(t('overall'))}
${escapeHtml(card.position || '—')} ${escapeHtml(finishLabel)} ${card.topLanguage ? `${escapeHtml(card.topLanguage)}` : ''}

${escapeHtml(card.archetype || '')}${ card.archetypeBlurb ? ` — ${escapeHtml(card.archetypeBlurb)}` : '' }

${statsHtml}
`; const popover = block.closest('.Popover.js-hovercard-content'); if (popover) applyHovercardTheme(popover, finish); } function renderHovercardSkeleton() { return `
`; } 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 = `
${escapeHtml(t('notFound'))}
`; return; } renderHovercardContent(block, card); block.dataset.gfState = 'ready'; } catch { if (seq !== hovercardSeq) return; if (!document.contains(block)) return; block.dataset.gfState = 'done'; block.innerHTML = `
${escapeHtml(t('loadError'))}
`; } } 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 = ''; 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 = ''; } 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 = `
${escapeHtml(t('panelTitle'))}
${escapeHtml(t('panelSubtitle'))}
${escapeHtml(t('version', { version: SCRIPT_VERSION }))}
${escapeHtml(t('sectionDisplay'))}

${escapeHtml(t('uiLanguageHint'))}

${escapeHtml(t('cardThemeHint'))}

${escapeHtml(t('off'))}

${escapeHtml(t('showHovercardHint'))}

${escapeHtml(t('sectionCache'))}

${escapeHtml(t('cacheStorageHint', { budget: formatBytes(CACHE_BUDGET_BYTES) }))}

${escapeHtml(t('cacheHoursHint'))}

${escapeHtml(t('cacheClearHint'))}

`; 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(); })();