diff --git a/static/script.js b/static/script.js
index 5a95bab0..8eb14fba 100644
--- a/static/script.js
+++ b/static/script.js
@@ -930,10 +930,32 @@ async function pollLive() {
}
}
+async function pollTotalRares() {
+ try {
+ const response = await fetch(`${API_BASE}/total-rares/`);
+ const data = await response.json();
+ updateTotalRaresDisplay(data);
+ } catch (e) {
+ console.error('Total rares fetch failed:', e);
+ }
+}
+
+function updateTotalRaresDisplay(data) {
+ const countElement = document.getElementById('totalRaresCount');
+ if (countElement && data.all_time !== undefined && data.today !== undefined) {
+ const allTimeFormatted = data.all_time.toLocaleString();
+ const todayFormatted = data.today.toLocaleString();
+ countElement.textContent = `${allTimeFormatted} (Today: ${todayFormatted})`;
+ }
+}
+
function startPolling() {
if (pollID !== null) return;
pollLive();
+ pollTotalRares(); // Initial fetch
pollID = setInterval(pollLive, POLL_MS);
+ // Poll total rares every 5 minutes (300,000 ms)
+ setInterval(pollTotalRares, 300000);
}
img.onload = () => {
@@ -968,6 +990,43 @@ function render(players) {
dots.innerHTML = '';
list.innerHTML = '';
+ // Update header with active player count
+ const header = document.getElementById('activePlayersHeader');
+ if (header) {
+ header.textContent = `Active Mosswart Enjoyers (${players.length})`;
+ }
+
+ // Calculate and update server KPH
+ const totalKPH = players.reduce((sum, p) => sum + (p.kills_per_hour || 0), 0);
+ const kphElement = document.getElementById('serverKphCount');
+ if (kphElement) {
+ // Format with commas and one decimal place for EPIC display
+ const formattedKPH = totalKPH.toLocaleString('en-US', {
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1
+ });
+ kphElement.textContent = formattedKPH;
+
+ // Add extra epic effect for high KPH
+ const container = document.getElementById('serverKphCounter');
+ if (container) {
+ if (totalKPH > 5000) {
+ container.classList.add('ultra-epic');
+ } else {
+ container.classList.remove('ultra-epic');
+ }
+ }
+ }
+
+ // Calculate and update total kills
+ const totalKills = players.reduce((sum, p) => sum + (p.total_kills || 0), 0);
+ const killsElement = document.getElementById('totalKillsCount');
+ if (killsElement) {
+ // Format with commas for readability
+ const formattedKills = totalKills.toLocaleString();
+ killsElement.textContent = formattedKills;
+ }
+
players.forEach(p => {
const { x, y } = worldToPx(p.ew, p.ns);
@@ -1002,7 +1061,8 @@ function render(players) {
const kpr = totalRares > 0 ? Math.round(totalKills / totalRares) : '∞';
li.innerHTML = `
-
${p.character_name} ${loc(p.ns, p.ew)}
+
${p.character_name}${createVitaeIndicator(p.character_name)} ${loc(p.ns, p.ew)}
+ ${createVitalsHTML(p.character_name)}
${p.kills}
${p.total_kills || 0}
${p.kills_per_hour}
@@ -1110,6 +1170,10 @@ function initWebSocket() {
try { msg = JSON.parse(evt.data); } catch { return; }
if (msg.type === 'chat') {
appendChatMessage(msg);
+ } else if (msg.type === 'vitals') {
+ updateVitalsDisplay(msg);
+ } else if (msg.type === 'rare') {
+ triggerEpicRareNotification(msg.character_name, msg.name);
}
});
socket.addEventListener('close', () => setTimeout(initWebSocket, 2000));
@@ -1276,3 +1340,310 @@ wrap.addEventListener('mousemove', e => {
wrap.addEventListener('mouseleave', () => {
coordinates.style.display = 'none';
});
+
+/* ---------- vitals display functions ----------------------------- */
+// Store vitals data per character
+const characterVitals = {};
+
+function updateVitalsDisplay(vitalsMsg) {
+ // Store the vitals data for this character
+ characterVitals[vitalsMsg.character_name] = {
+ health_percentage: vitalsMsg.health_percentage,
+ stamina_percentage: vitalsMsg.stamina_percentage,
+ mana_percentage: vitalsMsg.mana_percentage,
+ vitae: vitalsMsg.vitae
+ };
+
+ // Re-render the player list to update vitals in the UI
+ renderList();
+}
+
+function createVitalsHTML(characterName) {
+ const vitals = characterVitals[characterName];
+ if (!vitals) {
+ return ''; // No vitals data available
+ }
+
+ return `
+
+ `;
+}
+
+function createVitaeIndicator(characterName) {
+ const vitals = characterVitals[characterName];
+ if (!vitals || !vitals.vitae || vitals.vitae >= 100) {
+ return ''; // No vitae penalty
+ }
+
+ return `
⚰️ ${vitals.vitae}%`;
+}
+
+function getVitalClass(percentage) {
+ if (percentage <= 25) {
+ return 'critical-vital';
+ } else if (percentage <= 50) {
+ return 'low-vital';
+ }
+ return '';
+}
+
+/* ---------- epic rare notification system ------------------------ */
+// Track previous rare count to detect increases
+let lastRareCount = 0;
+let notificationQueue = [];
+let isShowingNotification = false;
+
+function triggerEpicRareNotification(characterName, rareName) {
+ // Add to queue
+ notificationQueue.push({ characterName, rareName });
+
+ // Process queue if not already showing a notification
+ if (!isShowingNotification) {
+ processNotificationQueue();
+ }
+
+ // Trigger fireworks immediately
+ createFireworks();
+
+ // Highlight the player in the list
+ highlightRareFinder(characterName);
+}
+
+function processNotificationQueue() {
+ if (notificationQueue.length === 0) {
+ isShowingNotification = false;
+ return;
+ }
+
+ isShowingNotification = true;
+ const notification = notificationQueue.shift();
+
+ // Create notification element
+ const container = document.getElementById('rareNotifications');
+ const notifEl = document.createElement('div');
+ notifEl.className = 'rare-notification';
+ notifEl.innerHTML = `
+
🎆 LEGENDARY RARE! 🎆
+
${notification.rareName}
+
found by
+
⚔️ ${notification.characterName} ⚔️
+ `;
+
+ container.appendChild(notifEl);
+
+ // Remove notification after 6 seconds and process next
+ setTimeout(() => {
+ notifEl.style.animation = 'notification-slide-out 0.5s ease-in forwards';
+ setTimeout(() => {
+ notifEl.remove();
+ processNotificationQueue();
+ }, 500);
+ }, 6000);
+}
+
+// Add slide out animation
+const style = document.createElement('style');
+style.textContent = `
+ @keyframes notification-slide-out {
+ to {
+ transform: translateY(-100px);
+ opacity: 0;
+ }
+ }
+`;
+document.head.appendChild(style);
+
+function createFireworks() {
+ const container = document.getElementById('fireworksContainer');
+ const rareCounter = document.getElementById('totalRaresCounter');
+ const rect = rareCounter.getBoundingClientRect();
+
+ // Create 30 particles
+ const particleCount = 30;
+ const colors = ['particle-gold', 'particle-red', 'particle-orange', 'particle-purple', 'particle-blue'];
+
+ for (let i = 0; i < particleCount; i++) {
+ const particle = document.createElement('div');
+ particle.className = `firework-particle ${colors[Math.floor(Math.random() * colors.length)]}`;
+
+ // Start position at rare counter
+ particle.style.left = `${rect.left + rect.width / 2}px`;
+ particle.style.top = `${rect.top + rect.height / 2}px`;
+
+ // Random explosion direction
+ const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5;
+ const velocity = 100 + Math.random() * 200;
+ const dx = Math.cos(angle) * velocity;
+ const dy = Math.sin(angle) * velocity - 50; // Slight upward bias
+
+ // Create custom animation for this particle
+ const animName = `particle-${Date.now()}-${i}`;
+ const keyframes = `
+ @keyframes ${animName} {
+ 0% {
+ transform: translate(0, 0) scale(1);
+ opacity: 1;
+ }
+ 100% {
+ transform: translate(${dx}px, ${dy + 200}px) scale(0);
+ opacity: 0;
+ }
+ }
+ `;
+
+ const styleEl = document.createElement('style');
+ styleEl.textContent = keyframes;
+ document.head.appendChild(styleEl);
+
+ particle.style.animation = `${animName} 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards`;
+
+ container.appendChild(particle);
+
+ // Clean up particle and animation after completion
+ setTimeout(() => {
+ particle.remove();
+ styleEl.remove();
+ }, 2000);
+ }
+}
+
+function highlightRareFinder(characterName) {
+ // Find the player in the list
+ const playerItems = document.querySelectorAll('#playerList li');
+ playerItems.forEach(item => {
+ const nameSpan = item.querySelector('.player-name');
+ if (nameSpan && nameSpan.textContent.includes(characterName)) {
+ item.classList.add('rare-finder-glow');
+ // Remove glow after 5 seconds
+ setTimeout(() => {
+ item.classList.remove('rare-finder-glow');
+ }, 5000);
+ }
+ });
+}
+
+// Update total rares display to trigger fireworks on increase
+const originalUpdateTotalRaresDisplay = updateTotalRaresDisplay;
+updateTotalRaresDisplay = function(data) {
+ originalUpdateTotalRaresDisplay(data);
+
+ // Check if total increased
+ const newTotal = data.all_time || 0;
+ if (newTotal > lastRareCount && lastRareCount > 0) {
+ // Don't trigger on initial load
+ createFireworks();
+
+ // Check for milestones when count increases
+ if (newTotal > 0 && newTotal % 100 === 0) {
+ triggerMilestoneCelebration(newTotal);
+ }
+ }
+ lastRareCount = newTotal;
+}
+
+function triggerMilestoneCelebration(rareNumber) {
+ console.log(`🏆 MILESTONE: Rare #${rareNumber}! 🏆`);
+
+ // Create full-screen milestone overlay
+ const overlay = document.createElement('div');
+ overlay.className = 'milestone-overlay';
+
+ overlay.innerHTML = `
+
+
#${rareNumber}
+
🏆 EPIC MILESTONE! 🏆
+
Server Achievement Unlocked
+
+ `;
+
+ document.body.appendChild(overlay);
+
+ // Add screen shake effect
+ document.body.classList.add('screen-shake');
+
+ // Create massive firework explosion
+ createMilestoneFireworks();
+
+ // Remove milestone overlay after 5 seconds
+ setTimeout(() => {
+ overlay.style.animation = 'milestone-fade-in 0.5s ease-out reverse';
+ document.body.classList.remove('screen-shake');
+ setTimeout(() => {
+ overlay.remove();
+ }, 500);
+ }, 5000);
+}
+
+function createMilestoneFireworks() {
+ const container = document.getElementById('fireworksContainer');
+
+ // Create multiple bursts across the screen
+ const burstCount = 5;
+ const particlesPerBurst = 50;
+ const colors = ['#ffd700', '#ff6600', '#ff0044', '#cc00ff', '#00ccff', '#00ff00'];
+
+ for (let burst = 0; burst < burstCount; burst++) {
+ setTimeout(() => {
+ // Random position for each burst
+ const x = Math.random() * window.innerWidth;
+ const y = Math.random() * (window.innerHeight * 0.7) + (window.innerHeight * 0.15);
+
+ for (let i = 0; i < particlesPerBurst; i++) {
+ const particle = document.createElement('div');
+ particle.className = 'milestone-particle';
+ particle.style.background = colors[Math.floor(Math.random() * colors.length)];
+ particle.style.boxShadow = `0 0 12px ${particle.style.background}`;
+
+ // Start position
+ particle.style.left = `${x}px`;
+ particle.style.top = `${y}px`;
+
+ // Random explosion direction
+ const angle = (Math.PI * 2 * i) / particlesPerBurst + (Math.random() - 0.5) * 0.8;
+ const velocity = 200 + Math.random() * 300;
+ const dx = Math.cos(angle) * velocity;
+ const dy = Math.sin(angle) * velocity - 100; // Upward bias
+
+ // Create custom animation
+ const animName = `milestone-particle-${Date.now()}-${burst}-${i}`;
+ const keyframes = `
+ @keyframes ${animName} {
+ 0% {
+ transform: translate(0, 0) scale(1);
+ opacity: 1;
+ }
+ 100% {
+ transform: translate(${dx}px, ${dy + 400}px) scale(0);
+ opacity: 0;
+ }
+ }
+ `;
+
+ const styleEl = document.createElement('style');
+ styleEl.textContent = keyframes;
+ document.head.appendChild(styleEl);
+
+ particle.style.animation = `${animName} 3s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards`;
+
+ container.appendChild(particle);
+
+ // Clean up
+ setTimeout(() => {
+ particle.remove();
+ styleEl.remove();
+ }, 3000);
+ }
+ }, burst * 200); // Stagger bursts
+ }
+}
+
diff --git a/static/style.css b/static/style.css
index 1f3fc519..5de2c5c4 100644
--- a/static/style.css
+++ b/static/style.css
@@ -122,6 +122,104 @@ body {
font-size: 1.25rem;
color: var(--accent);
}
+
+.total-rares-counter {
+ margin: 0 0 12px 0;
+ padding: 8px 12px;
+ background: linear-gradient(135deg, #2a2a2a, #1a1a1a);
+ border: 1px solid #444;
+ border-radius: 6px;
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: #ffd700;
+ text-align: center;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.total-rares-counter #totalRaresCount {
+ color: #fff;
+ margin-left: 4px;
+}
+
+.server-kph-counter {
+ margin: 0 0 12px 0;
+ padding: 9px 12px;
+ background: linear-gradient(135deg, #2a2a44, #1a1a33);
+ border: 2px solid #4466aa;
+ border-radius: 6px;
+ font-size: 1rem;
+ font-weight: 600;
+ color: #aaccff;
+ text-align: center;
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4);
+ position: relative;
+ animation: kph-border-glow 4s ease-in-out infinite;
+}
+
+@keyframes kph-border-glow {
+ 0%, 100% { border-color: #4466aa; box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4); }
+ 50% { border-color: #6688cc; box-shadow: 0 3px 12px rgba(102, 136, 204, 0.3); }
+}
+
+.server-kph-counter #serverKphCount {
+ color: #fff;
+ margin-left: 4px;
+ font-size: 1.1rem;
+ font-weight: 700;
+ text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
+ animation: kph-pulse 3s ease-in-out infinite;
+}
+
+@keyframes kph-pulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.02); }
+}
+
+/* ULTRA MODE for KPH > 5000 */
+.server-kph-counter.ultra-epic {
+ background: linear-gradient(135deg, #6644ff, #4422cc, #6644ff);
+ background-size: 200% 200%;
+ animation: kph-border-glow 4s ease-in-out infinite, ultra-background 3s ease-in-out infinite;
+ border-color: #8866ff;
+ color: #eeeeff;
+ box-shadow: 0 4px 12px rgba(102, 68, 255, 0.5);
+}
+
+@keyframes ultra-background {
+ 0% { background-position: 0% 50%; }
+ 50% { background-position: 100% 50%; }
+ 100% { background-position: 0% 50%; }
+}
+
+.server-kph-counter.ultra-epic #serverKphCount {
+ font-size: 1.3rem;
+ color: #ffffff;
+ text-shadow: 0 0 12px rgba(255, 255, 255, 0.7);
+ animation: kph-pulse 3s ease-in-out infinite, ultra-glow 2s ease-in-out infinite alternate;
+}
+
+@keyframes ultra-glow {
+ from { text-shadow: 0 0 12px rgba(255, 255, 255, 0.7); }
+ to { text-shadow: 0 0 18px rgba(255, 255, 255, 0.9), 0 0 25px rgba(136, 102, 255, 0.5); }
+}
+
+.total-kills-counter {
+ margin: 0 0 12px 0;
+ padding: 8px 12px;
+ background: linear-gradient(135deg, #2a2a2a, #1a1a1a);
+ border: 1px solid #555;
+ border-radius: 6px;
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: #ff6666;
+ text-align: center;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.total-kills-counter #totalKillsCount {
+ color: #fff;
+ margin-left: 4px;
+}
#playerList {
list-style: none;
margin: 0;
@@ -243,9 +341,10 @@ body {
#playerList li {
display: grid;
grid-template-columns: 1fr auto auto auto auto auto;
- grid-template-rows: auto auto auto auto;
+ grid-template-rows: auto auto auto auto auto;
grid-template-areas:
"name name name name name name"
+ "vitals vitals vitals vitals vitals vitals"
"kills totalkills kph kph kph kph"
"rares kpr meta meta meta meta"
"onlinetime deaths tapers tapers tapers tapers";
@@ -254,7 +353,7 @@ body {
padding: 8px 10px;
background: var(--card);
border-left: 4px solid transparent;
- transition: background 0.15s;
+ transition: none;
font-size: 0.85rem;
}
@@ -272,6 +371,8 @@ body {
.stat.deaths { grid-area: deaths; }
.stat.tapers { grid-area: tapers; }
+.player-vitals { grid-area: vitals; }
+
/* pill styling */
#playerList li .stat {
background: rgba(255,255,255,0.1);
@@ -719,3 +820,359 @@ body.noselect, body.noselect * {
margin-top: 4px;
text-align: center;
}
+
+/* ---------- inline vitals bars ---------------------------------- */
+.player-vitals {
+ grid-column: 1 / -1;
+ margin: 2px 0 4px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.vital-bar-inline {
+ height: 5px;
+ background: #222;
+ border-radius: 3px;
+ overflow: hidden;
+ position: relative;
+}
+
+.vitae-indicator {
+ font-size: 0.75rem;
+ color: #ff6666;
+ margin-left: 8px;
+ font-weight: 500;
+}
+
+.vital-fill {
+ height: 100%;
+ transition: width 0.3s ease-out;
+ border-radius: 2px;
+}
+
+.vital-fill.health {
+ background: linear-gradient(90deg, #ff4444, #ff6666);
+}
+
+.vital-fill.stamina {
+ background: linear-gradient(90deg, #ffaa00, #ffcc44);
+}
+
+.vital-fill.mana {
+ background: linear-gradient(90deg, #4488ff, #66aaff);
+}
+
+/* Pulsing effects for low vitals */
+.vital-bar-inline.low-vital {
+ animation: pulse-bar-low 2s ease-in-out infinite;
+}
+
+.vital-bar-inline.critical-vital {
+ animation: pulse-bar-critical 1s ease-in-out infinite;
+}
+
+@keyframes pulse-bar-low {
+ 0%, 100% { background: #222; }
+ 50% { background: #332200; }
+}
+
+@keyframes pulse-bar-critical {
+ 0%, 100% { background: #222; }
+ 50% { background: #440000; }
+}
+
+/* ---------- epic rare notifications ------------------------------ */
+.rare-notifications {
+ position: fixed;
+ top: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 10001;
+ pointer-events: none;
+}
+
+.rare-notification {
+ background: linear-gradient(135deg, #ffd700, #ffed4e, #ffd700);
+ border: 3px solid #ff6600;
+ border-radius: 12px;
+ padding: 20px 30px;
+ margin-bottom: 10px;
+ text-align: center;
+ box-shadow: 0 8px 32px rgba(255, 215, 0, 0.5);
+ animation: notification-slide-in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55),
+ epic-glow 2s ease-in-out infinite;
+ position: relative;
+ overflow: hidden;
+}
+
+@keyframes notification-slide-in {
+ from {
+ transform: translateY(-100px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes epic-glow {
+ 0%, 100% {
+ box-shadow: 0 8px 32px rgba(255, 215, 0, 0.5);
+ }
+ 50% {
+ box-shadow: 0 8px 48px rgba(255, 215, 0, 0.8);
+ }
+}
+
+.rare-notification-title {
+ font-size: 1.2rem;
+ font-weight: 800;
+ color: #ff0044;
+ text-transform: uppercase;
+ margin-bottom: 8px;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+ animation: epic-text-pulse 1s ease-in-out infinite;
+}
+
+@keyframes epic-text-pulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.05); }
+}
+
+.rare-notification-mob {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #1a0033;
+ margin-bottom: 4px;
+ text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.5);
+}
+
+.rare-notification-finder {
+ font-size: 1rem;
+ color: #333;
+ font-style: italic;
+ margin-bottom: 4px;
+}
+
+.rare-notification-character {
+ font-size: 1.3rem;
+ font-weight: 700;
+ color: #ff0044;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+/* Shine effect overlay */
+.rare-notification::before {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: linear-gradient(45deg,
+ transparent 30%,
+ rgba(255, 255, 255, 0.5) 50%,
+ transparent 70%
+ );
+ transform: rotate(45deg);
+ animation: notification-shine 3s infinite;
+}
+
+@keyframes notification-shine {
+ 0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
+ 100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
+}
+
+/* ---------- fireworks particles ---------------------------------- */
+.fireworks-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 9999;
+}
+
+.firework-particle {
+ position: absolute;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ pointer-events: none;
+ animation: firework-fly 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
+}
+
+@keyframes firework-fly {
+ 0% {
+ transform: translate(0, 0) scale(1);
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
+/* Different particle colors */
+.particle-gold { background: #ffd700; box-shadow: 0 0 6px #ffd700; }
+.particle-red { background: #ff4444; box-shadow: 0 0 6px #ff4444; }
+.particle-orange { background: #ff8800; box-shadow: 0 0 6px #ff8800; }
+.particle-purple { background: #cc00ff; box-shadow: 0 0 6px #cc00ff; }
+.particle-blue { background: #00ccff; box-shadow: 0 0 6px #00ccff; }
+
+/* Character glow effect in player list */
+.player-item.rare-finder-glow {
+ animation: rare-finder-highlight 5s ease-in-out;
+ border-left-color: #ffd700 !important;
+ border-left-width: 6px !important;
+}
+
+@keyframes rare-finder-highlight {
+ 0%, 100% {
+ background: var(--card);
+ box-shadow: none;
+ }
+ 50% {
+ background: rgba(255, 215, 0, 0.2);
+ box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
+ }
+}
+
+/* ---------- milestone celebration overlay ------------------------ */
+.milestone-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(ellipse at center, rgba(255, 215, 0, 0.3), rgba(0, 0, 0, 0.8));
+ z-index: 20000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: milestone-fade-in 0.5s ease-out;
+}
+
+@keyframes milestone-fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.milestone-content {
+ text-align: center;
+ animation: milestone-zoom 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+}
+
+@keyframes milestone-zoom {
+ from {
+ transform: scale(0);
+ }
+ to {
+ transform: scale(1);
+ }
+}
+
+.milestone-number {
+ font-size: 8rem;
+ font-weight: 900;
+ color: #ffd700;
+ text-shadow:
+ 0 0 30px #ffd700,
+ 0 0 60px #ff6600,
+ 0 0 90px #ff0044,
+ 0 0 120px #ff0044;
+ margin-bottom: 20px;
+ animation: milestone-pulse 1s ease-in-out infinite alternate;
+}
+
+@keyframes milestone-pulse {
+ from {
+ transform: scale(1);
+ text-shadow:
+ 0 0 30px #ffd700,
+ 0 0 60px #ff6600,
+ 0 0 90px #ff0044,
+ 0 0 120px #ff0044;
+ }
+ to {
+ transform: scale(1.1);
+ text-shadow:
+ 0 0 40px #ffd700,
+ 0 0 80px #ff6600,
+ 0 0 120px #ff0044,
+ 0 0 160px #ff0044;
+ }
+}
+
+.milestone-text {
+ font-size: 3rem;
+ font-weight: 700;
+ color: #fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ text-shadow: 0 0 20px rgba(255, 255, 255, 0.8);
+ animation: milestone-text-glow 2s ease-in-out infinite;
+}
+
+@keyframes milestone-text-glow {
+ 0%, 100% {
+ opacity: 0.8;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+.milestone-subtitle {
+ font-size: 1.5rem;
+ color: #ffcc00;
+ margin-top: 20px;
+ font-style: italic;
+ animation: milestone-subtitle-slide 1s ease-out;
+}
+
+@keyframes milestone-subtitle-slide {
+ from {
+ transform: translateY(50px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Milestone firework burst - larger particles */
+.milestone-particle {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ pointer-events: none;
+ background: #ffd700;
+ box-shadow: 0 0 12px #ffd700;
+}
+
+/* Screen shake effect */
+@keyframes screen-shake {
+ 0%, 100% { transform: translate(0, 0); }
+ 10% { transform: translate(-5px, -5px); }
+ 20% { transform: translate(5px, -5px); }
+ 30% { transform: translate(-5px, 5px); }
+ 40% { transform: translate(5px, 5px); }
+ 50% { transform: translate(-3px, -3px); }
+ 60% { transform: translate(3px, -3px); }
+ 70% { transform: translate(-3px, 3px); }
+ 80% { transform: translate(3px, 3px); }
+ 90% { transform: translate(-1px, -1px); }
+}
+
+.screen-shake {
+ animation: screen-shake 0.5s ease-in-out;
+}