commit ba7137a64eee69783b03c04bd2aa109e4a956e70 Author: zhangyuheng Date: Tue Feb 10 13:01:46 2026 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46e0b8e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/stats \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..75e42bb Binary files /dev/null and b/favicon.ico differ diff --git a/fund_progress.txt b/fund_progress.txt new file mode 100644 index 0000000..b00166d --- /dev/null +++ b/fund_progress.txt @@ -0,0 +1 @@ +服务器升级M4 Mac Mini 32G,2620,5000 diff --git a/index.html b/index.html new file mode 100644 index 0000000..b638d19 --- /dev/null +++ b/index.html @@ -0,0 +1,214 @@ + + + + + + 白鹿原 Minecraft 服务器 + + + + + + + + + + + + + +
+
+
+

白鹿原 Minecraft

+

永不换档的纯净原版服务器

+ +
+ 已稳定运行 00 小时 00 秒 +
+ +
+
+ mcpure.lunadeer.cn + + 已复制 +
+

点击复制服务器地址

+ + +
+
+ + 正在获取状态... +
+
+
加载中...
+
+
+
+
+
+ + +
+
+ + +
+ +
+
+
+ +

纯净原版

+

无纷繁复杂的 Mod,无破坏平衡的插件。一切简单的就像是单机模式的共享一般。

+
+
+ + +
+
+
+ +

深度自研

+

全栈自研核心,拒绝卡脖子,保证可持续发展。

+
+
+ + +
+
+
+ +

原汁原味

+

生物生成、红石参数与单机完全一致。

+
+
+ + +
+
+
+ +

免费圈地

+

2048*2048 超大领地

+
+
+
+
+
+ +

基岩互通

+

手机电脑随时畅玩

+
+
+
+
+
+ +

自有硬件

+

物理工作站,永不跑路

+
+
+
+
+
+ +

娱乐玩法

+

空岛、跑酷、小游戏

+
+
+ + +
+
+ +

紧跟新版

+

紧跟 Paper 核心版本更新,始终保持在版本前列。第一时间体验 Minecraft 的最新内容。

+
+
+ + +
+
+ +

新手指南

+

完善的服务器文档与活跃的社区,帮助你快速上手,加入白鹿原大家庭。

+
+
+
+
+
+ + +
+
+

特别鸣谢

+
+ +
+
+ +
+
+
+ + + + + + + + + + + + diff --git a/pigwei.html b/pigwei.html new file mode 100644 index 0000000..6aab5e7 --- /dev/null +++ b/pigwei.html @@ -0,0 +1,717 @@ + + + + + + 薇薇变小猪倒计时 + + + + + + +
🍬
+
🧋
+
💖
+
🧁
+ +
+
+ + + + + + + + + + + + + + + + +
哼哼~
+
+ +

薇薇变小猪倒计时

