diff --git a/.circleci/config.yml b/.circleci/config.yml
index a4fab1541..9e69c5919 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -226,6 +226,7 @@ workflows:
branches:
only:
- dev
+ - mm-final-2025-reveal
- deployQa:
context: org-global
diff --git a/public/mm-final-2025-reveal/index.html b/public/mm-final-2025-reveal/index.html
new file mode 100644
index 000000000..fcd9a0112
--- /dev/null
+++ b/public/mm-final-2025-reveal/index.html
@@ -0,0 +1,279 @@
+
+
+
+
+
+
+
+
+
+
+ The Marathon Match
+ Tournament Final
+ Has Concluded!
+
+
After 24 hours of intense competition, the champions have emerged.
+
+
+
+
+
+
+
+
+
+
+
๐งฉ
+
Grid Puzzle Game
+
+
+ Finalists faced a challenging tile-matching puzzle on an NรN grid.
+ Each tile has 4 numbers (one per edge), and the goal is to place tiles so that
+ adjacent edges match. When two edges match with value k , you score
+ kยณ points. With limited discards and a hidden tile queue, competitors
+ had to balance strategy, timing, and optimization to maximize their score.
+
+
+
+
+
Example Solution Animation
+
+
+
+
+
๐ฏ
+
+
Objective
+
Place tiles to match edges and maximize score
+
+
+
+
โก
+
+
Scoring
+
Matching edges with value k earns kยณ points
+
+
+
+
+
๐ฒ
+
+
Complexity
+
Grid size 4-16, hand size 1-N, up to 20 edge values
+
+
+
+
+
Problem Setter: dimkadimon
+
Problem Testers: JacoCronje and nika
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RANK
+ MEMBER
+ SCORE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
And the Champions Are...
+
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
Congratulations to All Finalists!
+
Your dedication, skill, and strategic thinking made this tournament unforgettable.
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/mm-final-2025-reveal/photos/Acarreo.png b/public/mm-final-2025-reveal/photos/Acarreo.png
new file mode 100644
index 000000000..c5d77f365
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/Acarreo.png differ
diff --git a/public/mm-final-2025-reveal/photos/Daiver19.png b/public/mm-final-2025-reveal/photos/Daiver19.png
new file mode 100644
index 000000000..88b8f2d45
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/Daiver19.png differ
diff --git a/public/mm-final-2025-reveal/photos/eulerscheZahl.png b/public/mm-final-2025-reveal/photos/eulerscheZahl.png
new file mode 100644
index 000000000..8ebf44b60
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/eulerscheZahl.png differ
diff --git a/public/mm-final-2025-reveal/photos/frictionless.png b/public/mm-final-2025-reveal/photos/frictionless.png
new file mode 100644
index 000000000..6ff8f2a78
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/frictionless.png differ
diff --git a/public/mm-final-2025-reveal/photos/gaha.png b/public/mm-final-2025-reveal/photos/gaha.png
new file mode 100644
index 000000000..26bb7e14f
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/gaha.png differ
diff --git a/public/mm-final-2025-reveal/photos/kovi.png b/public/mm-final-2025-reveal/photos/kovi.png
new file mode 100644
index 000000000..397a173a1
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/kovi.png differ
diff --git a/public/mm-final-2025-reveal/photos/krismaz.png b/public/mm-final-2025-reveal/photos/krismaz.png
new file mode 100644
index 000000000..fdfb36558
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/krismaz.png differ
diff --git a/public/mm-final-2025-reveal/photos/marwar22.png b/public/mm-final-2025-reveal/photos/marwar22.png
new file mode 100644
index 000000000..f0fd5ffbd
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/marwar22.png differ
diff --git a/public/mm-final-2025-reveal/photos/sullyper.png b/public/mm-final-2025-reveal/photos/sullyper.png
new file mode 100644
index 000000000..73dd74cbc
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/sullyper.png differ
diff --git a/public/mm-final-2025-reveal/photos/therealbeef.png b/public/mm-final-2025-reveal/photos/therealbeef.png
new file mode 100644
index 000000000..6eb08b445
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/therealbeef.png differ
diff --git a/public/mm-final-2025-reveal/photos/wleite.png b/public/mm-final-2025-reveal/photos/wleite.png
new file mode 100644
index 000000000..dec57aa2f
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/wleite.png differ
diff --git a/public/mm-final-2025-reveal/photos/xIlledanx.png b/public/mm-final-2025-reveal/photos/xIlledanx.png
new file mode 100644
index 000000000..641103a0b
Binary files /dev/null and b/public/mm-final-2025-reveal/photos/xIlledanx.png differ
diff --git a/public/mm-final-2025-reveal/script.js b/public/mm-final-2025-reveal/script.js
new file mode 100644
index 000000000..c96865b2e
--- /dev/null
+++ b/public/mm-final-2025-reveal/script.js
@@ -0,0 +1,688 @@
+// Configuration - Actual tournament data with real names, countries, and local photos
+const CONFIG = {
+ winners: {
+ first: {
+ name: "wleite",
+ fullName: "Wladimir Leite",
+ country: "Brazil",
+ countryCode: "BR",
+ score: "96.50406762453507",
+ photo: "photos/wleite.png"
+ },
+ second: {
+ name: "Daiver19",
+ fullName: "Dmytro Kovalenko",
+ country: "United States",
+ countryCode: "US",
+ score: "96.28012606883388",
+ photo: "photos/Daiver19.png"
+ },
+ third: {
+ name: "sullyper",
+ fullName: "Benjamin Butin",
+ country: "United States",
+ countryCode: "US",
+ score: "95.11713424850961",
+ photo: "photos/sullyper.png"
+ }
+ },
+ finalists: [
+ { name: "wleite", fullName: "Wladimir Leite", country: "Brazil", countryCode: "BR", photo: "photos/wleite.png" },
+ { name: "Daiver19", fullName: "Dmytro Kovalenko", country: "United States", countryCode: "US", photo: "photos/Daiver19.png" },
+ { name: "sullyper", fullName: "Benjamin Butin", country: "United States", countryCode: "US", photo: "photos/sullyper.png" },
+ { name: "gaha", fullName: "Szymon Mikler", country: "Poland", countryCode: "PL", photo: "photos/gaha.png" },
+ { name: "xllledanx", fullName: "Erik Kvanli", country: "Norway", countryCode: "NO", photo: "photos/xIlledanx.png" },
+ { name: "frictionless", fullName: "Jeremy Sawicki", country: "United States", countryCode: "US", photo: "photos/frictionless.png" },
+ { name: "Acarreo", fullName: "Alvaro Carrera", country: "Argentina", countryCode: "AR", photo: "photos/Acarreo.png" },
+ { name: "eulerscheZahl", fullName: "Ralph Pulletz", country: "Germany", countryCode: "DE", photo: "photos/eulerscheZahl.png" },
+ { name: "kovi", fullName: "Laszlo Kovacs", country: "Hungary", countryCode: "HU", photo: "photos/kovi.png" },
+ { name: "marwar22", fullName: "Marcin Wrรณbel", country: "Poland", countryCode: "PL", photo: "photos/marwar22.png" },
+ { name: "krismaz", fullName: "Krzysztof Maziarz", country: "Poland", countryCode: "PL", photo: "photos/krismaz.png" },
+ { name: "therealbeef", fullName: "Jan Nunnink", country: "South Korea", countryCode: "KR", photo: "photos/therealbeef.png" }
+ ],
+ provisionalScores: [
+ { name: "Daiver19", score: 95.92760634916554, submissionId: "provisional" },
+ { name: "wleite", score: 95.8428963143793, submissionId: "provisional" },
+ { name: "sullyper", score: 94.27375548042926, submissionId: "provisional" },
+ { name: "gaha", score: 93.60821110159368, submissionId: "provisional" },
+ { name: "xllledanx", score: 93.23566314334215, submissionId: "provisional" },
+ { name: "eulerscheZahl", score: 92.99606424429436, submissionId: "provisional" },
+ { name: "marwar22", score: 92.9930951680365, submissionId: "provisional" },
+ { name: "frictionless", score: 92.4923426686158, submissionId: "provisional" },
+ { name: "Acarreo", score: 92.45887119436483, submissionId: "provisional" },
+ { name: "kovi", score: 91.64713287113597, submissionId: "provisional" },
+ { name: "therealbeef", score: 88.62352311257933, submissionId: "provisional" },
+ { name: "krismaz", score: 88.59439182080432, submissionId: "provisional" }
+ ],
+ finalScores: [
+ { name: "wleite", score: 96.50406762453507, submissionId: "CERfxZpF4SIRNX" },
+ { name: "Daiver19", score: 96.28012606883388, submissionId: "EINAK7cSSO_7xX" },
+ { name: "sullyper", score: 95.11713424850961, submissionId: "uFx3lGhq4d2mHC" },
+ { name: "gaha", score: 93.61257767955452, submissionId: "CyFKBljw8duopJ" },
+ { name: "xllledanx", score: 93.55553699719715, submissionId: "nt_MP5KiE1wAJL" },
+ { name: "frictionless", score: 93.46929839889309, submissionId: "El9FikEjInD7J-" },
+ { name: "Acarreo", score: 93.28178845167851, submissionId: "LQZS_4pZB9jluJ" },
+ { name: "eulerscheZahl", score: 93.11112488260213, submissionId: "C2h6ObvYcTwsFM" },
+ { name: "kovi", score: 92.59779214174542, submissionId: "rafMzgvB9CIMIm" },
+ { name: "marwar22", score: 92.3025036061689, submissionId: "AsCoTAGKQIG8YB" },
+ { name: "krismaz", score: 89.4048802902513, submissionId: "F-8sneXwKKth6c" },
+ { name: "therealbeef", score: 88.70712272070001, submissionId: "NnFvq2ulEEOlyW" }
+ ],
+ timing: {
+ intro: 3000,
+ hero: 4000,
+ problem: 5000,
+ finalists: 8000,
+ leaderboardProvisional: 6000,
+ leaderboardTransition: 3000,
+ leaderboardFinal: 40000, // Increased to allow slower reveal cadence
+ countdown: 4000,
+ podium: 6000,
+ cta: 5000
+ }
+};
+
+// Simple presentation controller
+let currentStep = 0;
+const steps = [
+ { id: 'section-hero', duration: CONFIG.timing.hero },
+ // { id: 'section-problem', duration: CONFIG.timing.problem }, // Removed - skipping The Challenge page
+ { id: 'section-finalists', duration: CONFIG.timing.finalists },
+ { id: 'section-leaderboard', duration: CONFIG.timing.leaderboardProvisional + CONFIG.timing.leaderboardTransition + CONFIG.timing.leaderboardFinal },
+ { id: 'section-countdown', duration: CONFIG.timing.countdown },
+ { id: 'section-podium', duration: CONFIG.timing.podium }
+];
+
+// Initialize on page load
+document.addEventListener('DOMContentLoaded', () => {
+ initializePage();
+});
+
+function initializePage() {
+ setTimeout(() => {
+ fadeOutIntro();
+ }, CONFIG.timing.intro);
+}
+
+function fadeOutIntro() {
+ const introScreen = document.getElementById('intro-screen');
+ const mainContent = document.getElementById('main-content');
+
+ introScreen.classList.add('fade-out');
+
+ setTimeout(() => {
+ introScreen.classList.add('hidden');
+ mainContent.classList.remove('hidden');
+
+ // Initialize data
+ populateFinalists();
+ populateLeaderboard(CONFIG.provisionalScores, false);
+ initPuzzleAnimation();
+
+ // Start presentation
+ startPresentation();
+ }, 500);
+}
+
+function startPresentation() {
+ // Hide all sections
+ steps.forEach(step => {
+ const section = document.getElementById(step.id);
+ if (section) {
+ section.style.display = 'none';
+ section.classList.remove('active');
+ }
+ });
+
+ // Show first section
+ showStep(0);
+}
+
+function showStep(stepIndex) {
+ if (stepIndex >= steps.length) {
+ console.log('Presentation complete');
+ return;
+ }
+
+ const step = steps[stepIndex];
+ const section = document.getElementById(step.id);
+
+ if (!section) {
+ console.error(`Section ${step.id} not found, skipping`);
+ setTimeout(() => showStep(stepIndex + 1), 1000);
+ return;
+ }
+
+ // Hide previous section
+ if (stepIndex > 0) {
+ const prevSection = document.getElementById(steps[stepIndex - 1].id);
+ if (prevSection) {
+ prevSection.style.display = 'none';
+ prevSection.classList.remove('active');
+ }
+ }
+
+ // Show current section
+ section.style.display = 'flex';
+ section.classList.add('active');
+ section.style.opacity = '1';
+ section.style.visibility = 'visible';
+
+ // Handle step-specific logic
+ handleStepLogic(stepIndex);
+
+ // Advance to next step
+ setTimeout(() => {
+ showStep(stepIndex + 1);
+ }, step.duration);
+}
+
+function handleStepLogic(stepIndex) {
+ const step = steps[stepIndex];
+
+ switch (step.id) {
+ case 'section-leaderboard':
+ setTimeout(() => {
+ transitionToFinalScores();
+ }, CONFIG.timing.leaderboardProvisional);
+ break;
+ case 'section-countdown':
+ startCountdown();
+ break;
+ case 'section-podium':
+ populateWinners();
+ break;
+ }
+}
+
+function populateFinalists() {
+ const finalistsGrid = document.getElementById('finalists-grid');
+ if (!finalistsGrid) return;
+
+ finalistsGrid.innerHTML = '';
+
+ // Sort finalists alphabetically by handle (name)
+ const sortedFinalists = [...CONFIG.finalists].sort((a, b) => {
+ return (a.name || '').localeCompare(b.name || '', 'en', { sensitivity: 'base' });
+ });
+
+ sortedFinalists.forEach((finalist) => {
+ const card = document.createElement('div');
+ card.className = 'finalist-card';
+
+ // Load local image with fallback to initials
+ const imageUrl = finalist.photo || `photos/${finalist.name}.png`;
+ const initials = getInitials(finalist.fullName || finalist.name);
+ const flag = getCountryFlag(finalist.countryCode);
+
+ const avatarHTML = ` `;
+ const avatarPlaceholder = `${initials}
`;
+
+ card.innerHTML = `
+
+ ${avatarHTML}
+ ${avatarPlaceholder}
+
+ ${finalist.name}
+ ${finalist.fullName || ''}
+ ${flag} ${finalist.country}
+ `;
+
+ finalistsGrid.appendChild(card);
+ });
+
+ const totalFinalistsEl = document.getElementById('total-finalists');
+ if (totalFinalistsEl) {
+ totalFinalistsEl.textContent = CONFIG.finalists.length;
+ }
+}
+
+function getInitials(name) {
+ return name.split(' ').map(n => n[0]).join('').toUpperCase().substring(0, 2) || '?';
+}
+
+// Helper function to get country flag emoji from country code
+function getCountryFlag(countryCode) {
+ if (!countryCode) return '';
+ // Convert country code to flag emoji
+ const codePoints = countryCode
+ .toUpperCase()
+ .split('')
+ .map(char => 127397 + char.charCodeAt());
+ return String.fromCodePoint(...codePoints);
+}
+
+function populateLeaderboard(scores, isFinal) {
+ const tbody = document.getElementById('leaderboard-body');
+ if (!tbody) return;
+
+ tbody.innerHTML = '';
+
+ const sortedScores = [...scores].sort((a, b) => b.score - a.score);
+
+ sortedScores.forEach((entry, index) => {
+ const rank = index + 1;
+ const row = document.createElement('tr');
+
+ let rankClass = '';
+ if (rank === 1) rankClass = 'top-1';
+ else if (rank === 2) rankClass = 'top-2';
+ else if (rank === 3) rankClass = 'top-3';
+
+ // Find finalist info for country flag and photo
+ const finalistInfo = CONFIG.finalists.find(f => f.name === entry.name);
+ const flag = finalistInfo ? getCountryFlag(finalistInfo.countryCode) : '';
+ const photo = finalistInfo ? (finalistInfo.photo || `photos/${finalistInfo.name}.png`) : '';
+ const initials = finalistInfo ? getInitials(finalistInfo.fullName || finalistInfo.name) : entry.name.substring(0, 2).toUpperCase();
+
+ // Create avatar HTML
+ const avatarHTML = photo
+ ? ` `
+ : '';
+ const avatarPlaceholder = `${initials}
`;
+
+ row.innerHTML = `
+ ${rank}
+
+
+
+ ${avatarHTML}
+ ${avatarPlaceholder}
+
+
+
${entry.name}
+ ${finalistInfo ? `
${flag} ${finalistInfo.country}
` : ''}
+
+
+
+ ${entry.score.toFixed(14)}
+ `;
+
+ if (isFinal) {
+ const changeInfo = getRankChange(entry.name);
+ const changeCell = document.createElement('td');
+ changeCell.className = 'change-cell';
+
+ if (changeInfo.change === 'up') {
+ changeCell.innerHTML = `โ ${changeInfo.diff} `;
+ row.classList.add('rank-up');
+ } else if (changeInfo.change === 'down') {
+ changeCell.innerHTML = `โ ${changeInfo.diff} `;
+ row.classList.add('rank-down');
+ } else if (changeInfo.change === 'new') {
+ changeCell.innerHTML = `NEW `;
+ row.classList.add('rank-up');
+ } else {
+ changeCell.innerHTML = `โ `;
+ row.classList.add('rank-same');
+ }
+
+ row.appendChild(changeCell);
+ }
+
+ tbody.appendChild(row);
+ });
+}
+
+function getRankChange(name) {
+ const wasInProvisional = CONFIG.provisionalScores.some(entry => entry.name === name);
+
+ if (!wasInProvisional) {
+ return { change: 'new', diff: 0 };
+ }
+
+ const sortedProvisional = [...CONFIG.provisionalScores].sort((a, b) => b.score - a.score);
+ const provisionalRank = sortedProvisional.findIndex(entry => entry.name === name) + 1;
+
+ const sortedFinal = [...CONFIG.finalScores].sort((a, b) => b.score - a.score);
+ const finalRank = sortedFinal.findIndex(entry => entry.name === name) + 1;
+
+ const diff = provisionalRank - finalRank;
+
+ if (diff > 0) {
+ return { change: 'up', diff: Math.abs(diff) };
+ } else if (diff < 0) {
+ return { change: 'down', diff: Math.abs(diff) };
+ } else {
+ return { change: 'same', diff: 0 };
+ }
+}
+
+function transitionToFinalScores() {
+ const title = document.getElementById('leaderboard-title');
+ const subtitle = document.getElementById('leaderboard-subtitle');
+ const transitionIndicator = document.getElementById('transition-indicator');
+ const changeHeader = document.getElementById('change-header');
+ const tbody = document.getElementById('leaderboard-body');
+
+ if (!title || !subtitle || !transitionIndicator || !changeHeader || !tbody) return;
+
+ transitionIndicator.classList.remove('hidden');
+
+ setTimeout(() => {
+ title.textContent = 'FINAL SCORES';
+ subtitle.textContent = 'Official tournament results';
+ transitionIndicator.classList.add('hidden');
+ changeHeader.classList.remove('hidden');
+
+ // Fade out provisional scores
+ const rows = Array.from(tbody.querySelectorAll('tr'));
+ rows.forEach((row, index) => {
+ setTimeout(() => {
+ row.style.opacity = '0';
+ row.style.transform = 'translateX(-20px)';
+ row.style.transition = 'all 0.3s ease';
+ }, index * 50);
+ });
+
+ setTimeout(() => {
+ // Populate final scores but keep them hidden
+ populateLeaderboard(CONFIG.finalScores, true);
+
+ const newRows = Array.from(tbody.querySelectorAll('tr'));
+
+ // Hide all rows initially
+ newRows.forEach((row) => {
+ row.style.opacity = '0';
+ row.style.transform = 'translateY(30px) scale(0.95)';
+ row.style.transition = 'none';
+ });
+
+ // Reveal from last to first (reverse order)
+ // Use 2.5 seconds per placement (2500ms delay between each)
+ newRows.forEach((row, index) => {
+ // Rows are in order: index 0 = rank 1, index 1 = rank 2, etc.
+ // To reveal from last to first, reverse the index
+ const reverseIndex = newRows.length - 1 - index;
+ const actualRank = index + 1; // The actual rank of this row (index 0 = rank 1)
+ const delay = reverseIndex * 2500; // 2.5 seconds per placement
+
+ setTimeout(() => {
+ // Check if this is top 3 for special treatment
+ const isTopThree = actualRank <= 3;
+
+ if (isTopThree) {
+ // Special treatment for top 3: more dramatic reveal
+ row.style.transition = 'all 0.8s cubic-bezier(0.34, 1.56, 0.64, 1)'; // Bouncy animation
+ row.style.transform = 'translateY(0) scale(1.05)';
+ row.style.opacity = '1';
+
+ // Add glow effect for top 3
+ if (actualRank === 1) {
+ row.style.boxShadow = '0 0 30px rgba(255, 215, 0, 0.6)';
+ row.style.borderLeft = '4px solid var(--gold)';
+ } else if (actualRank === 2) {
+ row.style.boxShadow = '0 0 25px rgba(192, 192, 192, 0.5)';
+ row.style.borderLeft = '4px solid var(--silver)';
+ } else if (actualRank === 3) {
+ row.style.boxShadow = '0 0 25px rgba(205, 127, 50, 0.5)';
+ row.style.borderLeft = '4px solid var(--bronze)';
+ }
+
+ // Scale back to normal after bounce
+ setTimeout(() => {
+ row.style.transform = 'translateY(0) scale(1)';
+ if (actualRank === 1) {
+ row.style.boxShadow = '0 0 20px rgba(255, 215, 0, 0.4)';
+ } else if (actualRank === 2) {
+ row.style.boxShadow = '0 0 15px rgba(192, 192, 192, 0.3)';
+ } else if (actualRank === 3) {
+ row.style.boxShadow = '0 0 15px rgba(205, 127, 50, 0.3)';
+ }
+ }, 800);
+
+ // Highlight rank changes for top 3
+ if (row.classList.contains('rank-up') || row.classList.contains('rank-down')) {
+ setTimeout(() => {
+ row.classList.add('rank-change');
+ setTimeout(() => {
+ row.classList.remove('rank-change');
+ }, 1500);
+ }, 400);
+ }
+ } else {
+ // Regular reveal for other placements
+ row.style.transition = 'all 0.6s ease';
+ row.style.transform = 'translateY(0) scale(1)';
+ row.style.opacity = '1';
+
+ // Highlight rank changes
+ if (row.classList.contains('rank-up') || row.classList.contains('rank-down')) {
+ setTimeout(() => {
+ row.classList.add('rank-change');
+ setTimeout(() => {
+ row.classList.remove('rank-change');
+ }, 1000);
+ }, 300);
+ }
+ }
+ }, delay);
+ });
+ }, 500);
+ }, CONFIG.timing.leaderboardTransition);
+}
+
+function startCountdown() {
+ const countdownElement = document.getElementById('countdown');
+ if (!countdownElement) return;
+
+ let count = 3;
+
+ const countdownInterval = setInterval(() => {
+ countdownElement.textContent = count;
+ countdownElement.style.animation = 'none';
+ void countdownElement.offsetWidth;
+ countdownElement.style.animation = 'countdownPulse 1s ease infinite';
+
+ count--;
+
+ if (count < 0) {
+ clearInterval(countdownInterval);
+ }
+ }, 1000);
+}
+
+function populateWinners() {
+ const firstName = document.getElementById('first-name');
+ const firstCountry = document.getElementById('first-country');
+ const firstScore = document.getElementById('first-score');
+ const firstAvatar = document.querySelector('.first-avatar');
+
+ const firstFlag = getCountryFlag(CONFIG.winners.first.countryCode);
+ const firstInitials = getInitials(CONFIG.winners.first.fullName);
+ if (firstName) {
+ firstName.innerHTML = `${CONFIG.winners.first.name}
${CONFIG.winners.first.fullName}
`;
+ }
+ if (firstCountry) firstCountry.innerHTML = `${firstFlag} ${CONFIG.winners.first.country}`;
+ if (firstScore) firstScore.textContent = CONFIG.winners.first.score;
+ if (firstAvatar) {
+ const firstImageUrl = CONFIG.winners.first.photo || `photos/${CONFIG.winners.first.name}.png`;
+ firstAvatar.innerHTML = `
+
+ ${firstInitials}
+ `;
+ }
+
+ const secondName = document.getElementById('second-name');
+ const secondCountry = document.getElementById('second-country');
+ const secondScore = document.getElementById('second-score');
+ const secondAvatar = document.querySelector('.second-avatar');
+
+ const secondFlag = getCountryFlag(CONFIG.winners.second.countryCode);
+ const secondInitials = getInitials(CONFIG.winners.second.fullName);
+ if (secondName) {
+ secondName.innerHTML = `${CONFIG.winners.second.name}
${CONFIG.winners.second.fullName}
`;
+ }
+ if (secondCountry) secondCountry.innerHTML = `${secondFlag} ${CONFIG.winners.second.country}`;
+ if (secondScore) secondScore.textContent = CONFIG.winners.second.score;
+ if (secondAvatar) {
+ const secondImageUrl = CONFIG.winners.second.photo || `photos/${CONFIG.winners.second.name}.png`;
+ secondAvatar.innerHTML = `
+
+ ${secondInitials}
+ `;
+ }
+
+ const thirdName = document.getElementById('third-name');
+ const thirdCountry = document.getElementById('third-country');
+ const thirdScore = document.getElementById('third-score');
+ const thirdAvatar = document.querySelector('.third-avatar');
+
+ const thirdFlag = getCountryFlag(CONFIG.winners.third.countryCode);
+ const thirdInitials = getInitials(CONFIG.winners.third.fullName);
+ if (thirdName) {
+ thirdName.innerHTML = `${CONFIG.winners.third.name}
${CONFIG.winners.third.fullName}
`;
+ }
+ if (thirdCountry) thirdCountry.innerHTML = `${thirdFlag} ${CONFIG.winners.third.country}`;
+ if (thirdScore) thirdScore.textContent = CONFIG.winners.third.score;
+ if (thirdAvatar) {
+ const thirdImageUrl = CONFIG.winners.third.photo || `photos/${CONFIG.winners.third.name}.png`;
+ thirdAvatar.innerHTML = `
+
+ ${thirdInitials}
+ `;
+ }
+}
+
+// Puzzle Animation
+function initPuzzleAnimation() {
+ const canvas = document.getElementById('puzzle-animation');
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ const gridSize = 6;
+ const cellSize = Math.min(canvas.width, canvas.height) / gridSize;
+
+ // Set canvas size
+ canvas.width = gridSize * cellSize;
+ canvas.height = gridSize * cellSize;
+
+ // Color patterns for tiles (matching the problem description)
+ const colors = [
+ ['#3B82F6', '#EF4444', '#10B981', '#EC4899'], // Blue, Red, Green, Pink
+ ['#FBBF24', '#F97316', '#10B981', '#EF4444'], // Yellow, Orange, Green, Red
+ ['#EF4444', '#10B981', '#EC4899', '#3B82F6'], // Red, Green, Pink, Blue
+ ['#6B7280', '#6B7280', '#6B7280', '#6B7280'], // All Gray
+ ['#10B981', '#FBBF24', '#6B7280', '#F97316'], // Green, Yellow, Gray, Orange
+ ['#6B7280', '#3B82F6', '#10B981', '#EF4444'], // Gray, Blue, Green, Red
+ ['#EF4444', '#3B82F6', '#6B7280', '#10B981'], // Red, Blue, Gray, Green
+ ['#3B82F6', '#EC4899', '#EF4444', '#FBBF24'] // Blue, Pink, Red, Yellow
+ ];
+
+ let animationFrame = 0;
+ const totalFrames = 200; // Total animation frames
+ const tilesToPlace = 36; // 6x6 grid
+
+ function drawTile(x, y, colorPattern, alpha = 1) {
+ const size = cellSize * 0.9;
+ const offset = (cellSize - size) / 2;
+
+ ctx.save();
+ ctx.globalAlpha = alpha;
+
+ // Draw tile background
+ ctx.fillStyle = '#FFFFFF';
+ ctx.fillRect(x * cellSize + offset, y * cellSize + offset, size, size);
+
+ // Draw border
+ ctx.strokeStyle = '#E5E7EB';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(x * cellSize + offset, y * cellSize + offset, size, size);
+
+ // Draw four triangles
+ const centerX = x * cellSize + cellSize / 2;
+ const centerY = y * cellSize + cellSize / 2;
+ const halfSize = size / 2;
+
+ // Top-left triangle
+ ctx.fillStyle = colorPattern[0];
+ ctx.beginPath();
+ ctx.moveTo(centerX - halfSize, centerY - halfSize);
+ ctx.lineTo(centerX, centerY - halfSize);
+ ctx.lineTo(centerX - halfSize, centerY);
+ ctx.closePath();
+ ctx.fill();
+
+ // Top-right triangle
+ ctx.fillStyle = colorPattern[1];
+ ctx.beginPath();
+ ctx.moveTo(centerX, centerY - halfSize);
+ ctx.lineTo(centerX + halfSize, centerY - halfSize);
+ ctx.lineTo(centerX + halfSize, centerY);
+ ctx.closePath();
+ ctx.fill();
+
+ // Bottom-left triangle
+ ctx.fillStyle = colorPattern[2];
+ ctx.beginPath();
+ ctx.moveTo(centerX - halfSize, centerY);
+ ctx.lineTo(centerX, centerY + halfSize);
+ ctx.lineTo(centerX - halfSize, centerY + halfSize);
+ ctx.closePath();
+ ctx.fill();
+
+ // Bottom-right triangle
+ ctx.fillStyle = colorPattern[3];
+ ctx.beginPath();
+ ctx.moveTo(centerX, centerY + halfSize);
+ ctx.lineTo(centerX + halfSize, centerY);
+ ctx.lineTo(centerX + halfSize, centerY + halfSize);
+ ctx.closePath();
+ ctx.fill();
+
+ ctx.restore();
+ }
+
+ function animate() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Draw grid background
+ ctx.fillStyle = '#F5F7FA';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Draw grid lines
+ ctx.strokeStyle = '#E5E7EB';
+ ctx.lineWidth = 1;
+ for (let i = 0; i <= gridSize; i++) {
+ ctx.beginPath();
+ ctx.moveTo(i * cellSize, 0);
+ ctx.lineTo(i * cellSize, canvas.height);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(0, i * cellSize);
+ ctx.lineTo(canvas.width, i * cellSize);
+ ctx.stroke();
+ }
+
+ // Calculate how many tiles to show
+ const progress = (animationFrame % totalFrames) / totalFrames;
+ const tilesShown = Math.floor(progress * tilesToPlace);
+
+ // Place tiles in a spiral pattern for visual appeal
+ const positions = [];
+ for (let i = 0; i < gridSize; i++) {
+ for (let j = 0; j < gridSize; j++) {
+ positions.push([j, i]);
+ }
+ }
+
+ // Shuffle positions for more interesting animation
+ for (let i = positions.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [positions[i], positions[j]] = [positions[j], positions[i]];
+ }
+
+ for (let i = 0; i < tilesShown && i < positions.length; i++) {
+ const [x, y] = positions[i];
+ const colorIndex = i % colors.length;
+ const fadeIn = Math.min(1, (tilesShown - i) * 0.2);
+ drawTile(x, y, colors[colorIndex], fadeIn);
+ }
+
+ animationFrame++;
+ requestAnimationFrame(animate);
+ }
+
+ animate();
+}
diff --git a/public/mm-final-2025-reveal/styles.css b/public/mm-final-2025-reveal/styles.css
new file mode 100644
index 000000000..a57bfbc0c
--- /dev/null
+++ b/public/mm-final-2025-reveal/styles.css
@@ -0,0 +1,1617 @@
+@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@600;700;800&family=Nunito+Sans:wght@400;500;600;700&display=swap');
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --font-title: 'Figtree', 'Inter', 'Segoe UI', sans-serif;
+ --font-body: 'Nunito Sans', 'Inter', 'Segoe UI', sans-serif;
+
+ /* Brand Colors */
+ --tc-primary: #00797A;
+ /* highlight / accent */
+ --tc-primary-dark: #0F172A;
+ /* dark background anchor */
+ --tc-secondary: #00797A;
+ --tc-accent: #00797A;
+ --tc-purple: #00797A;
+ --tc-blue-light: #00797A;
+ --tc-link: #00797A;
+ --tc-link-hover: #0F172A;
+
+ /* Background Colors */
+ --bg-white: #FFFFFF;
+ --bg-light: #F5F7FA;
+ --bg-gray: #E5E7EB;
+ --bg-dark: #0F172A;
+ /* dark backgrounds */
+ --bg-darker: #0F172A;
+
+ /* Medal Colors */
+ --gold: #FFD700;
+ --silver: #C0C0C0;
+ --bronze: #CD7F32;
+
+ /* Card Colors */
+ --card-bg: #FFFFFF;
+ --card-border: #E5E7EB;
+ --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+
+ /* Text Colors */
+ --text-primary: #0A0A0A;
+ --text-secondary: #767676;
+ --text-muted: #9CA3AF;
+ --text-white: #FFFFFF;
+
+ /* Gradients */
+ --gradient-tc: linear-gradient(135deg, #00797A 0%, #0F172A 100%);
+ --gradient-hero: linear-gradient(135deg, #00797A 0%, #0F172A 50%, #00797A 100%);
+ --gradient-gold: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
+ --gradient-silver: linear-gradient(135deg, #C0C0C0 0%, #E8E8E8 100%);
+ --gradient-bronze: linear-gradient(135deg, #CD7F32 0%, #D4A574 100%);
+
+ /* Shadows */
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
+ --shadow-glow: 0 0 40px rgba(0, 121, 122, 0.3);
+ --shadow-glow-gold: 0 0 60px rgba(255, 215, 0, 0.4);
+}
+
+body {
+ font-family: var(--font-body);
+ background: var(--bg-light);
+ color: var(--text-primary);
+ overflow: hidden;
+ line-height: 1.6;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100vh;
+ position: relative;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+.intro-title,
+.hero-title,
+.section-title,
+.leaderboard-title,
+.podium-title,
+.cta-title {
+ font-family: var(--font-title);
+}
+
+.container {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 0 20px;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.main-content {
+ position: relative;
+ width: 100%;
+ height: 100vh;
+ overflow: hidden;
+}
+
+/* Full Screen Presentation Sections */
+.presentation-section {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100vh;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ opacity: 1;
+ visibility: visible;
+ transform: scale(1);
+ z-index: 10;
+ pointer-events: auto;
+ overflow: hidden;
+ transition: opacity 0.5s ease, transform 0.5s ease;
+}
+
+.presentation-section.active {
+ display: flex;
+ opacity: 1;
+ visibility: visible;
+ transform: scale(1);
+}
+
+/* Intro Screen */
+.intro-screen {
+ background: var(--bg-white);
+ position: relative;
+ overflow: hidden;
+}
+
+.intro-screen::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(circle at 50% 50%, rgba(0, 102, 255, 0.1) 0%, transparent 70%);
+ animation: pulseGlow 3s ease-in-out infinite;
+}
+
+@keyframes pulseGlow {
+
+ 0%,
+ 100% {
+ opacity: 0.5;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
+
+.intro-content {
+ text-align: center;
+ z-index: 2;
+ position: relative;
+}
+
+.logo-container {
+ margin-bottom: 3rem;
+}
+
+.globe-trophy-container {
+ width: 200px;
+ height: 200px;
+ margin: 0 auto 2rem;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.globe {
+ width: 180px;
+ height: 180px;
+ position: relative;
+ animation: globeRotate 20s linear infinite;
+}
+
+.globe-sphere {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background:
+ radial-gradient(circle at 30% 30%, rgba(34, 139, 34, 0.4) 0%, transparent 25%),
+ radial-gradient(circle at 70% 40%, rgba(139, 69, 19, 0.3) 0%, transparent 20%),
+ radial-gradient(circle at 50% 60%, rgba(34, 139, 34, 0.3) 0%, transparent 18%),
+ radial-gradient(circle at 20% 70%, rgba(139, 69, 19, 0.25) 0%, transparent 15%),
+ radial-gradient(circle at 80% 20%, rgba(34, 139, 34, 0.35) 0%, transparent 22%),
+ linear-gradient(135deg, #0f172a 0%, #0f7f80 20%, #2fa9aa 40%, #0f7f80 60%, #0f172a 80%, #0f172a 100%);
+ position: relative;
+ box-shadow:
+ inset -30px -30px 60px rgba(0, 0, 0, 0.4),
+ inset 30px 30px 60px rgba(255, 255, 255, 0.15),
+ 0 0 80px rgba(59, 130, 246, 0.4),
+ 0 0 120px rgba(59, 130, 246, 0.2);
+ overflow: hidden;
+}
+
+.globe-lines {
+ position: absolute;
+ width: 100%;
+ height: 1px;
+ background: rgba(255, 255, 255, 0.2);
+ left: 0;
+}
+
+.globe-lines:nth-child(1) {
+ top: 10%;
+ transform: rotate(0deg);
+}
+
+.globe-lines:nth-child(2) {
+ top: 30%;
+ transform: rotate(0deg);
+}
+
+.globe-lines:nth-child(3) {
+ top: 50%;
+ transform: translateY(-50%);
+ height: 100%;
+ width: 1px;
+ background: rgba(255, 255, 255, 0.25);
+ left: 50%;
+}
+
+.globe-lines:nth-child(4) {
+ top: 70%;
+ transform: rotate(0deg);
+}
+
+.globe-lines:nth-child(5) {
+ top: 90%;
+ transform: rotate(0deg);
+}
+
+.trophy {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 4rem;
+ z-index: 10;
+ animation: trophyFloat 3s ease-in-out infinite;
+ filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.8));
+ text-shadow: 0 0 30px rgba(255, 215, 0, 0.6);
+ pointer-events: none;
+}
+
+@keyframes globeRotate {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+/* Add more Earth-like details with pseudo-elements */
+.globe-sphere::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background:
+ radial-gradient(ellipse 40% 30% at 25% 35%, rgba(34, 139, 34, 0.5) 0%, transparent 50%),
+ radial-gradient(ellipse 35% 25% at 70% 45%, rgba(139, 69, 19, 0.4) 0%, transparent 50%),
+ radial-gradient(ellipse 30% 20% at 50% 65%, rgba(34, 139, 34, 0.4) 0%, transparent 50%),
+ radial-gradient(ellipse 25% 15% at 15% 75%, rgba(139, 69, 19, 0.3) 0%, transparent 50%),
+ radial-gradient(ellipse 30% 25% at 85% 25%, rgba(34, 139, 34, 0.45) 0%, transparent 50%);
+ pointer-events: none;
+}
+
+.globe-sphere::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background:
+ radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 30%),
+ radial-gradient(circle at 70% 70%, rgba(0, 0, 0, 0.1) 0%, transparent 30%);
+ pointer-events: none;
+}
+
+@keyframes trophyFloat {
+
+ 0%,
+ 100% {
+ transform: translate(-50%, -50%) translateY(0px) scale(1);
+ }
+
+ 50% {
+ transform: translate(-50%, -50%) translateY(-10px) scale(1.05);
+ }
+}
+
+.intro-title {
+ font-size: clamp(2rem, 5vw, 3rem);
+ font-weight: 800;
+ color: var(--tc-primary);
+ margin-bottom: 1rem;
+ letter-spacing: -0.02em;
+}
+
+.intro-subtitle {
+ font-size: clamp(1rem, 2vw, 1.2rem);
+ color: var(--text-secondary);
+ margin-bottom: 2rem;
+}
+
+.loading-bar {
+ width: 400px;
+ height: 6px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 3px;
+ margin: 0 auto;
+ overflow: hidden;
+}
+
+.loading-progress {
+ height: 100%;
+ background: var(--gradient-tc);
+ width: 0%;
+ animation: loading 2.5s ease-in-out forwards;
+ box-shadow: 0 0 20px rgba(0, 121, 122, 0.6);
+}
+
+@keyframes loading {
+ to {
+ width: 100%;
+ }
+}
+
+/* Hero Section */
+.hero-section {
+ background: var(--gradient-hero);
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-section .hero-content {
+ color: var(--text-white);
+}
+
+.hero-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background:
+ radial-gradient(circle at 20% 50%, rgba(0, 121, 122, 0.15) 0%, transparent 50%),
+ radial-gradient(circle at 80% 50%, rgba(15, 23, 42, 0.15) 0%, transparent 50%);
+ animation: backgroundShift 10s ease-in-out infinite;
+}
+
+@keyframes backgroundShift {
+
+ 0%,
+ 100% {
+ opacity: 0.5;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
+
+.hero-content {
+ text-align: center;
+ z-index: 2;
+ position: relative;
+}
+
+.hero-title {
+ font-size: clamp(2rem, 6vw, 4rem);
+ font-weight: 900;
+ line-height: 1.1;
+ margin-bottom: 1rem;
+ color: var(--text-white);
+}
+
+.hero-title .text-line {
+ display: block;
+ opacity: 0;
+ transform: translateY(50px);
+ animation: heroReveal 1s ease forwards;
+}
+
+.hero-title .text-line:nth-child(1) {
+ animation-delay: 0.2s;
+}
+
+.hero-title .text-line:nth-child(2) {
+ animation-delay: 0.4s;
+}
+
+.hero-title .text-line:nth-child(3) {
+ animation-delay: 0.6s;
+}
+
+.hero-title .highlight {
+ color: var(--tc-primary);
+}
+
+@keyframes heroReveal {
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.hero-subtitle {
+ font-size: clamp(1rem, 2.5vw, 1.4rem);
+ color: rgba(255, 255, 255, 0.9);
+ opacity: 0;
+ animation: heroReveal 1s ease forwards;
+ animation-delay: 0.8s;
+}
+
+/* Problem Section */
+.problem-section {
+ background: var(--bg-white);
+ position: relative;
+}
+
+.problem-content {
+ max-width: 900px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 0 1rem;
+}
+
+.section-header {
+ text-align: center;
+ margin-bottom: 1.5rem;
+}
+
+.section-header h2 {
+ font-size: clamp(2rem, 5vw, 3rem);
+ font-weight: 800;
+ color: var(--tc-primary);
+ margin-bottom: 0.5rem;
+ letter-spacing: -0.02em;
+}
+
+.section-subtitle {
+ font-size: clamp(0.9rem, 1.5vw, 1.1rem);
+ color: var(--text-secondary);
+}
+
+.problem-card {
+ background: var(--card-bg);
+ border-radius: 12px;
+ padding: 2rem;
+ box-shadow: var(--shadow-lg);
+ border: 1px solid var(--card-border);
+ max-width: 900px;
+ width: 100%;
+}
+
+.problem-icon {
+ font-size: clamp(3rem, 6vw, 4rem);
+ text-align: center;
+ margin-bottom: 1rem;
+}
+
+.problem-card h3 {
+ font-size: clamp(1.5rem, 3vw, 2rem);
+ margin-bottom: 1rem;
+ text-align: center;
+ color: var(--text-primary);
+}
+
+.problem-details p {
+ font-size: clamp(0.9rem, 1.5vw, 1.1rem);
+ color: var(--text-secondary);
+ margin-bottom: 1.5rem;
+ text-align: center;
+ line-height: 1.6;
+}
+
+.problem-summary {
+ font-size: clamp(1rem, 1.6vw, 1.2rem) !important;
+ color: var(--text-primary) !important;
+ margin-bottom: 2rem !important;
+ line-height: 1.8 !important;
+ max-width: 800px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.problem-animation-container {
+ margin: 2rem 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+}
+
+.puzzle-animation {
+ width: 100%;
+ max-width: 500px;
+ height: 500px;
+ border: 2px solid var(--card-border);
+ border-radius: 12px;
+ background: var(--bg-light);
+ box-shadow: var(--shadow-md);
+}
+
+.animation-label {
+ font-size: clamp(0.8rem, 1.2vw, 0.9rem);
+ color: var(--text-secondary);
+ font-style: italic;
+}
+
+.problem-features {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ margin: 1.5rem 0;
+}
+
+.feature-item {
+ display: flex;
+ gap: 1rem;
+ padding: 1rem;
+ background: var(--bg-light);
+ border-radius: 8px;
+ border: 1px solid var(--card-border);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.feature-item:hover {
+ transform: translateY(-3px);
+ box-shadow: var(--shadow-md);
+ border-color: var(--tc-primary);
+}
+
+.feature-icon {
+ font-size: clamp(1.5rem, 3vw, 2rem);
+ flex-shrink: 0;
+}
+
+.feature-item strong {
+ display: block;
+ margin-bottom: 0.3rem;
+ color: var(--text-primary);
+ font-size: clamp(0.9rem, 1.5vw, 1rem);
+}
+
+.feature-item p {
+ font-size: clamp(0.8rem, 1.2vw, 0.9rem);
+ color: var(--text-secondary);
+ margin: 0;
+ text-align: left;
+}
+
+.problem-credits {
+ margin-top: 1.5rem;
+ padding-top: 1.5rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ flex-wrap: wrap;
+}
+
+.problem-credits p {
+ margin: 0;
+ font-size: clamp(0.8rem, 1.2vw, 0.9rem);
+}
+
+/* Finalists Section */
+.finalists-section {
+ background: var(--bg-light);
+ position: relative;
+}
+
+.finalists-content {
+ max-width: 1200px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 0 1rem;
+}
+
+.finalists-grid {
+ display: grid;
+ grid-template-columns: repeat(6, minmax(140px, 1fr));
+ gap: 1rem;
+ margin-top: 1.5rem;
+ max-width: 1200px;
+ width: 100%;
+}
+
+.finalist-card {
+ background: var(--card-bg);
+ border-radius: 12px;
+ padding: 1rem;
+ text-align: center;
+ border: 1px solid var(--card-border);
+ box-shadow: var(--shadow-sm);
+ transition: all 0.3s ease;
+ opacity: 0;
+ transform: translateY(30px) scale(0.9);
+ animation: finalistReveal 0.6s ease forwards;
+}
+
+.finalist-card:nth-child(1) {
+ animation-delay: 0.1s;
+}
+
+.finalist-card:nth-child(2) {
+ animation-delay: 0.2s;
+}
+
+.finalist-card:nth-child(3) {
+ animation-delay: 0.3s;
+}
+
+.finalist-card:nth-child(4) {
+ animation-delay: 0.4s;
+}
+
+.finalist-card:nth-child(5) {
+ animation-delay: 0.5s;
+}
+
+.finalist-card:nth-child(6) {
+ animation-delay: 0.6s;
+}
+
+.finalist-card:nth-child(7) {
+ animation-delay: 0.7s;
+}
+
+.finalist-card:nth-child(8) {
+ animation-delay: 0.8s;
+}
+
+.finalist-card:nth-child(9) {
+ animation-delay: 0.9s;
+}
+
+.finalist-card:nth-child(10) {
+ animation-delay: 1.0s;
+}
+
+.finalist-card:nth-child(11) {
+ animation-delay: 1.1s;
+}
+
+.finalist-card:nth-child(12) {
+ animation-delay: 1.2s;
+}
+
+.finalist-card:hover {
+ transform: translateY(-5px);
+ box-shadow: var(--shadow-md);
+ border-color: var(--tc-primary);
+}
+
+@keyframes finalistReveal {
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.finalist-avatar {
+ width: 80px;
+ height: 80px;
+ margin: 0 auto 0.8rem;
+ border-radius: 50%;
+ background: var(--bg-light);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.8rem;
+ border: 2px solid var(--card-border);
+ overflow: hidden;
+ position: relative;
+}
+
+.finalist-avatar img.finalist-photo {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.finalist-avatar .avatar-placeholder {
+ font-size: 1.8rem;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-light);
+ color: var(--text-secondary);
+ font-weight: 700;
+}
+
+.finalist-name {
+ font-size: clamp(0.9rem, 1.5vw, 1rem);
+ font-weight: 600;
+ margin-bottom: 0.2rem;
+ color: var(--text-primary);
+}
+
+.finalist-fullname {
+ font-size: clamp(0.7rem, 1.1vw, 0.8rem);
+ color: var(--text-secondary);
+ margin-bottom: 0.2rem;
+ font-weight: 400;
+}
+
+.finalist-country {
+ font-size: clamp(0.75rem, 1.2vw, 0.85rem);
+ color: var(--text-secondary);
+}
+
+/* Leaderboard Section - Topcoder AI Hub Style */
+.leaderboard-transition-section {
+ background: var(--bg-white);
+ position: relative;
+}
+
+.leaderboard-container {
+ background: var(--card-bg);
+ border-radius: 8px;
+ padding: 0.75rem;
+ box-shadow: var(--shadow-md);
+ border: 1px solid var(--card-border);
+ max-width: 1000px;
+ width: 100%;
+ margin: 0 auto;
+ max-height: 88vh;
+ overflow-y: hidden;
+}
+
+.leaderboard-header {
+ margin-bottom: 0.4rem;
+ position: relative;
+}
+
+.leaderboard-title-container {
+ text-align: center;
+ margin-bottom: 0.4rem;
+}
+
+.leaderboard-title {
+ font-size: clamp(1.2rem, 2.5vw, 1.6rem);
+ font-weight: 800;
+ margin-bottom: 0.2rem;
+ color: var(--tc-primary);
+ transition: all 0.5s ease;
+ letter-spacing: 0.05em;
+}
+
+.leaderboard-subtitle {
+ font-size: clamp(0.75rem, 1.3vw, 0.9rem);
+ color: var(--text-secondary);
+}
+
+.transition-indicator {
+ text-align: center;
+ margin-top: 1rem;
+ padding: 1rem;
+ background: rgba(0, 102, 255, 0.1);
+ border-radius: 10px;
+ border: 2px solid rgba(0, 102, 255, 0.3);
+}
+
+.transition-text {
+ font-size: clamp(0.9rem, 1.5vw, 1.1rem);
+ color: var(--text-primary);
+ margin-bottom: 0.8rem;
+ font-weight: 600;
+}
+
+.transition-bar {
+ width: 100%;
+ height: 6px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.transition-progress {
+ height: 100%;
+ background: var(--gradient-tc);
+ width: 0%;
+ animation: transitionProgress 2s ease forwards;
+ box-shadow: 0 0 20px rgba(0, 102, 255, 0.6);
+}
+
+@keyframes transitionProgress {
+ to {
+ width: 100%;
+ }
+}
+
+.leaderboard-table-wrapper {
+ overflow-x: auto;
+ overflow-y: hidden;
+ border-radius: 8px;
+ border: 1px solid var(--card-border);
+ max-height: calc(88vh - 120px);
+}
+
+.leaderboard-table {
+ width: 100%;
+ border-collapse: collapse;
+ background: var(--card-bg);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.leaderboard-table thead {
+ background: var(--bg-light);
+ border-bottom: 2px solid var(--card-border);
+}
+
+.leaderboard-table th {
+ padding: 0.4rem 0.6rem;
+ text-align: left;
+ font-weight: 600;
+ font-size: clamp(0.65rem, 1vw, 0.8rem);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--text-secondary);
+ border-bottom: none;
+}
+
+.leaderboard-table th.rank-col {
+ width: 100px;
+ text-align: center;
+}
+
+.leaderboard-table th.member-col {
+ width: auto;
+}
+
+.leaderboard-table th.score-col {
+ width: 250px;
+ text-align: right;
+}
+
+.leaderboard-table th.change-col {
+ width: 150px;
+ text-align: center;
+}
+
+.leaderboard-table tbody tr {
+ border-bottom: 1px solid var(--card-border);
+ transition: all 0.2s ease;
+}
+
+.leaderboard-table tbody tr:last-child {
+ border-bottom: none;
+}
+
+.leaderboard-table tbody tr:hover {
+ background: var(--bg-light);
+}
+
+.leaderboard-table tbody tr.rank-change {
+ animation: rankChangeHighlight 1s ease;
+}
+
+.leaderboard-table tbody tr.rank-up {
+ background: rgba(76, 175, 80, 0.08);
+ border-left: 3px solid #4caf50;
+}
+
+.leaderboard-table tbody tr.rank-down {
+ background: rgba(244, 67, 54, 0.08);
+ border-left: 3px solid #f44336;
+}
+
+.leaderboard-table tbody tr.rank-same {
+ border-left: 3px solid transparent;
+}
+
+@keyframes rankChangeHighlight {
+ 0% {
+ background: rgba(0, 102, 255, 0.4);
+ }
+
+ 100% {
+ background: transparent;
+ }
+}
+
+.leaderboard-table td {
+ padding: 0.4rem 0.6rem;
+ color: var(--text-primary);
+ font-size: clamp(0.7rem, 1vw, 0.85rem);
+}
+
+.leaderboard-table td.rank-cell {
+ text-align: center;
+ font-weight: 800;
+ font-size: clamp(0.85rem, 1.6vw, 1rem);
+}
+
+.leaderboard-table td.rank-cell.top-1 {
+ color: var(--gold);
+ text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
+}
+
+.leaderboard-table td.rank-cell.top-2 {
+ color: var(--silver);
+}
+
+.leaderboard-table td.rank-cell.top-3 {
+ color: var(--bronze);
+}
+
+.leaderboard-table td.member-cell {
+ font-weight: 600;
+ font-size: clamp(0.85rem, 1.3vw, 1rem);
+ color: var(--text-primary);
+}
+
+.member-info {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.member-avatar {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ overflow: hidden;
+ flex-shrink: 0;
+ background: var(--bg-light);
+ border: 2px solid var(--card-border);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+
+.leaderboard-avatar {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.leaderboard-avatar-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: var(--text-secondary);
+ background: var(--bg-light);
+}
+
+.member-details {
+ display: flex;
+ flex-direction: column;
+ gap: 0.1rem;
+}
+
+.member-name {
+ font-weight: 600;
+ color: var(--text-primary);
+ line-height: 1.2;
+}
+
+.member-country {
+ font-size: clamp(0.65rem, 0.9vw, 0.75rem);
+ color: var(--text-secondary);
+ font-weight: 400;
+ line-height: 1.2;
+}
+
+.leaderboard-table td.score-cell {
+ text-align: right;
+ font-family: 'Courier New', monospace;
+ font-size: clamp(0.65rem, 1vw, 0.8rem);
+ color: var(--text-secondary);
+}
+
+.leaderboard-table td.change-cell {
+ text-align: center;
+ font-weight: 700;
+}
+
+.change-indicator {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ padding: 0.3rem 0.6rem;
+ border-radius: 12px;
+ font-size: clamp(0.7rem, 1vw, 0.85rem);
+ font-weight: 600;
+}
+
+.change-up {
+ background: rgba(76, 175, 80, 0.15);
+ color: #16a34a;
+ border: 1px solid rgba(76, 175, 80, 0.3);
+}
+
+.change-down {
+ background: rgba(244, 67, 54, 0.15);
+ color: #dc2626;
+ border: 1px solid rgba(244, 67, 54, 0.3);
+}
+
+.change-same {
+ background: var(--bg-light);
+ color: var(--text-secondary);
+ border: 1px solid var(--card-border);
+}
+
+.change-new {
+ background: rgba(0, 121, 122, 0.15);
+ color: var(--tc-primary);
+ border: 1px solid rgba(0, 121, 122, 0.3);
+}
+
+/* Countdown Section */
+.countdown-section {
+ background: var(--gradient-hero);
+ position: relative;
+}
+
+.countdown-content {
+ color: var(--text-white);
+}
+
+.countdown-content {
+ text-align: center;
+}
+
+.countdown-content h2 {
+ font-size: clamp(2rem, 5vw, 3rem);
+ margin-bottom: 1.5rem;
+ color: var(--text-white);
+ font-weight: 800;
+ letter-spacing: -0.02em;
+}
+
+.countdown-timer {
+ display: inline-block;
+}
+
+.countdown-number {
+ font-size: clamp(6rem, 15vw, 10rem);
+ font-weight: 900;
+ color: var(--text-white);
+ display: inline-block;
+ animation: countdownPulse 1s ease infinite;
+ line-height: 1;
+ text-shadow: 0 0 30px rgba(255, 255, 255, 0.5);
+}
+
+@keyframes countdownPulse {
+
+ 0%,
+ 100% {
+ transform: scale(1);
+ }
+
+ 50% {
+ transform: scale(1.15);
+ }
+}
+
+/* Podium Section */
+.podium-section {
+ background: var(--bg-white);
+ position: relative;
+ overflow: hidden;
+}
+
+.podium-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background:
+ radial-gradient(circle at 30% 30%, rgba(255, 215, 0, 0.1) 0%, transparent 50%),
+ radial-gradient(circle at 70% 70%, rgba(0, 102, 255, 0.1) 0%, transparent 50%);
+}
+
+.podium-container {
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+ gap: 40px;
+ max-width: 900px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 0.5rem 0;
+ flex-wrap: wrap;
+ position: relative;
+ z-index: 2;
+}
+
+.podium-place {
+ flex: 1;
+ min-width: 200px;
+ max-width: 280px;
+}
+
+.podium-pedestal {
+ position: relative;
+}
+
+.podium-base {
+ background: var(--card-bg);
+ border-radius: 12px 12px 0 0;
+ padding: 1.5rem 1rem 2.5rem;
+ text-align: center;
+ position: relative;
+ border: 1px solid var(--card-border);
+ box-shadow: var(--shadow-md);
+}
+
+.first-base {
+ height: clamp(120px, 18vh, 160px);
+ background: var(--gradient-gold);
+ border-color: var(--gold);
+ box-shadow: 0 20px 60px rgba(255, 215, 0, 0.4), var(--shadow-glow-gold);
+}
+
+.second-base {
+ height: clamp(100px, 15vh, 130px);
+ background: var(--gradient-silver);
+ border-color: var(--silver);
+ box-shadow: 0 20px 60px rgba(192, 192, 192, 0.3);
+}
+
+.third-base {
+ height: clamp(85px, 13vh, 110px);
+ background: var(--gradient-bronze);
+ border-color: var(--bronze);
+ box-shadow: 0 20px 60px rgba(205, 127, 50, 0.3);
+}
+
+.podium-number {
+ font-size: clamp(1.8rem, 4vw, 2.5rem);
+ font-weight: 900;
+ position: absolute;
+ top: 0.5rem;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 10;
+ line-height: 1;
+}
+
+.first-base .podium-number {
+ color: #0A0A0A;
+ text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
+}
+
+.second-base .podium-number {
+ color: #0A0A0A;
+}
+
+.third-base .podium-number {
+ color: #fff;
+}
+
+.crown {
+ font-size: clamp(1.8rem, 3vw, 2.2rem);
+ position: absolute;
+ top: -1.2rem;
+ left: 50%;
+ transform: translateX(-50%);
+ animation: crownFloat 3s ease-in-out infinite;
+ filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.8));
+}
+
+@keyframes crownFloat {
+
+ 0%,
+ 100% {
+ transform: translateX(-50%) translateY(0) rotate(0deg);
+ }
+
+ 50% {
+ transform: translateX(-50%) translateY(-15px) rotate(5deg);
+ }
+}
+
+.podium-trophy {
+ font-size: clamp(2rem, 4vw, 3rem);
+ position: absolute;
+ bottom: 0.3rem;
+ left: 50%;
+ transform: translateX(-50%);
+ animation: trophyFloat 3s ease-in-out infinite;
+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
+ z-index: 5;
+ line-height: 1;
+}
+
+.trophy-gold {
+ filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.8)) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
+ animation: trophyFloat 3s ease-in-out infinite, trophyGlow 2s ease-in-out infinite;
+}
+
+.trophy-silver {
+ filter: drop-shadow(0 0 15px rgba(192, 192, 192, 0.6)) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
+}
+
+.trophy-bronze {
+ filter: drop-shadow(0 0 15px rgba(205, 127, 50, 0.6)) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
+}
+
+@keyframes trophyFloat {
+
+ 0%,
+ 100% {
+ transform: translateX(-50%) translateY(0) rotate(0deg);
+ }
+
+ 50% {
+ transform: translateX(-50%) translateY(-8px) rotate(-5deg);
+ }
+}
+
+@keyframes trophyGlow {
+
+ 0%,
+ 100% {
+ filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.8)) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
+ }
+
+ 50% {
+ filter: drop-shadow(0 0 30px rgba(255, 215, 0, 1)) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
+ }
+}
+
+.winner-card {
+ background: var(--card-bg);
+ border-radius: 12px;
+ padding: 1rem;
+ margin-top: 0.5rem;
+ text-align: center;
+ border: 1px solid var(--card-border);
+ box-shadow: var(--shadow-md);
+ transition: transform 0.3s ease;
+ position: relative;
+}
+
+.first-winner {
+ border-color: var(--gold);
+ box-shadow: 0 25px 80px rgba(255, 215, 0, 0.5), var(--shadow-glow-gold);
+ transform: scale(1.08);
+}
+
+.second-winner {
+ border-color: var(--silver);
+}
+
+.third-winner {
+ border-color: var(--bronze);
+}
+
+.winner-avatar {
+ width: clamp(70px, 10vw, 90px);
+ height: clamp(70px, 10vw, 90px);
+ margin: 0 auto 0.6rem;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: clamp(2rem, 4vw, 2.5rem);
+ position: relative;
+ overflow: hidden;
+ border: 3px solid;
+}
+
+.winner-avatar img.winner-photo {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.first-avatar {
+ background: var(--gradient-gold);
+ border-color: var(--gold);
+ box-shadow: 0 0 40px rgba(255, 215, 0, 0.6);
+ animation: avatarGlow 2s ease-in-out infinite;
+}
+
+.second-avatar {
+ background: var(--gradient-silver);
+ border-color: var(--silver);
+ box-shadow: 0 0 30px rgba(192, 192, 192, 0.5);
+}
+
+.third-avatar {
+ background: var(--gradient-bronze);
+ border-color: var(--bronze);
+ box-shadow: 0 0 30px rgba(205, 127, 50, 0.5);
+}
+
+@keyframes avatarGlow {
+
+ 0%,
+ 100% {
+ box-shadow: 0 0 40px rgba(255, 215, 0, 0.6);
+ }
+
+ 50% {
+ box-shadow: 0 0 60px rgba(255, 215, 0, 0.9);
+ }
+}
+
+.avatar-placeholder {
+ font-size: 4rem;
+}
+
+.winner-info {
+ margin-bottom: 0.8rem;
+}
+
+.winner-name {
+ font-size: clamp(1.3rem, 2.5vw, 1.8rem);
+ font-weight: 700;
+ margin-bottom: 0.3rem;
+ color: var(--text-primary);
+}
+
+.winner-name .winner-fullname {
+ font-size: clamp(0.9rem, 1.5vw, 1.1rem);
+ font-weight: 400;
+ color: var(--text-secondary);
+ margin-top: 0.2rem;
+}
+
+.first-winner .winner-name {
+ color: #0A0A0A;
+ text-shadow: none;
+}
+
+.winner-country {
+ font-size: clamp(0.9rem, 1.5vw, 1rem);
+ color: var(--text-secondary);
+ margin-bottom: 1rem;
+}
+
+.winner-score {
+ font-size: clamp(1rem, 2vw, 1.3rem);
+ font-weight: 700;
+ color: var(--text-primary);
+ padding: 0.6rem 1.2rem;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ display: inline-block;
+ font-family: 'Courier New', monospace;
+}
+
+.winner-badge {
+ position: absolute;
+ top: -15px;
+ right: -15px;
+ padding: 0.8rem 1.5rem;
+ border-radius: 25px;
+ font-size: 0.9rem;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
+}
+
+.first-badge {
+ background: var(--gold);
+ color: #0A0A0A;
+ box-shadow: 0 0 30px rgba(255, 215, 0, 0.8);
+}
+
+.second-badge {
+ background: var(--silver);
+ color: #0A0A0A;
+}
+
+.third-badge {
+ background: var(--bronze);
+ color: #fff;
+}
+
+.reveal-podium {
+ opacity: 0;
+ transform: translateY(80px) scale(0.8);
+ animation: revealPodium 1.2s ease forwards;
+ animation-delay: 0.3s;
+}
+
+.reveal-podium-delay {
+ opacity: 0;
+ transform: translateY(80px) scale(0.8);
+ animation: revealPodium 1.2s ease forwards;
+ animation-delay: 0.5s;
+}
+
+.reveal-podium-delay-2 {
+ opacity: 0;
+ transform: translateY(80px) scale(0.8);
+ animation: revealPodium 1.2s ease forwards;
+ animation-delay: 0.7s;
+}
+
+@keyframes revealPodium {
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.celebration-message {
+ text-align: center;
+ margin-top: 80px;
+ padding: 1.5rem;
+ background: var(--card-bg);
+ border-radius: 12px;
+ border: 1px solid var(--card-border);
+ box-shadow: var(--shadow-sm);
+ max-width: 700px;
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+ position: relative;
+ z-index: 2;
+}
+
+.celebration-message h2 {
+ font-size: clamp(1.5rem, 3vw, 2rem);
+ margin-bottom: 1rem;
+ color: var(--tc-primary);
+ font-weight: 800;
+}
+
+.celebration-message p {
+ font-size: clamp(0.9rem, 1.5vw, 1.1rem);
+ color: var(--text-secondary);
+ margin-bottom: 1.5rem;
+}
+
+.celebration-stats {
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ flex-wrap: wrap;
+ margin-top: 1.5rem;
+}
+
+.stat-item {
+ text-align: center;
+}
+
+.stat-number {
+ font-size: clamp(2rem, 4vw, 3rem);
+ font-weight: 900;
+ color: var(--tc-primary);
+ margin-bottom: 0.5rem;
+ line-height: 1;
+}
+
+.stat-label {
+ font-size: clamp(0.8rem, 1.2vw, 1rem);
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+/* Leaderboard CTA Section */
+.leaderboard-section {
+ background: var(--bg-light);
+ position: relative;
+}
+
+.leaderboard-cta {
+ text-align: center;
+ padding: 2rem;
+ background: var(--card-bg);
+ border-radius: 12px;
+ border: 1px solid var(--card-border);
+ box-shadow: var(--shadow-sm);
+ max-width: 700px;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.leaderboard-cta h3 {
+ font-size: clamp(1.5rem, 3vw, 2rem);
+ margin-bottom: 1rem;
+ font-weight: 800;
+}
+
+.leaderboard-cta p {
+ font-size: clamp(0.9rem, 1.5vw, 1.1rem);
+ color: var(--text-secondary);
+ margin-bottom: 1.5rem;
+}
+
+.cta-button {
+ display: inline-block;
+ padding: 1.2rem 3rem;
+ background: var(--gradient-tc);
+ color: var(--text-primary);
+ text-decoration: none;
+ border-radius: 50px;
+ font-weight: 700;
+ font-size: 1.2rem;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ box-shadow: 0 10px 30px rgba(0, 102, 255, 0.4);
+}
+
+.cta-button:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 15px 40px rgba(0, 102, 255, 0.6);
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .podium-container {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .podium-place {
+ width: 100%;
+ max-width: 250px;
+ }
+
+ .first-place {
+ order: 1;
+ }
+
+ .second-place {
+ order: 2;
+ }
+
+ .third-place {
+ order: 3;
+ }
+
+ .countdown-number {
+ font-size: clamp(4rem, 12vw, 6rem);
+ }
+
+ .problem-features {
+ grid-template-columns: 1fr;
+ }
+
+ .celebration-stats {
+ gap: 3rem;
+ }
+
+ .container {
+ padding: 0 15px;
+ }
+
+ .leaderboard-container {
+ max-height: 85vh;
+ padding: 0.8rem;
+ }
+
+ .podium-container {
+ gap: 0.8rem;
+ padding: 0.3rem 0;
+ }
+
+ .first-base {
+ height: clamp(100px, 15vh, 130px);
+ }
+
+ .second-base {
+ height: clamp(85px, 12vh, 110px);
+ }
+
+ .third-base {
+ height: clamp(70px, 10vh, 90px);
+ }
+}
\ No newline at end of file
diff --git a/src/apps/calendar/index.ts b/src/apps/calendar/index.ts
new file mode 100644
index 000000000..6f39cd49b
--- /dev/null
+++ b/src/apps/calendar/index.ts
@@ -0,0 +1 @@
+export * from './src'
diff --git a/src/apps/calendar/src/CalendarApp.tsx b/src/apps/calendar/src/CalendarApp.tsx
new file mode 100644
index 000000000..39bc823bc
--- /dev/null
+++ b/src/apps/calendar/src/CalendarApp.tsx
@@ -0,0 +1,33 @@
+import { FC, useContext, useEffect, useMemo } from 'react'
+import { Outlet, Routes } from 'react-router-dom'
+
+import { routerContext, RouterContextData } from '~/libs/core'
+
+import { CalendarContextProvider, Layout, SWRConfigProvider } from './lib'
+import { toolTitle } from './calendar-app.routes'
+import './lib/styles/index.scss'
+
+const CalendarApp: FC = () => {
+ const { getChildRoutes }: RouterContextData = useContext(routerContext)
+ const childRoutes = useMemo(() => getChildRoutes(toolTitle), [getChildRoutes])
+
+ useEffect(() => {
+ document.body.classList.add('calendar-app')
+ return () => {
+ document.body.classList.remove('calendar-app')
+ }
+ }, [])
+
+ return (
+
+
+
+
+ {childRoutes}
+
+
+
+ )
+}
+
+export default CalendarApp
diff --git a/src/apps/calendar/src/calendar-app.routes.tsx b/src/apps/calendar/src/calendar-app.routes.tsx
new file mode 100644
index 000000000..b3eec295f
--- /dev/null
+++ b/src/apps/calendar/src/calendar-app.routes.tsx
@@ -0,0 +1,43 @@
+import { AppSubdomain, ToolTitle } from '~/config'
+import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core'
+import { UserRole } from '~/libs/core/lib/profile/profile-functions/profile-factory/user-role.enum'
+
+import { personalCalendarRouteId, rootRoute, teamCalendarRouteId } from './config/routes.config'
+
+const CalendarApp: LazyLoadedComponent = lazyLoad(() => import('./CalendarApp'))
+const PersonalCalendarPage: LazyLoadedComponent = lazyLoad(
+ () => import('./pages/personal-calendar'),
+ 'PersonalCalendarPage',
+)
+const TeamCalendarPage: LazyLoadedComponent = lazyLoad(
+ () => import('./pages/team-calendar'),
+ 'TeamCalendarPage',
+)
+
+export const toolTitle: string = ToolTitle.calendar
+
+export const calendarRoutes: ReadonlyArray = [
+ {
+ authRequired: true,
+ children: [
+ {
+ element: ,
+ id: personalCalendarRouteId,
+ route: '',
+ title: 'Personal Calendar',
+ },
+ {
+ element: ,
+ id: teamCalendarRouteId,
+ route: 'team-calendar',
+ title: 'Team Calendar',
+ },
+ ],
+ domain: AppSubdomain.calendar,
+ element: ,
+ id: toolTitle,
+ rolesRequired: [UserRole.topcoderStaff, UserRole.administrator],
+ route: rootRoute,
+ title: toolTitle,
+ },
+]
diff --git a/src/apps/calendar/src/config/index.config.ts b/src/apps/calendar/src/config/index.config.ts
new file mode 100644
index 000000000..f1a930826
--- /dev/null
+++ b/src/apps/calendar/src/config/index.config.ts
@@ -0,0 +1,4 @@
+/**
+ * Common calendar config constants.
+ */
+export const APP_NAME = 'Calendar'
diff --git a/src/apps/calendar/src/config/routes.config.ts b/src/apps/calendar/src/config/routes.config.ts
new file mode 100644
index 000000000..194a42193
--- /dev/null
+++ b/src/apps/calendar/src/config/routes.config.ts
@@ -0,0 +1,9 @@
+import { AppSubdomain, EnvironmentConfig } from '~/config'
+
+export const rootRoute: string
+ = EnvironmentConfig.SUBDOMAIN === AppSubdomain.calendar
+ ? ''
+ : `/${AppSubdomain.calendar}`
+
+export const personalCalendarRouteId = 'personal-calendar'
+export const teamCalendarRouteId = 'team-calendar'
diff --git a/src/apps/calendar/src/index.ts b/src/apps/calendar/src/index.ts
new file mode 100644
index 000000000..393b0b665
--- /dev/null
+++ b/src/apps/calendar/src/index.ts
@@ -0,0 +1,2 @@
+export { calendarRoutes } from './calendar-app.routes'
+export { rootRoute as calendarRootRoute } from './config/routes.config'
diff --git a/src/apps/calendar/src/lib/components/Calendar/Calendar.module.scss b/src/apps/calendar/src/lib/components/Calendar/Calendar.module.scss
new file mode 100644
index 000000000..29891004a
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/Calendar/Calendar.module.scss
@@ -0,0 +1,116 @@
+.calendar {
+ position: relative;
+ background: #ffffff;
+ border: 1px solid var(--calendar-border);
+ border-radius: 12px;
+ padding: 16px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04);
+}
+
+.dayNames {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 8px;
+ margin-bottom: 12px;
+ color: #6c757d;
+ text-align: center;
+ font-size: 12px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.dayName {
+ padding: 4px 0;
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 8px;
+}
+
+.cell {
+ position: relative;
+ min-height: 88px;
+ width: 100%;
+ border-radius: 10px;
+ border: 1px solid var(--calendar-border);
+ background: var(--status-available);
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-end;
+ padding: 10px;
+ cursor: pointer;
+ transition: background-color 0.12s ease, transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
+ font-weight: 700;
+ color: #2d2d2d;
+}
+
+.cell:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
+ background: var(--calendar-hover);
+}
+
+.cell:disabled {
+ cursor: not-allowed;
+ opacity: 0.75;
+}
+
+.empty {
+ background: transparent;
+ border: none;
+ pointer-events: none;
+}
+
+.dateNumber {
+ font-size: 16px;
+ line-height: 1;
+}
+
+.status-available {
+ background: var(--status-available);
+}
+
+.status-leave {
+ background: var(--status-leave);
+ color: #ffffff;
+}
+
+.status-holiday {
+ background: var(--status-holiday);
+}
+
+.status-weekend {
+ background: var(--status-weekend);
+}
+
+.selected {
+ border-color: var(--status-selected);
+ box-shadow: 0 0 0 2px rgba(77, 171, 247, 0.3);
+}
+
+.loading {
+ pointer-events: none;
+}
+
+.loadingOverlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.7);
+ border-radius: 12px;
+}
+
+@media (max-width: 768px) {
+ .calendar {
+ padding: 12px;
+ }
+
+ .cell {
+ min-height: 72px;
+ }
+}
diff --git a/src/apps/calendar/src/lib/components/Calendar/Calendar.tsx b/src/apps/calendar/src/lib/components/Calendar/Calendar.tsx
new file mode 100644
index 000000000..6b2b9ff10
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/Calendar/Calendar.tsx
@@ -0,0 +1,124 @@
+import { MouseEvent, useMemo } from 'react'
+import classNames from 'classnames'
+
+import { LoadingSpinner } from '~/libs/ui'
+
+import { LeaveDate } from '../../models'
+import {
+ getDateKey,
+ getMonthDates,
+ getStatusColor,
+ getStatusForDate,
+} from '../../utils'
+
+import styles from './Calendar.module.scss'
+
+const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
+
+interface CalendarProps {
+ currentDate: Date
+ leaveDates: LeaveDate[]
+ selectedDates: Set
+ onDateClick: (dateKey: string) => void
+ isLoading: boolean
+}
+
+export const Calendar = (props: CalendarProps): JSX.Element => {
+ const currentDate = props.currentDate
+ const isLoading = props.isLoading
+ const leaveDates = props.leaveDates
+ const onDateClick = props.onDateClick
+ const selectedDates = props.selectedDates
+
+ const monthDates = useMemo(
+ () => getMonthDates(currentDate.getFullYear(), currentDate.getMonth()),
+ [currentDate],
+ )
+
+ const paddedDates = useMemo(() => {
+ if (!monthDates.length) {
+ return []
+ }
+
+ const padding = monthDates[0].getDay()
+ const cells: Array = []
+
+ for (let i = 0; i < padding; i += 1) {
+ cells.push(undefined)
+ }
+
+ cells.push(...monthDates)
+
+ while (cells.length % 7 !== 0) {
+ cells.push(undefined)
+ }
+
+ return cells
+ }, [monthDates])
+
+ function handleDateClick(event: MouseEvent): void {
+ const dateKey = event.currentTarget.dataset.dateKey
+
+ if (dateKey) {
+ onDateClick(dateKey)
+ }
+ }
+
+ return (
+
+
+ {dayNames.map(day => (
+
+ {day}
+
+ ))}
+
+
+
+ {paddedDates.map((date, index) => {
+ if (!date) {
+ return (
+
+ )
+ }
+
+ const dateKey = getDateKey(date)
+ const status = getStatusForDate(date, leaveDates)
+ const isSelected = selectedDates.has(dateKey)
+ const statusClass = styles[getStatusColor(status)]
+
+ return (
+
+ {date.getDate()}
+
+ )
+ })}
+
+
+ {isLoading && (
+
+
+
+ )}
+
+ )
+}
+
+export default Calendar
diff --git a/src/apps/calendar/src/lib/components/Calendar/index.ts b/src/apps/calendar/src/lib/components/Calendar/index.ts
new file mode 100644
index 000000000..8c50cf89e
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/Calendar/index.ts
@@ -0,0 +1 @@
+export * from './Calendar'
diff --git a/src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.module.scss b/src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.module.scss
new file mode 100644
index 000000000..4447f6d9d
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.module.scss
@@ -0,0 +1,42 @@
+.legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px 20px;
+ align-items: center;
+ margin: 8px 0 16px;
+}
+
+.item {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: #4b5563;
+}
+
+.color {
+ width: 14px;
+ height: 14px;
+ border-radius: 4px;
+ border: 1px solid var(--calendar-border);
+}
+
+.status-available {
+ background: var(--status-available);
+}
+
+.status-leave {
+ background: var(--status-leave);
+}
+
+.status-holiday {
+ background: var(--status-holiday);
+}
+
+.status-weekend {
+ background: var(--status-weekend);
+}
+
+.label {
+ white-space: nowrap;
+}
diff --git a/src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.tsx b/src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.tsx
new file mode 100644
index 000000000..375b0a04f
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/CalendarLegend/CalendarLegend.tsx
@@ -0,0 +1,19 @@
+import { FC } from 'react'
+import classNames from 'classnames'
+
+import { legendItems } from '../../utils'
+
+import styles from './CalendarLegend.module.scss'
+
+export const CalendarLegend: FC = () => (
+
+ {legendItems.map(item => (
+
+
+ {item.label}
+
+ ))}
+
+)
+
+export default CalendarLegend
diff --git a/src/apps/calendar/src/lib/components/CalendarLegend/index.ts b/src/apps/calendar/src/lib/components/CalendarLegend/index.ts
new file mode 100644
index 000000000..99e195112
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/CalendarLegend/index.ts
@@ -0,0 +1 @@
+export * from './CalendarLegend'
diff --git a/src/apps/calendar/src/lib/components/Layout/Layout.module.scss b/src/apps/calendar/src/lib/components/Layout/Layout.module.scss
new file mode 100644
index 000000000..25e66ab2f
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/Layout/Layout.module.scss
@@ -0,0 +1,47 @@
+@import '@libs/ui/styles/includes';
+
+.layout {
+ position: relative;
+ font-family: $font-roboto;
+ color: var(--Primary);
+ background: #ffffff;
+ border: 1px solid var(--BorderColor, #e0e0e0);
+ border-radius: 10px;
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
+ padding: $sp-5;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: $sp-3;
+ flex-wrap: wrap;
+ margin-bottom: $sp-4;
+}
+
+.headerActions {
+ display: flex;
+ align-items: center;
+ gap: $sp-2;
+
+ @include ltemd {
+ width: 100%;
+ justify-content: flex-start;
+ }
+}
+
+.main {
+ @include ltelg {
+ padding-top: $sp-3;
+ }
+}
+
+.contentLayoutOuter {
+ margin: $sp-6 auto !important;
+}
+
+.contentLayoutInner {
+ box-sizing: border-box;
+ width: 100%;
+}
diff --git a/src/apps/calendar/src/lib/components/Layout/Layout.tsx b/src/apps/calendar/src/lib/components/Layout/Layout.tsx
new file mode 100644
index 000000000..9e3a87b79
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/Layout/Layout.tsx
@@ -0,0 +1,61 @@
+import { FC, PropsWithChildren } from 'react'
+import { useLocation, useNavigate } from 'react-router-dom'
+
+import { Button, ContentLayout, IconOutline } from '~/libs/ui'
+
+import { rootRoute, teamCalendarRouteId } from '../../../config/routes.config'
+
+import styles from './Layout.module.scss'
+
+export const NullLayout: FC = props => (
+ <>{props.children}>
+)
+
+export const Layout: FC = props => {
+ const location = useLocation()
+ const navigate = useNavigate()
+
+ const buildPath = (...parts: string[]): string => {
+ const cleanedParts = parts
+ .filter(Boolean)
+ .map(part => part.replace(/^\/+|\/+$/g, ''))
+
+ return `/${cleanedParts.join('/')}` || '/'
+ }
+
+ const normalizedRootPath = rootRoute || ''
+ const teamCalendarPath = buildPath(normalizedRootPath, teamCalendarRouteId)
+ const personalCalendarPath = buildPath(normalizedRootPath)
+ const normalizedCurrentPath = location.pathname.replace(/\/+$/, '') || '/'
+ const isTeamCalendar = normalizedCurrentPath === teamCalendarPath
+ const buttonLabel = isTeamCalendar ? 'View My Calendar' : 'View Team Leave'
+ const buttonIcon = isTeamCalendar ? IconOutline.UserIcon : IconOutline.UsersIcon
+ const targetPath = isTeamCalendar ? personalCalendarPath : teamCalendarPath
+ function handleToggleCalendar(): void {
+ navigate(targetPath)
+ }
+
+ return (
+
+
+
+
+
+ {buttonLabel}
+
+
+
+
{props.children}
+
+
+ )
+}
+
+export default Layout
diff --git a/src/apps/calendar/src/lib/components/Layout/index.ts b/src/apps/calendar/src/lib/components/Layout/index.ts
new file mode 100644
index 000000000..19b84975d
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/Layout/index.ts
@@ -0,0 +1 @@
+export * from './Layout'
diff --git a/src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.module.scss b/src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.module.scss
new file mode 100644
index 000000000..488adf5d1
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.module.scss
@@ -0,0 +1,32 @@
+.navigation {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.navButton {
+ min-width: 44px;
+ height: 44px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.currentMonth {
+ font-size: 20px;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ color: #1f2933;
+}
+
+@media (max-width: 480px) {
+ .navigation {
+ gap: 8px;
+ }
+
+ .currentMonth {
+ font-size: 18px;
+ }
+}
diff --git a/src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.tsx b/src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.tsx
new file mode 100644
index 000000000..edf5c2d96
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/MonthNavigation/MonthNavigation.tsx
@@ -0,0 +1,36 @@
+import { FC } from 'react'
+
+import { Button, IconOutline } from '~/libs/ui'
+
+import { formatMonthYear } from '../../utils'
+
+import styles from './MonthNavigation.module.scss'
+
+interface MonthNavigationProps {
+ currentDate: Date
+ onNextMonth: () => void
+ onPrevMonth: () => void
+}
+
+export const MonthNavigation: FC = (props: MonthNavigationProps) => (
+
+
+
{formatMonthYear(props.currentDate)}
+
+
+)
+
+export default MonthNavigation
diff --git a/src/apps/calendar/src/lib/components/MonthNavigation/index.ts b/src/apps/calendar/src/lib/components/MonthNavigation/index.ts
new file mode 100644
index 000000000..86e180167
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/MonthNavigation/index.ts
@@ -0,0 +1 @@
+export * from './MonthNavigation'
diff --git a/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.module.scss b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.module.scss
new file mode 100644
index 000000000..d0ff3d3b8
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.module.scss
@@ -0,0 +1,151 @@
+@import '@libs/ui/styles/includes';
+
+.teamCalendar {
+ position: relative;
+ background: #ffffff;
+ border: 1px solid var(--calendar-border);
+ border-radius: 12px;
+ padding: 16px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04);
+}
+
+.dayNames {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 8px;
+ margin-bottom: 12px;
+ color: #6c757d;
+ text-align: center;
+ font-size: 12px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.dayName {
+ padding: 4px 0;
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 10px;
+}
+
+.cell {
+ position: relative;
+ min-height: 120px;
+ width: 100%;
+ border-radius: 10px;
+ border: 1px solid var(--team-cell-border, #e5e7eb);
+ background: #f9fafb;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 12px;
+ gap: 8px;
+ transition: background-color 0.12s ease, transform 0.12s ease, box-shadow 0.12s ease;
+}
+
+.cell:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 8px 18px rgba(0, 0, 0, 0.05);
+ background: #f3f4f6;
+}
+
+.empty {
+ background: transparent;
+ border: none;
+ pointer-events: none;
+}
+
+.weekend {
+ background: #f0f7ff;
+ border-color: #dbeafe;
+}
+
+.dateNumber {
+ align-self: flex-end;
+ font-size: 16px;
+ font-weight: 800;
+ color: #111827;
+ line-height: 1;
+}
+
+.userList {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ width: 100%;
+}
+
+.userItem {
+ width: 100%;
+ border-radius: 8px;
+ padding: 8px 10px;
+ font-weight: 700;
+ font-size: 14px;
+ line-height: 1.2;
+ border: 1px solid transparent;
+}
+
+.userLeave {
+ background: var(--user-leave-bg, #fee2e2);
+ border-color: #fecdd3;
+ color: #9b1c1c;
+}
+
+.userHoliday {
+ background: var(--user-holiday-bg, #fef3c7);
+ border-color: #fde68a;
+ color: #92400e;
+}
+
+.emptyState {
+ color: #6b7280;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.overflowIndicator {
+ color: #374151;
+ font-size: 13px;
+ font-weight: 700;
+}
+
+.loading {
+ pointer-events: none;
+}
+
+.loadingOverlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.7);
+ border-radius: 12px;
+}
+
+@media (max-width: 768px) {
+ .teamCalendar {
+ padding: 12px;
+ }
+
+ .grid {
+ gap: 8px;
+ }
+
+ .cell {
+ min-height: 96px;
+ padding: 10px;
+ }
+
+ .userItem {
+ font-size: 13px;
+ }
+
+ .dateNumber {
+ font-size: 15px;
+ }
+}
diff --git a/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx
new file mode 100644
index 000000000..723be6b0c
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.tsx
@@ -0,0 +1,154 @@
+import { isWeekend } from 'date-fns'
+import { FC, useMemo } from 'react'
+import classNames from 'classnames'
+
+import { LoadingSpinner } from '~/libs/ui'
+
+import { LeaveStatus, TeamLeaveDate } from '../../models'
+import { getDateKey, getMonthDates } from '../../utils'
+
+import styles from './TeamCalendar.module.scss'
+
+const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
+type TeamLeaveUser = TeamLeaveDate['usersOnLeave'][number]
+
+const getUserDisplayName = (user: TeamLeaveUser): string => {
+ const firstName = user.firstName?.trim()
+ const lastName = user.lastName?.trim()
+ const fullName = [firstName, lastName]
+ .filter(Boolean)
+ .join(' ')
+
+ return fullName || user.handle || user.userId
+}
+
+const compareUsersByName = (userA: TeamLeaveUser, userB: TeamLeaveUser): number => {
+ const firstNameA = userA.firstName?.trim() || getUserDisplayName(userA)
+ const firstNameB = userB.firstName?.trim() || getUserDisplayName(userB)
+ const firstNameCompare = firstNameA.localeCompare(firstNameB, undefined, { sensitivity: 'base' })
+
+ if (firstNameCompare !== 0) {
+ return firstNameCompare
+ }
+
+ const lastNameA = userA.lastName?.trim() ?? ''
+ const lastNameB = userB.lastName?.trim() ?? ''
+ const lastNameCompare = lastNameA.localeCompare(lastNameB, undefined, { sensitivity: 'base' })
+
+ if (lastNameCompare !== 0) {
+ return lastNameCompare
+ }
+
+ return (userA.userId ?? '').localeCompare(userB.userId ?? '', undefined, { sensitivity: 'base' })
+}
+
+interface TeamCalendarProps {
+ currentDate: Date
+ teamLeaveDates: TeamLeaveDate[]
+ isLoading: boolean
+}
+
+export const TeamCalendar: FC = (props: TeamCalendarProps) => {
+ const currentDate = props.currentDate
+ const isLoading = props.isLoading
+ const teamLeaveDates = props.teamLeaveDates
+
+ const monthDates = useMemo(
+ () => getMonthDates(currentDate.getFullYear(), currentDate.getMonth()),
+ [currentDate],
+ )
+
+ const paddedDates = useMemo(() => {
+ if (!monthDates.length) {
+ return []
+ }
+
+ const padding = monthDates[0].getDay()
+ const cells: Array = []
+
+ for (let i = 0; i < padding; i += 1) {
+ cells.push(undefined)
+ }
+
+ cells.push(...monthDates)
+
+ while (cells.length % 7 !== 0) {
+ cells.push(undefined)
+ }
+
+ return cells
+ }, [monthDates])
+
+ return (
+
+
+ {dayNames.map(day => (
+
+ {day}
+
+ ))}
+
+
+
+ {paddedDates.map((date, index) => {
+ if (!date) {
+ return (
+
+ )
+ }
+
+ const dateKey = getDateKey(date)
+ const leaveEntry = teamLeaveDates.find(item => item.date === dateKey)
+ const users = leaveEntry?.usersOnLeave ?? []
+ const sortedUsers = [...users].sort(compareUsersByName)
+ const displayedUsers = sortedUsers.slice(0, 10)
+ const overflowCount = sortedUsers.length - displayedUsers.length
+ const weekendClass = isWeekend(date) ? styles.weekend : undefined
+
+ return (
+
+
{date.getDate()}
+
+ {displayedUsers.length > 0
+ && displayedUsers.map((user, userIndex) => (
+
+ {getUserDisplayName(user)}
+
+ ))}
+ {overflowCount > 0 && (
+
+ {`+${overflowCount} more`}
+
+ )}
+
+
+ )
+ })}
+
+
+ {isLoading && (
+
+
+
+ )}
+
+ )
+}
+
+export default TeamCalendar
diff --git a/src/apps/calendar/src/lib/components/TeamCalendar/index.ts b/src/apps/calendar/src/lib/components/TeamCalendar/index.ts
new file mode 100644
index 000000000..1aa33e31a
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/TeamCalendar/index.ts
@@ -0,0 +1 @@
+export * from './TeamCalendar'
diff --git a/src/apps/calendar/src/lib/components/index.ts b/src/apps/calendar/src/lib/components/index.ts
new file mode 100644
index 000000000..705dd2af7
--- /dev/null
+++ b/src/apps/calendar/src/lib/components/index.ts
@@ -0,0 +1,5 @@
+export * from './Layout'
+export * from './Calendar'
+export * from './MonthNavigation'
+export * from './CalendarLegend'
+export * from './TeamCalendar'
diff --git a/src/apps/calendar/src/lib/contexts/CalendarContext.ts b/src/apps/calendar/src/lib/contexts/CalendarContext.ts
new file mode 100644
index 000000000..d9611437b
--- /dev/null
+++ b/src/apps/calendar/src/lib/contexts/CalendarContext.ts
@@ -0,0 +1,9 @@
+import { createContext } from 'react'
+
+import type { CalendarContextModel } from '../models'
+
+export const CalendarContext = createContext({
+ loginUserInfo: undefined,
+})
+
+export default CalendarContext
diff --git a/src/apps/calendar/src/lib/contexts/CalendarContextProvider.tsx b/src/apps/calendar/src/lib/contexts/CalendarContextProvider.tsx
new file mode 100644
index 000000000..550987623
--- /dev/null
+++ b/src/apps/calendar/src/lib/contexts/CalendarContextProvider.tsx
@@ -0,0 +1,40 @@
+import {
+ FC,
+ PropsWithChildren,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react'
+
+import { tokenGetAsync, TokenModel } from '~/libs/core'
+
+import type { CalendarContextModel } from '../models'
+
+import { CalendarContext } from './CalendarContext'
+
+export const CalendarContextProvider: FC = props => {
+ const [loginUserInfo, setLoginUserInfo] = useState(undefined)
+
+ const value = useMemo(
+ () => ({
+ loginUserInfo,
+ }),
+ [loginUserInfo],
+ )
+
+ useEffect(() => {
+ tokenGetAsync()
+ .then((token: TokenModel) => setLoginUserInfo(token))
+ .catch(() => {
+ // no-op, consumer can handle missing token
+ })
+ }, [])
+
+ return (
+
+ {props.children}
+
+ )
+}
+
+export default CalendarContextProvider
diff --git a/src/apps/calendar/src/lib/contexts/SWRConfigProvider.tsx b/src/apps/calendar/src/lib/contexts/SWRConfigProvider.tsx
new file mode 100644
index 000000000..c9efac909
--- /dev/null
+++ b/src/apps/calendar/src/lib/contexts/SWRConfigProvider.tsx
@@ -0,0 +1,19 @@
+import { FC, PropsWithChildren } from 'react'
+import { SWRConfig } from 'swr'
+
+import { xhrGetAsync } from '~/libs/core'
+
+export const SWRConfigProvider: FC = props => (
+ xhrGetAsync(resource),
+ refreshInterval: 0,
+ revalidateOnFocus: false,
+ revalidateOnMount: true,
+ }}
+ >
+ {props.children}
+
+)
+
+export default SWRConfigProvider
diff --git a/src/apps/calendar/src/lib/contexts/index.ts b/src/apps/calendar/src/lib/contexts/index.ts
new file mode 100644
index 000000000..b2bdb3318
--- /dev/null
+++ b/src/apps/calendar/src/lib/contexts/index.ts
@@ -0,0 +1,3 @@
+export * from './CalendarContext'
+export * from './CalendarContextProvider'
+export * from './SWRConfigProvider'
diff --git a/src/apps/calendar/src/lib/hooks/index.ts b/src/apps/calendar/src/lib/hooks/index.ts
new file mode 100644
index 000000000..19778633f
--- /dev/null
+++ b/src/apps/calendar/src/lib/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from './useFetchLeaveDates'
+export * from './useFetchTeamLeave'
diff --git a/src/apps/calendar/src/lib/hooks/useFetchLeaveDates.ts b/src/apps/calendar/src/lib/hooks/useFetchLeaveDates.ts
new file mode 100644
index 000000000..7a445f68d
--- /dev/null
+++ b/src/apps/calendar/src/lib/hooks/useFetchLeaveDates.ts
@@ -0,0 +1,105 @@
+import { useCallback, useRef, useState } from 'react'
+
+import { handleError } from '~/libs/shared'
+
+import { LeaveDate, LeaveUpdateStatus } from '../models'
+import { fetchUserLeaveDates, setLeaveDates as setLeaveDatesService } from '../services'
+import { getDateKey } from '../utils'
+
+export interface UseFetchLeaveDatesResult {
+ leaveDates: LeaveDate[]
+ isLoading: boolean
+ isUpdating: boolean
+ error: unknown
+ loadLeaveDates: (startDate?: Date, endDate?: Date) => Promise
+ updateLeaveDates: (dates: string[], status: LeaveUpdateStatus) => Promise
+}
+
+const buildRequestKey = (...parts: Array): string => (
+ parts
+ .filter(Boolean)
+ .join('|')
+)
+
+export function useFetchLeaveDates(): UseFetchLeaveDatesResult {
+ const [leaveDates, setLeaveDates] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [isUpdating, setIsUpdating] = useState(false)
+ const [error, setError] = useState(undefined)
+ const latestLoadRequestRef = useRef('')
+ const latestUpdateRequestRef = useRef('')
+
+ const loadLeaveDates = useCallback(
+ async (startDate?: Date, endDate?: Date) => {
+ const startKey = startDate ? getDateKey(startDate) : undefined
+ const endKey = endDate ? getDateKey(endDate) : undefined
+ const requestKey = buildRequestKey(
+ startKey,
+ endKey,
+ Date.now()
+ .toString(),
+ )
+ latestLoadRequestRef.current = requestKey
+ setIsLoading(true)
+ setError(undefined)
+
+ try {
+ const response = await fetchUserLeaveDates(startKey, endKey)
+ if (latestLoadRequestRef.current !== requestKey) {
+ return
+ }
+
+ setLeaveDates(response)
+ } catch (err) {
+ if (latestLoadRequestRef.current === requestKey) {
+ setError(err)
+ handleError(err)
+ throw err
+ }
+ } finally {
+ if (latestLoadRequestRef.current === requestKey) {
+ setIsLoading(false)
+ }
+ }
+ },
+ [],
+ )
+
+ const updateLeaveDates = useCallback(
+ async (dates: string[], status: LeaveUpdateStatus) => {
+ const requestKey = buildRequestKey(
+ status,
+ dates.join(','),
+ Date.now()
+ .toString(),
+ )
+ latestUpdateRequestRef.current = requestKey
+ setIsUpdating(true)
+ setError(undefined)
+
+ try {
+ await setLeaveDatesService(dates, status)
+ } catch (err) {
+ if (latestUpdateRequestRef.current === requestKey) {
+ setError(err)
+ handleError(err)
+ throw err
+ }
+ } finally {
+ if (latestUpdateRequestRef.current === requestKey) {
+ setIsUpdating(false)
+ }
+ }
+ },
+ [],
+ )
+
+ return {
+ error,
+ isLoading,
+ isUpdating,
+ leaveDates,
+ loadLeaveDates,
+ updateLeaveDates,
+ }
+}
diff --git a/src/apps/calendar/src/lib/hooks/useFetchTeamLeave.ts b/src/apps/calendar/src/lib/hooks/useFetchTeamLeave.ts
new file mode 100644
index 000000000..6446241fc
--- /dev/null
+++ b/src/apps/calendar/src/lib/hooks/useFetchTeamLeave.ts
@@ -0,0 +1,71 @@
+import { useCallback, useRef, useState } from 'react'
+
+import { handleError } from '~/libs/shared'
+
+import { TeamLeaveDate } from '../models'
+import { fetchTeamLeave } from '../services'
+import { getDateKey } from '../utils'
+
+export interface UseFetchTeamLeaveResult {
+ teamLeaveDates: TeamLeaveDate[]
+ isLoading: boolean
+ error: unknown
+ loadTeamLeave: (startDate?: Date, endDate?: Date) => Promise
+}
+
+const buildRequestKey = (...parts: Array): string => (
+ parts
+ .filter(Boolean)
+ .join('|')
+)
+
+export function useFetchTeamLeave(): UseFetchTeamLeaveResult {
+ const [teamLeaveDates, setTeamLeaveDates] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(undefined)
+ const latestLoadRequestRef = useRef('')
+
+ const loadTeamLeave = useCallback(
+ async (startDate?: Date, endDate?: Date) => {
+ const startKey = startDate ? getDateKey(startDate) : undefined
+ const endKey = endDate ? getDateKey(endDate) : undefined
+ const requestKey = buildRequestKey(
+ startKey,
+ endKey,
+ Date.now()
+ .toString(),
+ )
+ latestLoadRequestRef.current = requestKey
+ setIsLoading(true)
+ setTeamLeaveDates([])
+ setError(undefined)
+
+ try {
+ const response = await fetchTeamLeave(startKey, endKey)
+ if (latestLoadRequestRef.current !== requestKey) {
+ return
+ }
+
+ setTeamLeaveDates(response)
+ } catch (err) {
+ if (latestLoadRequestRef.current === requestKey) {
+ setError(err)
+ handleError(err)
+ throw err
+ }
+ } finally {
+ if (latestLoadRequestRef.current === requestKey) {
+ setIsLoading(false)
+ }
+ }
+ },
+ [],
+ )
+
+ return {
+ error,
+ isLoading,
+ loadTeamLeave,
+ teamLeaveDates,
+ }
+}
diff --git a/src/apps/calendar/src/lib/index.ts b/src/apps/calendar/src/lib/index.ts
new file mode 100644
index 000000000..d5dc44d56
--- /dev/null
+++ b/src/apps/calendar/src/lib/index.ts
@@ -0,0 +1,6 @@
+export * from './components'
+export * from './contexts'
+export * from './services'
+export * from './models'
+export * from './hooks'
+export * from './utils'
diff --git a/src/apps/calendar/src/lib/models/index.ts b/src/apps/calendar/src/lib/models/index.ts
new file mode 100644
index 000000000..e993d3773
--- /dev/null
+++ b/src/apps/calendar/src/lib/models/index.ts
@@ -0,0 +1,30 @@
+import { TokenModel } from '~/libs/core'
+
+export enum LeaveStatus {
+ AVAILABLE = 'AVAILABLE',
+ LEAVE = 'LEAVE',
+ WEEKEND = 'WEEKEND',
+ WIPRO_HOLIDAY = 'WIPRO_HOLIDAY',
+}
+
+export type LeaveUpdateStatus = LeaveStatus.AVAILABLE | LeaveStatus.LEAVE
+
+export interface LeaveDate {
+ date: string
+ status: LeaveStatus
+}
+
+export interface TeamLeaveDate {
+ date: string
+ usersOnLeave: Array<{
+ userId: string
+ handle?: string
+ firstName?: string
+ lastName?: string
+ status: LeaveStatus.LEAVE | LeaveStatus.WIPRO_HOLIDAY
+ }>
+}
+
+export interface CalendarContextModel {
+ loginUserInfo?: TokenModel
+}
diff --git a/src/apps/calendar/src/lib/services/index.ts b/src/apps/calendar/src/lib/services/index.ts
new file mode 100644
index 000000000..f5ff03efd
--- /dev/null
+++ b/src/apps/calendar/src/lib/services/index.ts
@@ -0,0 +1 @@
+export * from './leave.service'
diff --git a/src/apps/calendar/src/lib/services/leave.service.ts b/src/apps/calendar/src/lib/services/leave.service.ts
new file mode 100644
index 000000000..4169e8138
--- /dev/null
+++ b/src/apps/calendar/src/lib/services/leave.service.ts
@@ -0,0 +1,34 @@
+import qs from 'qs'
+
+import { EnvironmentConfig } from '~/config'
+import { xhrGetAsync, xhrPostAsync } from '~/libs/core'
+
+import type { LeaveDate, LeaveUpdateStatus, TeamLeaveDate } from '../models'
+
+const serializeQuery = (params: Record): string => qs.stringify(
+ params,
+ { addQueryPrefix: true, skipNulls: true },
+)
+
+export const fetchUserLeaveDates = async (
+ startDate?: string,
+ endDate?: string,
+): Promise => {
+ const queryString = serializeQuery({ endDate, startDate })
+
+ return xhrGetAsync(`${EnvironmentConfig.API.V6}/leave/dates${queryString}`)
+}
+
+export const setLeaveDates = async (
+ dates: string[],
+ status: LeaveUpdateStatus,
+): Promise => xhrPostAsync(`${EnvironmentConfig.API.V6}/leave/dates`, { dates, status })
+
+export const fetchTeamLeave = async (
+ startDate?: string,
+ endDate?: string,
+): Promise => {
+ const queryString = serializeQuery({ endDate, startDate })
+
+ return xhrGetAsync(`${EnvironmentConfig.API.V6}/leave/team${queryString}`)
+}
diff --git a/src/apps/calendar/src/lib/styles/index.scss b/src/apps/calendar/src/lib/styles/index.scss
new file mode 100644
index 000000000..6c1ffcaa9
--- /dev/null
+++ b/src/apps/calendar/src/lib/styles/index.scss
@@ -0,0 +1,37 @@
+@import '@libs/ui/styles/includes';
+
+:root {
+ --LeaveColor: #4caf50;
+ --AvailableColor: #e8f5e9;
+ --HolidayColor: #ffc107;
+ --WeekendColor: #f5f5f5;
+ --BorderColor: #e0e0e0;
+ --user-leave-bg: #fee2e2;
+ --user-holiday-bg: #fef3c7;
+ --team-cell-border: #e5e7eb;
+}
+
+.calendar-app {
+ --status-available: #f0f0f0;
+ --status-leave: #ff6b6b;
+ --status-holiday: #ffd93d;
+ --status-weekend: #a8dadc;
+ --status-selected: #4dabf7;
+ --calendar-border: #dee2e6;
+ --calendar-hover: #e9ecef;
+
+ // App-specific global styles
+}
+
+.primaryButton {
+ background-color: var(--LeaveColor);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px 16px;
+ cursor: pointer;
+
+ &:hover {
+ opacity: 0.9;
+ }
+}
diff --git a/src/apps/calendar/src/lib/utils/calendar.utils.ts b/src/apps/calendar/src/lib/utils/calendar.utils.ts
new file mode 100644
index 000000000..b1e6fd505
--- /dev/null
+++ b/src/apps/calendar/src/lib/utils/calendar.utils.ts
@@ -0,0 +1,56 @@
+import { eachDayOfInterval, endOfMonth, format, startOfMonth } from 'date-fns'
+
+import { LeaveDate, LeaveStatus } from '../models'
+
+const statusColorMap: Record = {
+ [LeaveStatus.LEAVE]: 'status-leave',
+ [LeaveStatus.WIPRO_HOLIDAY]: 'status-holiday',
+ [LeaveStatus.WEEKEND]: 'status-weekend',
+ [LeaveStatus.AVAILABLE]: 'status-available',
+}
+
+const statusLabelMap: Record = {
+ [LeaveStatus.LEAVE]: 'Leave',
+ [LeaveStatus.WIPRO_HOLIDAY]: 'Wipro Holiday',
+ [LeaveStatus.WEEKEND]: 'Weekend',
+ [LeaveStatus.AVAILABLE]: 'Available',
+}
+
+export const legendStatusOrder: LeaveStatus[] = [
+ LeaveStatus.AVAILABLE,
+ LeaveStatus.LEAVE,
+ LeaveStatus.WIPRO_HOLIDAY,
+ LeaveStatus.WEEKEND,
+]
+
+export const getMonthDates = (year: number, month: number): Date[] => {
+ const monthDate = new Date(year, month, 1)
+
+ return eachDayOfInterval({
+ end: endOfMonth(monthDate),
+ start: startOfMonth(monthDate),
+ })
+}
+
+export const formatMonthYear = (date: Date): string => format(date, 'LLLL yyyy')
+
+export const getDateKey = (date: Date): string => format(date, 'yyyy-MM-dd')
+
+export const getStatusForDate = (date: Date, leaveDates: LeaveDate[]): LeaveStatus => {
+ const dateKey = getDateKey(date)
+ const match = leaveDates.find(item => item.date === dateKey)
+
+ return match?.status ?? LeaveStatus.AVAILABLE
+}
+
+export const getStatusColor = (status: LeaveStatus): string => (
+ statusColorMap[status] ?? statusColorMap[LeaveStatus.AVAILABLE]
+)
+
+export const getStatusLabel = (status: LeaveStatus): string => statusLabelMap[status]
+
+export const legendItems = legendStatusOrder.map(status => ({
+ label: getStatusLabel(status),
+ status,
+ statusClass: getStatusColor(status),
+}))
diff --git a/src/apps/calendar/src/lib/utils/index.ts b/src/apps/calendar/src/lib/utils/index.ts
new file mode 100644
index 000000000..91a1467b7
--- /dev/null
+++ b/src/apps/calendar/src/lib/utils/index.ts
@@ -0,0 +1 @@
+export * from './calendar.utils'
diff --git a/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.module.scss b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.module.scss
new file mode 100644
index 000000000..e4a32af5d
--- /dev/null
+++ b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.module.scss
@@ -0,0 +1,77 @@
+.page {
+ max-width: 1080px;
+ margin: 0 auto;
+ padding: 24px 16px 48px;
+}
+
+.header {
+ margin-bottom: 12px;
+}
+
+.subtitle {
+ margin: 0;
+ font-size: 14px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: #6b7280;
+}
+
+.title {
+ margin: 2px 0 0;
+ font-size: 28px;
+ font-weight: 800;
+ color: #111827;
+}
+
+.navigation {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ margin: 12px 0 8px;
+}
+
+.calendarSection {
+ margin: 0 auto;
+}
+
+.actions {
+ margin-top: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.actionButtons {
+ display: inline-flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.selectionInfo {
+ font-weight: 700;
+ color: #374151;
+}
+
+.error {
+ margin-top: 12px;
+ padding: 12px 16px;
+ background: #fff5f5;
+ border: 1px solid #fda4af;
+ border-radius: 8px;
+ color: #b91c1c;
+ font-weight: 600;
+}
+
+@media (max-width: 720px) {
+ .actions {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .selectionInfo {
+ order: 2;
+ }
+}
diff --git a/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx
new file mode 100644
index 000000000..f0a70c87b
--- /dev/null
+++ b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx
@@ -0,0 +1,154 @@
+import { addMonths, endOfMonth, startOfMonth, subMonths } from 'date-fns'
+import { FC, useCallback, useContext, useEffect, useMemo, useState } from 'react'
+
+import { Button } from '~/libs/ui'
+
+import { Calendar, CalendarLegend, MonthNavigation } from '../../lib/components'
+import { CalendarContext } from '../../lib/contexts/CalendarContext'
+import { useFetchLeaveDates } from '../../lib/hooks'
+import { LeaveStatus } from '../../lib/models'
+
+import styles from './PersonalCalendarPage.module.scss'
+
+const PersonalCalendarPage: FC = () => {
+ const calendarContext = useContext(CalendarContext)
+ const [currentDate, setCurrentDate] = useState(new Date())
+ const [selectedDates, setSelectedDates] = useState>(new Set())
+ const leaveDatesState = useFetchLeaveDates()
+ const error = leaveDatesState.error
+ const isLoading = leaveDatesState.isLoading
+ const isUpdating = leaveDatesState.isUpdating
+ const leaveDates = leaveDatesState.leaveDates
+ const loadLeaveDates = leaveDatesState.loadLeaveDates
+ const updateLeaveDates = leaveDatesState.updateLeaveDates
+ const [actionError, setActionError] = useState('')
+
+ const loadCurrentMonth = useCallback(async () => {
+ setActionError('')
+ try {
+ await loadLeaveDates(
+ startOfMonth(currentDate),
+ endOfMonth(currentDate),
+ )
+ } catch {
+ setActionError('Unable to load leave dates. Please try again.')
+ }
+ }, [currentDate, loadLeaveDates])
+
+ useEffect(() => {
+ loadCurrentMonth()
+ }, [loadCurrentMonth])
+
+ const handlePrevMonth = useCallback(() => {
+ setSelectedDates(new Set())
+ setCurrentDate(prev => subMonths(prev, 1))
+ }, [])
+
+ const handleNextMonth = useCallback(() => {
+ setSelectedDates(new Set())
+ setCurrentDate(prev => addMonths(prev, 1))
+ }, [])
+
+ const handleDateClick = useCallback((dateKey: string) => {
+ setSelectedDates(prev => {
+ const next = new Set(prev)
+ if (next.has(dateKey)) {
+ next.delete(dateKey)
+ } else {
+ next.add(dateKey)
+ }
+
+ return next
+ })
+ }, [])
+
+ const handleSetAsLeave = useCallback(async () => {
+ if (!selectedDates.size) return
+
+ setActionError('')
+ try {
+ await updateLeaveDates(Array.from(selectedDates), LeaveStatus.LEAVE)
+ setSelectedDates(new Set())
+ await loadCurrentMonth()
+ } catch {
+ setActionError('Unable to update leave dates. Please try again.')
+ }
+ }, [loadCurrentMonth, selectedDates, updateLeaveDates])
+
+ const handleSetAsAvailable = useCallback(async () => {
+ if (!selectedDates.size) return
+
+ setActionError('')
+ try {
+ await updateLeaveDates(Array.from(selectedDates), LeaveStatus.AVAILABLE)
+ setSelectedDates(new Set())
+ await loadCurrentMonth()
+ } catch {
+ setActionError('Unable to update leave dates. Please try again.')
+ }
+ }, [loadCurrentMonth, selectedDates, updateLeaveDates])
+
+ const selectionLabel = useMemo(() => {
+ const count = selectedDates.size
+ if (!count) return 'No dates selected'
+ return `${count} date${count > 1 ? 's' : ''} selected`
+ }, [selectedDates])
+
+ const errorMessage = actionError || (error ? 'Something went wrong. Please try again.' : '')
+
+ return (
+
+
+ Welcome back
+
+ {calendarContext.loginUserInfo?.handle ?? 'Your calendar'}
+
+
+
+
+
+
+
+
+
+
+ Set as Leave
+
+
+ Set as Available
+
+
+ {selectionLabel}
+
+
+ {errorMessage && (
+
{errorMessage}
+ )}
+
+ )
+}
+
+export default PersonalCalendarPage
diff --git a/src/apps/calendar/src/pages/personal-calendar/index.ts b/src/apps/calendar/src/pages/personal-calendar/index.ts
new file mode 100644
index 000000000..5141e08f7
--- /dev/null
+++ b/src/apps/calendar/src/pages/personal-calendar/index.ts
@@ -0,0 +1 @@
+export { default as PersonalCalendarPage } from './PersonalCalendarPage'
diff --git a/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.module.scss b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.module.scss
new file mode 100644
index 000000000..4735ddd7f
--- /dev/null
+++ b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.module.scss
@@ -0,0 +1,46 @@
+.page {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 24px 16px 48px;
+}
+
+.header {
+ margin-bottom: 8px;
+}
+
+.title {
+ margin: 0;
+ font-size: 28px;
+ font-weight: 800;
+ color: #111827;
+}
+
+.navigation {
+ display: flex;
+ justify-content: center;
+ margin: 14px 0 10px;
+}
+
+.calendarSection {
+ margin: 0 auto;
+}
+
+.error {
+ margin-top: 12px;
+ padding: 12px 16px;
+ background: #fff5f5;
+ border: 1px solid #fda4af;
+ border-radius: 8px;
+ color: #b91c1c;
+ font-weight: 600;
+}
+
+@media (max-width: 720px) {
+ .page {
+ padding: 20px 12px 32px;
+ }
+
+ .title {
+ font-size: 24px;
+ }
+}
diff --git a/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx
new file mode 100644
index 000000000..ab50c6d28
--- /dev/null
+++ b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx
@@ -0,0 +1,77 @@
+import { addMonths, endOfMonth, startOfMonth, subMonths } from 'date-fns'
+import { FC, useCallback, useEffect, useMemo, useState } from 'react'
+
+import { MonthNavigation, TeamCalendar } from '../../lib/components'
+import { useFetchTeamLeave } from '../../lib/hooks'
+
+import styles from './TeamCalendarPage.module.scss'
+
+const TeamCalendarPage: FC = () => {
+ const [currentDate, setCurrentDate] = useState(new Date())
+ const [actionError, setActionError] = useState('')
+ const teamLeaveState = useFetchTeamLeave()
+ const error = teamLeaveState.error
+ const isLoading = teamLeaveState.isLoading
+ const loadTeamLeave = teamLeaveState.loadTeamLeave
+ const teamLeaveDates = teamLeaveState.teamLeaveDates
+
+ const loadCurrentMonth = useCallback(async () => {
+ setActionError('')
+ try {
+ await loadTeamLeave(
+ startOfMonth(currentDate),
+ endOfMonth(currentDate),
+ )
+ } catch {
+ setActionError('Unable to load team leave. Please try again.')
+ }
+ }, [currentDate, loadTeamLeave])
+
+ useEffect(() => {
+ loadCurrentMonth()
+ }, [loadCurrentMonth])
+
+ const handlePrevMonth = useCallback(() => {
+ setCurrentDate(prev => subMonths(prev, 1))
+ }, [])
+
+ const handleNextMonth = useCallback(() => {
+ setCurrentDate(prev => addMonths(prev, 1))
+ }, [])
+
+ const errorMessage = useMemo(() => {
+ if (actionError) return actionError
+ if (error) return 'Something went wrong. Please try again.'
+ return ''
+ }, [actionError, error])
+
+ return (
+
+
+
+
+
+
+
+ {errorMessage && (
+
{errorMessage}
+ )}
+
+ )
+}
+
+export default TeamCalendarPage
diff --git a/src/apps/calendar/src/pages/team-calendar/index.ts b/src/apps/calendar/src/pages/team-calendar/index.ts
new file mode 100644
index 000000000..be8c78227
--- /dev/null
+++ b/src/apps/calendar/src/pages/team-calendar/index.ts
@@ -0,0 +1 @@
+export { default as TeamCalendarPage } from './TeamCalendarPage'
diff --git a/src/apps/onboarding/src/pages/onboarding/index.tsx b/src/apps/onboarding/src/pages/onboarding/index.tsx
index 55447227d..525fd7485 100644
--- a/src/apps/onboarding/src/pages/onboarding/index.tsx
+++ b/src/apps/onboarding/src/pages/onboarding/index.tsx
@@ -1,12 +1,10 @@
import { FC, useContext, useEffect } from 'react'
import { Outlet, Routes, useLocation } from 'react-router-dom'
-import { Provider, useDispatch, useSelector } from 'react-redux'
+import { Provider, useDispatch } from 'react-redux'
import classNames from 'classnames'
import { routerContext, RouterContextData } from '~/libs/core'
-import { Member } from '~/apps/talent-search/src/lib/models'
import { SharedSwrConfig } from '~/libs/shared'
-import { EnvironmentConfig } from '~/config'
import { onboardRouteId } from '../../onboarding.routes'
import { fetchMemberInfo, fetchMemberTraits } from '../../redux/actions/member'
@@ -20,7 +18,6 @@ const OnboardingContent: FC<{
const { getChildRoutes }: RouterContextData = useContext(routerContext)
const location = useLocation()
const dispatch = useDispatch()
- const reduxMemberInfo: Member = useSelector((state: any) => state.member.memberInfo)
useEffect(() => {
dispatch(fetchMemberInfo())
@@ -43,11 +40,6 @@ const OnboardingContent: FC<{
{getChildRoutes(onboardRouteId)}
-