+ +
距离变小猪还有...
+ +
+
+ 00 + Days +
+
+ 00 + Hours +
+
+ 00 + Mins +
+
+ 00 + Secs +
+
+ +
+ Loading Happiness... + 0.000% +
+
+
+
+ +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Zzz... +
+
+
+ + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..5bceac2 --- /dev/null +++ b/script.js @@ -0,0 +1,258 @@ +function copyIp() { + const ipText = document.getElementById('server-ip').innerText; + const tooltip = document.getElementById('copy-tooltip'); + + navigator.clipboard.writeText(ipText).then(() => { + tooltip.innerText = "已复制!"; + tooltip.classList.add('show'); + + setTimeout(() => { + tooltip.classList.remove('show'); + setTimeout(() => { + tooltip.innerText = "点击复制 IP"; + }, 200); // Wait for fade out + }, 2000); + }).catch(err => { + console.error('无法复制文本: ', err); + tooltip.innerText = "复制失败"; + tooltip.classList.add('show'); + setTimeout(() => { + tooltip.classList.remove('show'); + }, 2000); + }); +} + +// Sponsors Logic +document.addEventListener('DOMContentLoaded', () => { + fetchSponsors(); + fetchCrowdfunding(); + setupModal(); + fetchServerStatus(); + startRuntimeTimer(); +}); + +function startRuntimeTimer() { + const startTime = new Date("2021-09-14T09:57:59").getTime(); + + function update() { + const now = new Date().getTime(); + const diff = now - startTime; + + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + const daysEl = document.getElementById("runtime-days"); + const hoursEl = document.getElementById("runtime-hours"); + const minutesEl = document.getElementById("runtime-minutes"); + const secondsEl = document.getElementById("runtime-seconds"); + + if (daysEl) daysEl.innerText = days; + if (hoursEl) hoursEl.innerText = hours; + if (minutesEl) minutesEl.innerText = minutes; + if (secondsEl) secondsEl.innerText = seconds; + } + + update(); + setInterval(update, 1000); +} + +async function fetchServerStatus() { + const countElement = document.getElementById('online-count'); + const listElement = document.getElementById('players-list'); + const dotElement = document.querySelector('.status-dot'); + + try { + const response = await fetch('https://api.mcstatus.io/v2/status/java/mcpure.lunadeer.cn'); + const data = await response.json(); + + if (data.online) { + countElement.innerText = `在线人数: ${data.players.online} / ${data.players.max}`; + dotElement.classList.remove('offline'); + + if (data.players.list && data.players.list.length > 0) { + listElement.innerHTML = data.players.list.map(player => ` +
+ + ${player.name_raw} +
+ `).join(''); + } else { + listElement.innerHTML = '
暂无玩家在线
'; + } + } else { + countElement.innerText = '服务器离线'; + dotElement.classList.add('offline'); + listElement.innerHTML = '
服务器离线
'; + } + } catch (error) { + console.error('Error fetching server status:', error); + countElement.innerText = '无法获取状态'; + dotElement.classList.add('offline'); + listElement.innerHTML = '
获取失败
'; + } +} + +async function fetchCrowdfunding() { + try { + console.log('Fetching crowdfunding data...'); + const response = await fetch('fund_progress.txt'); + if (!response.ok) { + console.error('Failed to fetch fund_progress.txt:', response.status, response.statusText); + return; + } + + const text = await response.text(); + console.log('Crowdfunding data received:', text); + const lines = text.trim().split('\n'); + + const funds = []; + lines.forEach(line => { + // Replace Chinese comma with English comma just in case + const normalizedLine = line.replace(/,/g, ','); + const parts = normalizedLine.split(','); + if (parts.length >= 3) { + const name = parts[0].trim(); + const current = parseFloat(parts[1].trim()); + const target = parseFloat(parts[2].trim()); + + if (name && !isNaN(current) && !isNaN(target)) { + funds.push({ name, current, target }); + } + } + }); + console.log('Parsed funds:', funds); + + if (funds.length > 0) { + renderCrowdfunding(funds); + const section = document.getElementById('crowdfunding-section'); + if (section) { + section.style.display = 'block'; + console.log('Crowdfunding section displayed'); + } else { + console.error('Crowdfunding section element not found'); + } + } else { + console.warn('No valid crowdfunding data found'); + } + + } catch (error) { + console.error('Error loading crowdfunding data:', error); + } +} + +function renderCrowdfunding(funds) { + const container = document.getElementById('crowdfunding-grid'); + + container.innerHTML = funds.map(fund => { + const percentage = Math.min(100, Math.max(0, (fund.current / fund.target) * 100)); + return ` +
+
+
${fund.name}
+
+ ¥${fund.current} / ¥${fund.target} +
+
+
+
+
+
${percentage.toFixed(1)}%
+
+ `; + }).join(''); +} + +async function fetchSponsors() { + try { + const response = await fetch('sponsors.txt'); + const text = await response.text(); + const lines = text.trim().split('\n'); + + const sponsors = []; + const userTotals = {}; + + lines.forEach(line => { + const parts = line.split(','); + if (parts.length >= 3) { + const name = parts[0].trim(); + const project = parts[1].trim(); + const amountStr = parts[2].trim().replace('¥', ''); + const amount = parseFloat(amountStr); + const date = parts[3] ? parts[3].trim() : ''; + + if (!isNaN(amount)) { + sponsors.push({ name, project, amount, date }); + + if (userTotals[name]) { + userTotals[name] += amount; + } else { + userTotals[name] = amount; + } + } + } + }); + + // Sort users by total amount for Top 3 + const sortedUsers = Object.keys(userTotals).map(name => ({ + name, + total: userTotals[name] + })).sort((a, b) => b.total - a.total); + + renderTopSponsors(sortedUsers.slice(0, 3)); + renderSponsorsTable(sponsors); + + } catch (error) { + console.error('Error loading sponsors:', error); + } +} + +function renderTopSponsors(topUsers) { + const container = document.getElementById('top-sponsors'); + const medals = ['🥇', '🥈', '🥉']; + + container.innerHTML = topUsers.map((user, index) => ` + + `).join(''); +} + +function renderSponsorsTable(sponsors) { + const tbody = document.querySelector('#sponsors-table tbody'); + // Sort by date descending (newest first) or keep original order? + // Usually original order in file is chronological. Let's reverse it to show newest first. + const reversedSponsors = [...sponsors].reverse(); + + tbody.innerHTML = reversedSponsors.map(s => ` + + ${s.name} + ${s.project} + ¥${s.amount.toFixed(2)} + ${s.date} + + `).join(''); +} + +function setupModal() { + const modal = document.getElementById('sponsors-modal'); + const btn = document.getElementById('view-sponsors-btn'); + const span = document.getElementsByClassName('close-modal')[0]; + + btn.onclick = function() { + modal.style.display = "flex"; + } + + span.onclick = function() { + modal.style.display = "none"; + } + + window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = "none"; + } + } +} diff --git a/sponsors.txt b/sponsors.txt new file mode 100644 index 0000000..a206f68 --- /dev/null +++ b/sponsors.txt @@ -0,0 +1,36 @@ +TowardsCommunism,服务器硬件升级,20¥,2024-11-22 +Kun_Wu,服务器硬件升级,50¥,2024-11-22 +ZhuTiZi_y,服务器硬件升级,20¥,2024-11-22 +Moon_Bridge,服务器硬件升级,50¥,2024-11-22 +Clincded_Xsa,服务器硬件升级,50¥,2024-11-22 +Genera1314,服务器硬件升级,20¥,2024-11-22 +Huakou,服务器硬件升级,50¥,2024-11-22 +Director_HOU,服务器硬件升级,20¥,2024-11-23 +Cinetank,服务器硬件升级,50¥,2024-11-23 +33zi,服务器硬件升级,70¥,2024-11-23 +obligationi,服务器硬件升级,100¥,2024-11-23 +himscars,服务器硬件升级,50¥,2024-11-23 +TowardsCommunism,服务器硬件升级,50¥,2024-11-23 +dghyuiok,服务器硬件升级,20¥,2024-11-23 +rainy_daily,服务器硬件升级,50¥,2024-11-23 +OG Trouble,电费,5¥,2024-11-25 +33zi,服务器硬件升级,20¥,2024-12-7 +Kim,服务器硬件升级,100¥,2024-12-9 +Su_,服务器硬件升级,50¥,2024-12-9 +ZhuTiZi_y,服务器硬件升级,100¥,2024-12-17 +M1AOYIN,服务器硬件升级,1000¥,2024-12-25 +TowardsCommunism,服务器硬件升级,20¥,2024-12-25 +流明,服务器硬件升级,50¥,2024-12-25 +MISS_U,服务器硬件升级,100¥,2025-1-18 +Treasure_yu,服务器硬件升级,30¥,2025-1-28 +Huakou,服务器硬件升级,100¥,2025-1-31 +Forever_Qi,服务器硬件升级,20¥,2025-2-21 +C0ldWood,服务器硬件升级,20¥,2025-3-1 +天气,服务器硬件升级,50¥,2025-3-23 +天气,服务器硬件升级,20¥,2025-3-26 +Bear_Brother,电费赞助,30¥,2025-4-20 +Kun_Wu,服务器硬件升级,220¥,2025-4-21 +CN_snowman,电费赞助,900¥,2025-5-2 +BE.BackedKey9120,电费赞助,20¥,2025-5-19 +wait_running,服务器硬件升级,100¥,2025-11-26 +NthM7,服务器硬件升级,20¥,2025-12-2 diff --git a/stats.html b/stats.html new file mode 100644 index 0000000..f35aea0 --- /dev/null +++ b/stats.html @@ -0,0 +1,376 @@ + + + + + + 玩家数据 - 白鹿原 Minecraft 服务器 + + + + + + + + + + + + + + +
+
+
+

数据中心

+

记录每一位冒险者的足迹

+
+
+ + +
+
+ + +

排行榜

+
+ +
+
+
+
旅行者
+
+
+
加载中...
+
+
+ + +
+
+
+
搬石大师
+
+
+
加载中...
+
+
+ + +
+
+
+
挖挖机
+
+
+
加载中...
+
+
+ + +
+
+
+
亡灵
+
+
+
加载中...
+
+
+
+ + +

玩家档案

+ +
+ + +
+ +
+ +
+ +
+ +
+ +
正在从服务器获取数据...
+
+
+ + + + + + + + + diff --git a/stats_script.js b/stats_script.js new file mode 100644 index 0000000..4c8ac51 --- /dev/null +++ b/stats_script.js @@ -0,0 +1,188 @@ +document.addEventListener('DOMContentLoaded', () => { + fetchStats(); + setupModal(); + setupSearch(); + setupLoadMore(); +}); + +let allPlayers = []; +let displayedPlayers = []; +let currentPage = 1; +const pageSize = 24; + +async function fetchStats() { + try { + const response = await fetch('stats/summary.json'); + if (!response.ok) throw new Error('Failed to load stats'); + + const data = await response.json(); + allPlayers = data.players; + + // Hide loading + document.getElementById('loading-indicator').style.display = 'none'; + + // Render things + renderLeaderboards(); + + // Initial Grid Render + displayedPlayers = allPlayers; // Start with all + renderPlayerGrid(true); // reset + + } catch (error) { + console.error('Error:', error); + document.getElementById('loading-indicator').innerText = "加载数据失败,请稍后重试。"; + } +} + +function renderLeaderboards() { + // Helper to sort and slice + const getTop = (key, subKey) => { + return [...allPlayers] + .sort((a, b) => { + let valA = subKey ? a.stats[key] : a.stats[key]; // if structure allows direct access + let valB = subKey ? b.stats[key] : b.stats[key]; + + // Special case for walk which has raw sorting value + if (key === 'walk_fmt') valA = a.stats.walk_raw; + if (key === 'walk_fmt') valB = b.stats.walk_raw; + + return valB - valA; + }) + .slice(0, 4); // Top 4 + }; + + const renderCard = (elementId, players, valueFormatter) => { + const container = document.getElementById(elementId); + if (!players || players.length === 0) { + container.innerHTML = '
暂无数据
'; + return; + } + + const top1 = players[0]; + let html = ` +
+ +
${top1.name}
+
${valueFormatter(top1)}
+
+
+ `; + + for (let i = 1; i < players.length; i++) { + const p = players[i]; + html += ` +
+
+ ${i+1} + ${p.name} +
+ ${valueFormatter(p)} +
+ `; + } + html += '
'; + container.innerHTML = html; + }; + + // 1. Walk (stats.walk_raw) + const topWalkers = getTop('walk_fmt'); // uses walk_raw internally + renderCard('lb-walk', topWalkers, p => p.stats.walk_fmt); + + // 2. Placed (stats.placed) + const topPlacers = getTop('placed'); + renderCard('lb-placed', topPlacers, p => p.stats.placed.toLocaleString()); + + // 3. Mined (stats.mined) + const topMiners = getTop('mined'); + renderCard('lb-mined', topMiners, p => p.stats.mined.toLocaleString()); + + // 4. Deaths (stats.deaths) + const topDeaths = getTop('deaths'); + renderCard('lb-deaths', topDeaths, p => p.stats.deaths); +} + +function renderPlayerGrid(reset = false) { + const grid = document.getElementById('players-grid'); + const loadMoreBtn = document.getElementById('load-more-btn'); + + if (reset) { + grid.innerHTML = ''; + currentPage = 1; + } + + const start = (currentPage - 1) * pageSize; + const end = start + pageSize; + const items = displayedPlayers.slice(start, end); + + items.forEach(p => { + const card = document.createElement('div'); + card.className = 'player-card'; + card.onclick = () => openModal(p); + + card.innerHTML = ` + +

${p.name}

+ + `; + grid.appendChild(card); + }); + + if (end >= displayedPlayers.length) { + loadMoreBtn.style.display = 'none'; + } else { + loadMoreBtn.style.display = 'inline-block'; + } +} + +function setupLoadMore() { + document.getElementById('load-more-btn').addEventListener('click', () => { + currentPage++; + renderPlayerGrid(false); + }); +} + +function setupSearch() { + const input = document.getElementById('player-search'); + input.addEventListener('input', (e) => { + const term = e.target.value.toLowerCase().trim(); + if (!term) { + displayedPlayers = allPlayers; + } else { + displayedPlayers = allPlayers.filter(p => + p.name.toLowerCase().includes(term) || + p.uuid.toLowerCase().includes(term) + ); + } + renderPlayerGrid(true); + }); +} + +// Modal Logic +const modal = document.getElementById("player-modal"); +const span = document.getElementsByClassName("close-modal")[0]; + +function setupModal() { + span.onclick = function() { + modal.style.display = "none"; + } + window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = "none"; + } + } +} + +function openModal(player) { + document.getElementById('modal-name').innerText = player.name; + document.getElementById('modal-uuid').innerText = player.uuid; + const avatar = document.getElementById('modal-avatar'); + avatar.src = player.avatar; + avatar.onerror = () => { avatar.src = `https://crafatar.com/avatars/${player.uuid}?size=64&overlay`; }; + + document.getElementById('modal-walk').innerText = player.stats.walk_fmt; + document.getElementById('modal-placed').innerText = player.stats.placed.toLocaleString(); + document.getElementById('modal-mined').innerText = player.stats.mined.toLocaleString(); + document.getElementById('modal-deaths').innerText = player.stats.deaths; + + modal.style.display = "block"; +} diff --git a/statsprocess.py b/statsprocess.py new file mode 100644 index 0000000..4cce493 --- /dev/null +++ b/statsprocess.py @@ -0,0 +1,180 @@ +import os +import json +import requests +import re +import time +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime + +BASE_URL = "http://x2.sjcmc.cn:15960/stats/" +STATS_DIR = "stats" +IMAGE_DIR = os.path.join(STATS_DIR, "images") + +# Ensure directories exist +os.makedirs(STATS_DIR, exist_ok=True) +os.makedirs(IMAGE_DIR, exist_ok=True) + +print("Fetching file list...") +try: + response = requests.get(BASE_URL, timeout=10) + response.raise_for_status() + content = response.text + # Regex for UUID.json + files = re.findall(r'href="([0-9a-f-]{36}\.json)"', content) + files = list(set(files)) + print(f"Found {len(files)} player stats files.") +except Exception as e: + print(f"Error fetching file list: {e}") + files = [] + +def get_player_name(uuid): + # Try Ashcon first + try: + r = requests.get(f"https://api.ashcon.app/mojang/v2/user/{uuid}", timeout=5) + if r.status_code == 200: + return r.json().get('username') + except: + pass + + # Try Mojang Session + try: + r = requests.get(f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}", timeout=5) + if r.status_code == 200: + return r.json().get('name') + except: + pass + + return "Unknown" + +def process_player(filename): + uuid = filename.replace(".json", "") + json_path = os.path.join(STATS_DIR, filename) + # img_path = os.path.join(IMAGE_DIR, f"{uuid}.png") # No longer used + + print(f"Processing {uuid}...") + + # 1. Download/Load JSON + data = None + try: + # Check if we already have it locally and it's valid, maybe skip download? + # User implies fetching updates, so we download. + r = requests.get(BASE_URL + filename, timeout=10) + if r.status_code == 200: + data = r.json() + else: + print(f"Failed to download {filename}") + return None + except Exception as e: + print(f"Error downloading {filename}: {e}") + return None + + if not data: + return None + + # 2. Get Name + # We can check if name is already in the processing file to avoid API calls if scraping repeatedly? + # For this task, we assume we need to fetch it. + # To save API calls, we could check if we have a saved version with a name. + player_name = "Unknown" + + # Check if 'extra' exists in downloaded data (unlikely if strictly from server) + # But checking if we have a local cache of this file with a name is smart + if os.path.exists(json_path): + try: + with open(json_path, 'r', encoding='utf-8') as f: + local_data = json.load(f) + if 'extra' in local_data and local_data['extra'].get('player_name') != "Unknown": + player_name = local_data['extra']['player_name'] + except: + pass + + if player_name == "Unknown": + player_name = get_player_name(uuid) + # Sleep slightly to be nice to APIs if meaningful massive parallel + time.sleep(0.1) + + # 3. Download Avatar - SKIPPED to avoid rate limits + # The frontend will handle dynamic loading of avatars using Minotar/Crafatar URLs. + + # 4. Calculate Stats + stats = data.get('stats', {}) + + # Walk + # Handle both modern ':' and potentially flattened or different versions if necessary, + # but usually proper JSON has "minecraft:custom" + # "minecraft:walk_one_cm" + + custom = stats.get('minecraft:custom', {}) + walk_cm = custom.get('minecraft:walk_one_cm', 0) + + def format_dist(cm): + m = cm / 100 + if m < 1000: + return f"{m:.1f} m" + else: + return f"{m/1000:.2f} km" + + walk_fmt = format_dist(walk_cm) + + # Mined + mined = stats.get('minecraft:mined', {}) + total_mined = sum(mined.values()) + + # Placed (Used) + used = stats.get('minecraft:used', {}) + total_placed = sum(used.values()) + + # Deaths (Killed By) + killed_by = stats.get('minecraft:killed_by', {}) + total_deaths = sum(killed_by.values()) + + # Inject into JSON + data['extra'] = { + 'player_name': player_name, + 'formatted_walk': walk_fmt, + 'walk_cm': walk_cm, + 'total_mined': total_mined, + 'total_placed': total_placed, + 'total_deaths': total_deaths + } + + # Save + with open(json_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + return { + 'uuid': uuid, + 'name': player_name, + 'avatar': f"https://minotar.net/avatar/{player_name}/64" if player_name != "Unknown" else f"https://minotar.net/avatar/{uuid}/64", + 'stats': { + 'walk_fmt': walk_fmt, + 'walk_raw': walk_cm, + 'mined': total_mined, + 'placed': total_placed, + 'deaths': total_deaths + } + } + +# Process in parallel +# Reduce max_workers to avoid hitting API limits too hard locally +results = [] +with ThreadPoolExecutor(max_workers=4) as executor: + # Process only first 50 for testing? No, user wants all. + # But for a script I am writing for them, I should let them run it. + # I will process ALL found files. + results = list(executor.map(process_player, files)) + +results = [r for r in results if r is not None] + +# Sort by name perhaps? Or just raw list. +results.sort(key=lambda x: x['name']) + +summary = { + 'updated_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + 'players': results +} + +with open(os.path.join(STATS_DIR, 'summary.json'), 'w', encoding='utf-8') as f: + json.dump(summary, f, ensure_ascii=False, indent=4) + +print("Processing complete. Summary saved to stats/summary.json") diff --git a/style.css b/style.css new file mode 100644 index 0000000..3f753b4 --- /dev/null +++ b/style.css @@ -0,0 +1,707 @@ +:root { + --bg-color: #f5f5f7; + --card-bg: #ffffff; + --text-primary: #1d1d1f; + --text-secondary: #86868b; + --accent-color: #0071e3; /* Apple Blue, but we might want Green */ + --brand-green: #34c759; /* Apple Green */ + --radius-large: 30px; + --radius-medium: 20px; + --transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Inter", "Noto Sans SC", -apple-system, BlinkMacSystemFont, sans-serif; + background-color: var(--bg-color); + color: var(--text-primary); + -webkit-font-smoothing: antialiased; +} + +/* Navbar */ +.navbar { + position: fixed; + top: 0; + width: 100%; + height: 44px; + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + z-index: 1000; + border-bottom: 1px solid rgba(0,0,0,0.05); + display: flex; + justify-content: center; +} + +.nav-content { + width: 100%; + max-width: 1000px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 20px; + font-size: 12px; +} + +.logo img { + height: 32px; + width: auto; + display: block; +} + +.nav-links a { + text-decoration: none; + color: var(--text-primary); + margin-left: 24px; + opacity: 0.8; + transition: opacity 0.2s; +} + +.nav-links a:hover { + opacity: 1; +} + +.nav-cta { + background: #0071e3; + color: white !important; + padding: 6px 14px; + border-radius: 980px; + font-size: 12px; + font-weight: 500; + opacity: 1 !important; + transition: all 0.2s; +} + +.nav-cta:hover { + background: #0077ed; + transform: scale(1.05); +} + +/* Hero */ +.hero { + height: 80vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + padding-top: 44px; /* Navbar height */ + background-color: #000; /* Fallback */ + background-size: cover; + background-position: center; + position: relative; + color: white; +} + +.hero-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.4); /* Dark overlay for text readability */ + z-index: 1; +} + +.hero-content { + position: relative; + z-index: 2; +} + +.hero-title { + font-size: 56px; + line-height: 1.07143; + font-weight: 700; + letter-spacing: -0.005em; + margin-bottom: 10px; + color: white; + text-shadow: 0 2px 10px rgba(0,0,0,0.3); + /* Remove gradient text for image background */ + background: none; + -webkit-text-fill-color: white; +} + +.hero-subtitle { + font-size: 28px; + line-height: 1.10722; + font-weight: 400; + letter-spacing: .004em; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 15px; + text-shadow: 0 2px 5px rgba(0,0,0,0.3); +} + +.server-runtime { + font-size: 18px; + color: rgba(255, 255, 255, 0.85); + margin-bottom: 40px; + font-weight: 500; + text-shadow: 0 1px 3px rgba(0,0,0,0.3); +} + +.hero-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.server-ip-box { + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 12px 24px; + border-radius: 980px; + font-size: 17px; + font-weight: 400; + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + gap: 8px; + position: relative; +} + +.server-ip-box:hover { + background: rgba(255, 255, 255, 0.3); + transform: scale(1.02); +} + +.ip-hint { + font-size: 12px; + color: rgba(255, 255, 255, 0.7); +} + +.tooltip { + position: absolute; + top: -30px; + background: #333; + color: #fff; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + opacity: 0; + transition: opacity 0.2s; +} +.tooltip.show { opacity: 1; } + +/* Online Status */ +.online-status-box { + margin-top: 15px; + position: relative; + cursor: default; +} + +.status-indicator { + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(10px); + padding: 8px 16px; + border-radius: 20px; + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; + transition: var(--transition); +} + +.status-dot { + width: 8px; + height: 8px; + background-color: #34c759; /* Green */ + border-radius: 50%; + box-shadow: 0 0 8px rgba(52, 199, 89, 0.6); +} + +.status-dot.offline { + background-color: #ff3b30; /* Red */ + box-shadow: 0 0 8px rgba(255, 59, 48, 0.6); +} + +.players-tooltip { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + margin-top: 10px; + background: rgba(255, 255, 255, 0.95); + color: #1d1d1f; + padding: 10px; + border-radius: 12px; + width: 200px; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; + z-index: 10; + text-align: left; +} + +.online-status-box:hover .players-tooltip { + opacity: 1; + visibility: visible; + margin-top: 15px; +} + +.players-tooltip::before { + content: ''; + position: absolute; + top: -6px; + left: 50%; + transform: translateX(-50%); + border-width: 0 6px 6px 6px; + border-style: solid; + border-color: transparent transparent rgba(255, 255, 255, 0.95) transparent; +} + +.player-item { + padding: 6px 8px; + font-size: 13px; + border-bottom: 1px solid rgba(0,0,0,0.05); + display: flex; + align-items: center; + gap: 8px; +} + +.player-item:last-child { + border-bottom: none; +} + +.player-avatar { + width: 16px; + height: 16px; + border-radius: 2px; +} + +/* Features Section */ +.features-section { + padding: 100px 0; + background: var(--bg-color); +} + +.container { + max-width: 1000px; + margin: 0 auto; + padding: 0 20px; +} + +.section-header { + font-size: 40px; + font-weight: 700; + text-align: center; + margin-bottom: 60px; +} + +/* Bento Grid */ +.bento-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-auto-rows: 180px; + gap: 20px; +} + +.bento-item { + background: var(--card-bg); + border-radius: var(--radius-large); + padding: 30px; + display: flex; + flex-direction: column; + justify-content: flex-end; /* Align content to bottom for image cards */ + align-items: flex-start; /* Align left */ + text-align: left; + transition: var(--transition); + box-shadow: 2px 4px 12px rgba(0,0,0,0.02); + overflow: hidden; + position: relative; + background-size: cover; + background-position: center; +} + +.bento-item:hover { + transform: scale(1.02); + box-shadow: 2px 8px 24px rgba(0,0,0,0.06); +} + +/* Overlay for Bento Items with Images */ +.bento-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0.6) 100%); + z-index: 1; +} + +.bento-content { + position: relative; + z-index: 2; + width: 100%; +} + +/* Grid Spans */ +.large-item { + grid-column: span 2; + grid-row: span 2; +} + +.medium-item { + grid-column: span 2; + grid-row: span 1; +} + +.small-item { + grid-column: span 1; + grid-row: span 1; +} + +/* Hardware Card (No Image) */ +.hardware-card { + background: linear-gradient(135deg, #2c3e50 0%, #000000 100%); + justify-content: center; + align-items: center; + text-align: center; + position: relative; +} + +/* Add a subtle tech grid pattern overlay */ +.hardware-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px); + background-size: 20px 20px; + z-index: 1; +} + +.hardware-card .bento-content { + color: white; + z-index: 2; +} + +.hardware-card .icon { + color: white; + font-size: 32px; + margin-bottom: 10px; +} + +.hardware-card h4 { + color: white; + font-size: 17px; + margin: 10px 0 5px; +} + +.hardware-card p { + color: rgba(255, 255, 255, 0.8); + font-size: 13px; +} + +/* Content Styling for Image Cards */ +.bento-item:not(.hardware-card) .icon { + color: white; + font-size: 32px; + margin-bottom: 10px; +} + +.bento-item:not(.hardware-card) h3, +.bento-item:not(.hardware-card) h4 { + color: white; + text-shadow: 0 2px 4px rgba(0,0,0,0.3); +} + +.bento-item:not(.hardware-card) p { + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + text-shadow: 0 1px 2px rgba(0,0,0,0.3); +} + +.bento-item h3 { + font-size: 24px; + margin-bottom: 10px; +} + +.small-item h4 { + font-size: 17px; + margin: 10px 0 5px; +} + +.small-item p { + font-size: 13px; +} + +/* Details Section */ +.details-section { + padding: 100px 0; + background: #fff; +} + +.detail-row { + margin-bottom: 80px; + text-align: center; +} + +.detail-text h3 { + font-size: 32px; + margin-bottom: 15px; +} + +.detail-text p { + font-size: 19px; + color: var(--text-secondary); + max-width: 600px; + margin: 0 auto; +} + +/* Footer */ +footer { + background: var(--bg-color); + padding: 40px 0; + border-top: 1px solid #e5e5e5; + font-size: 12px; + color: var(--text-secondary); +} + +/* Responsive */ +@media (max-width: 900px) { + .bento-grid { + grid-template-columns: 1fr; + grid-auto-rows: auto; + } + + .large-item, .medium-item, .small-item { + grid-column: span 1; + grid-row: auto; + min-height: 250px; + } + + .hero-title { + font-size: 40px; + } +} + +/* Sponsors Section */ +.sponsors-section { + padding: 80px 0; + background: #fff; + text-align: center; +} + +.top-sponsors-grid { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 40px; + flex-wrap: wrap; +} + +.sponsor-card { + background: var(--bg-color); + border-radius: var(--radius-medium); + padding: 30px; + width: 250px; + box-shadow: 0 4px 12px rgba(0,0,0,0.05); + transition: var(--transition); + display: flex; + flex-direction: column; + align-items: center; +} + +.sponsor-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 24px rgba(0,0,0,0.1); +} + +.sponsor-rank { + font-size: 48px; + margin-bottom: 10px; +} + +.sponsor-name { + font-size: 20px; + font-weight: 600; + margin-bottom: 5px; + color: var(--text-primary); +} + +.sponsor-amount { + font-size: 16px; + color: var(--accent-color); + font-weight: 500; +} + +.view-sponsors-btn { + background: var(--text-primary); + color: white; + border: none; + padding: 12px 30px; + border-radius: 980px; + font-size: 16px; + cursor: pointer; + transition: var(--transition); +} + +.view-sponsors-btn:hover { + background: #000; + transform: scale(1.05); +} + +/* Crowdfunding Section */ +.crowdfunding-section { + padding: 80px 0; + background: var(--bg-color); /* Or white, depending on contrast preference */ +} + +.crowdfunding-grid { + display: flex; + flex-direction: column; + gap: 30px; + max-width: 800px; + margin: 0 auto; +} + +.fund-card { + background: #fff; + border-radius: var(--radius-medium); + padding: 30px; + box-shadow: 0 4px 12px rgba(0,0,0,0.05); + transition: var(--transition); +} + +.fund-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 24px rgba(0,0,0,0.1); +} + +.fund-header { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 15px; +} + +.fund-title { + font-size: 20px; + font-weight: 600; + color: var(--text-primary); +} + +.fund-stats { + font-size: 14px; + color: var(--text-secondary); +} + +.fund-stats span { + font-weight: 600; + color: var(--text-primary); +} + +.progress-bar-bg { + width: 100%; + height: 12px; + background-color: #f0f0f0; + border-radius: 6px; + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + background: linear-gradient(90deg, #0071e3, #34c759); + border-radius: 6px; + width: 0%; /* Will be set by JS */ + transition: width 1s ease-out; +} + +.fund-percentage { + text-align: right; + font-size: 12px; + color: var(--text-secondary); + margin-top: 8px; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + backdrop-filter: blur(5px); + justify-content: center; + align-items: center; +} + +.modal-content { + background-color: #fff; + padding: 40px; + border-radius: var(--radius-large); + width: 90%; + max-width: 800px; + max-height: 80vh; + position: relative; + display: flex; + flex-direction: column; +} + +.close-modal { + position: absolute; + top: 20px; + right: 30px; + color: #aaa; + font-size: 28px; + font-weight: bold; + cursor: pointer; + transition: 0.2s; +} + +.close-modal:hover { + color: #000; +} + +.modal-content h2 { + margin-bottom: 20px; + text-align: center; +} + +.sponsors-list-container { + overflow-y: auto; + flex-grow: 1; +} + +#sponsors-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +#sponsors-table th, #sponsors-table td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #eee; +} + +#sponsors-table th { + background-color: #f9f9f9; + font-weight: 600; + position: sticky; + top: 0; +} + +#sponsors-table tr:hover { + background-color: #f5f5f7; +}