diff --git a/projecthearthstone.in/Courses/ASL course/Tree-graphic.png b/projecthearthstone.in/Courses/ASL course/Tree-graphic.png
new file mode 100644
index 0000000..11a4b7b
Binary files /dev/null and b/projecthearthstone.in/Courses/ASL course/Tree-graphic.png differ
diff --git a/projecthearthstone.in/Courses/ASL course/course-data.json b/projecthearthstone.in/Courses/ASL course/course-data.json
new file mode 100644
index 0000000..e376b58
--- /dev/null
+++ b/projecthearthstone.in/Courses/ASL course/course-data.json
@@ -0,0 +1,144 @@
+{
+ "course": {
+ "title": "ASL Mastery",
+ "description": "American Sign Language from foundations to fluency"
+ },
+ "sections": [
+ {
+ "id": 1,
+ "key": "foundations",
+ "label": "Foundations",
+ "theme": "emerald",
+ "color": "#22dd88",
+ "portalColor": "34,221,136",
+ "description": "The core building blocks of ASL, including history, handshape, posture, fingerspelling, and expression.",
+ "videos": [
+ { "id": "v1_1", "title": "What is ASL?", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v1_2", "title": "History of ASL", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v1_3", "title": "Deaf Culture and Community", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v1_4", "title": "How to Hold Your Hands and Body Posture", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v1_5", "title": "Fingerspelling A–M", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v1_6", "title": "Fingerspelling N–Z", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v1_7", "title": "Numbers 1–100", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v1_8", "title": "Eye Contact and Facial Expressions", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v1_9", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true }
+ ]
+ },
+ {
+ "id": 2,
+ "key": "everyday-vocab",
+ "label": "Everyday Vocab",
+ "theme": "frost",
+ "color": "#4499ff",
+ "portalColor": "68,153,255",
+ "description": "Practical everyday vocabulary, including family, pronouns, places, people, and asking the six W questions.",
+ "locked": true,
+ "videos": [
+ { "id": "v2_1", "title": "Family Signs (Naming)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v2_2", "title": "Pronouns and Indications", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v2_3", "title": "Describing People, Places, and Objects", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v2_4", "title": "At-Home Vocabulary", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v2_5", "title": "Cities and Public Places", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v2_6", "title": "Asking and Responding to the 6 W Questions", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v2_7", "title": "Responding to or Asking for Help (Emergency)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v2_8", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true }
+ ]
+ },
+ {
+ "id": 3,
+ "key": "quantity-time",
+ "label": "Quantity & Time",
+ "theme": "gold",
+ "color": "#ffaa22",
+ "portalColor": "255,170,34",
+ "description": "Numbers up to 1000, reading clocks, relative time, seasons, years, and money.",
+ "locked": true,
+ "videos": [
+ { "id": "v3_1", "title": "Numbers 1–1000", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v3_2", "title": "Reading and Interpreting Time on a Clock", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v3_3", "title": "Days, Weeks, and Months", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v3_4", "title": "Relative Time (Today, Tomorrow, Yesterday…)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v3_5", "title": "Seasons and Years", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v3_6", "title": "Money and Prices", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v3_7", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true }
+ ]
+ },
+ {
+ "id": 4,
+ "key": "actions-feelings",
+ "label": "Actions & Feelings",
+ "theme": "hellfire",
+ "color": "#ff5533",
+ "portalColor": "255,85,51",
+ "description": "Common action words, emotions, facial grammar, body/health markers, adverbs, and question types.",
+ "locked": true,
+ "videos": [
+ { "id": "v4_1", "title": "Common Action Words (Eat, Drink, Go…)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v4_2", "title": "Negation (No, Don't, Not)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v4_3", "title": "Emotions", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v4_4", "title": "Facial Indicators for Emotions", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v4_5", "title": "Body and Health Markers (Hungry, Tired, Sick…)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v4_6", "title": "Adverbs in ASL", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v4_7", "title": "Question Types: Yes/No vs. 6W Questions", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v4_8", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true }
+ ]
+ },
+ {
+ "id": 5,
+ "key": "grammar",
+ "label": "ASL Grammar",
+ "theme": "toxic",
+ "color": "#66cc00",
+ "portalColor": "102,204,0",
+ "description": "ASL sentence structure, spatial references, tense, classifiers, perspective, rhetorics, and conditionals.",
+ "locked": true,
+ "videos": [
+ { "id": "v5_1", "title": "ASL Sentence Structure (Topic-Comment)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v5_2", "title": "Setting Spatial References", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v5_3", "title": "Tense Types in ASL", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v5_4", "title": "Classifiers (Descriptive Signs)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v5_5", "title": "Perspective and Directionality (Reflexive Verbs)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v5_6", "title": "Rhetorical Questions", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v5_7", "title": "Conditionals in ASL", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v5_8", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true }
+ ]
+ },
+ {
+ "id": 6,
+ "key": "conversation",
+ "label": "Conversation",
+ "theme": "blood",
+ "color": "#cc0022",
+ "portalColor": "180,0,34",
+ "description": "Opening and closing conversations, clarification, and real-world conversational contexts.",
+ "locked": true,
+ "videos": [
+ { "id": "v6_1", "title": "Opening and Closing a Conversation", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v6_2", "title": "Clarifying, Repeating, and Asking Again", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v6_3", "title": "Medical Conversations (Vocab and Flow)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v6_4", "title": "Shopping Conversations", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v6_5", "title": "School and Sports Conversations", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v6_6", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true }
+ ]
+ },
+ {
+ "id": 7,
+ "key": "fluency",
+ "label": "Fluency",
+ "theme": "arcane",
+ "color": "#aa55ff",
+ "portalColor": "140,60,255",
+ "description": "Regional variation, initialized signs, mouthing, speed reading, ASL literature, and the final challenge.",
+ "locked": true,
+ "boss": true,
+ "videos": [
+ { "id": "v7_1", "title": "Regional and Stylistic Variation", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v7_2", "title": "Initialized Signs (F for Family, etc.)", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v7_3", "title": "Mouthing Words in ASL", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v7_4", "title": "Reading Fast Signers", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v7_5", "title": "ASL Literature and Its Importance", "youtube": "www.youtube.com", "duration": "" },
+ { "id": "v7_6", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true }
+ ]
+ }
+ ]
+}
diff --git a/projecthearthstone.in/Courses/ASL course/course_video/course_video.html b/projecthearthstone.in/Courses/ASL course/course_video/course_video.html
new file mode 100644
index 0000000..cf8d57a
--- /dev/null
+++ b/projecthearthstone.in/Courses/ASL course/course_video/course_video.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+ ASL Course - Playback
+
+
+
+
+
+
+
+
ASL Mastery
+
Loading course…
+
+
+
+
Lesson unlocked
+
+
+
+
+
+
+
+
+
+
+
+
+
Lesson Locked
+
Complete the previous lesson to unlock this one.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projecthearthstone.in/Courses/ASL course/course_video/script.js b/projecthearthstone.in/Courses/ASL course/course_video/script.js
new file mode 100644
index 0000000..119dffa
--- /dev/null
+++ b/projecthearthstone.in/Courses/ASL course/course_video/script.js
@@ -0,0 +1,339 @@
+(function() {
+ const canvas = document.getElementById('star-canvas');
+ const ctx = canvas.getContext('2d');
+ let W, H, stars = [];
+ function resize() {
+ W = canvas.width = window.innerWidth;
+ H = canvas.height = window.innerHeight;
+ stars = [];
+ const n = Math.floor(W * H / 2800);
+ for (let i = 0; i < n; i++) {
+ const t = Math.random();
+ stars.push({
+ x: Math.random()*W, y: Math.random()*H,
+ r: t>0.92?1.3+Math.random()*0.7:t>0.7?0.7+Math.random()*0.4:0.2+Math.random()*0.35,
+ a: 0.1+Math.random()*0.6, sp: 0.4+Math.random()*2.2, ph: Math.random()*Math.PI*2,
+ col: Math.random()>0.88?[180,200,255]:Math.random()>0.8?[200,160,255]:[215,215,238],
+ });
+ }
+ }
+ window.addEventListener('resize', resize); resize();
+ let f = 0;
+ (function draw() {
+ ctx.clearRect(0,0,W,H);
+ const t = f++ * 0.016;
+ stars.forEach(s => {
+ const al = s.a*(0.45+0.55*Math.sin(t*s.sp+s.ph));
+ const [r,g,b] = s.col;
+ ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2);
+ ctx.fillStyle=`rgba(${r},${g},${b},${al.toFixed(3)})`; ctx.fill();
+ });
+ requestAnimationFrame(draw);
+ })();
+})();
+
+const LS_VIDEO_PROGRESS = 'asl_video_progress_v2';
+const LS_PORTAL_PROGRESS = 'asl_progress_v2';
+
+function lsGet(key) { try { return localStorage.getItem(key); } catch(e) { return null; } }
+function lsSet(key, val) { try { localStorage.setItem(key, val); } catch(e) {} }
+
+function loadCompletedVideos() {
+ try {
+ const raw = lsGet(LS_VIDEO_PROGRESS);
+ return raw ? JSON.parse(raw) : [];
+ } catch(e) { return []; }
+}
+function saveCompletedVideos(arr) { lsSet(LS_VIDEO_PROGRESS, JSON.stringify(arr)); }
+function markVideoComplete(videoId) {
+ const done = loadCompletedVideos();
+ if (!done.includes(videoId)) { done.push(videoId); saveCompletedVideos(done); }
+}
+function isVideoCompleted(videoId) { return loadCompletedVideos().includes(videoId); }
+
+function buildGlobalVideoList(sections) {
+ const list = [];
+ sections.forEach(sec => sec.videos.forEach(v => list.push({ ...v, sectionKey: sec.key })));
+ return list;
+}
+function isVideoUnlocked(videoId, globalList) {
+ const idx = globalList.findIndex(v => v.id === videoId);
+ if (idx === 0) return true;
+ return isVideoCompleted(globalList[idx - 1].id);
+}
+
+let courseData = null;
+let currentSection = null;
+let currentVideoIdx = 0;
+let globalVideoList = [];
+
+function applyTheme(section) {
+ const root = document.documentElement;
+ root.style.setProperty('--col', section.color);
+ root.style.setProperty('--col-dim', hexToRgba(section.color, 0.15));
+ const [r,g,b] = section.portalColor.split(',').map(Number);
+ root.style.setProperty('--col-shadow', `rgba(${r},${g},${b},0.18)`);
+ const name = document.getElementById('sidebar-section-name');
+ name.textContent = section.label;
+ name.style.color = section.color;
+ name.style.textShadow = `0 0 28px ${section.color}`;
+ document.getElementById('sidebar-section-desc').textContent = section.description;
+ document.title = `ASL Mastery — ${section.label}`;
+}
+
+function hexToRgba(hex, alpha) {
+ const r = parseInt(hex.slice(1,3),16);
+ const g = parseInt(hex.slice(3,5),16);
+ const b = parseInt(hex.slice(5,7),16);
+ return `rgba(${r},${g},${b},${alpha})`;
+}
+
+function updateProgressBar() {}
+
+function buildSectionTabs(activeSection) {
+ const container = document.getElementById('section-tabs');
+ container.innerHTML = '';
+ courseData.sections.forEach(sec => {
+ const isLocked = !isVideoUnlocked(sec.videos[0].id, globalVideoList);
+ const isActive = sec.key === activeSection.key;
+ const btn = document.createElement('button');
+ btn.className = 'section-tab' + (isActive ? ' active' : '') + (isLocked ? ' locked' : '');
+ const dot = document.createElement('span');
+ dot.className = 'tab-dot';
+ dot.style.background = isLocked ? 'transparent' : sec.color;
+ dot.style.borderColor = sec.color;
+ dot.style.color = sec.color;
+ const label = document.createElement('span');
+ label.className = 'tab-label';
+ label.textContent = sec.label;
+ btn.appendChild(dot);
+ btn.appendChild(label);
+ if (isLocked) {
+ const lockIcon = document.createElement('span');
+ lockIcon.className = 'tab-lock-icon';
+ lockIcon.innerHTML = ``;
+ btn.appendChild(lockIcon);
+ } else {
+ btn.addEventListener('click', () => switchSection(sec));
+ }
+ container.appendChild(btn);
+ });
+}
+
+function buildVideoList(section, activeIdx) {
+ const container = document.getElementById('video-list');
+ container.innerHTML = '';
+ section.videos.forEach((video, i) => {
+ const unlocked = isVideoUnlocked(video.id, globalVideoList);
+ const completed = isVideoCompleted(video.id);
+ const isActive = i === activeIdx;
+ const btn = document.createElement('button');
+ btn.className = 'video-item' + (isActive ? ' active' : '') + (!unlocked ? ' locked' : '');
+ const num = document.createElement('span');
+ num.className = 'video-num';
+ num.textContent = String(i + 1).padStart(2, '0');
+ const info = document.createElement('div');
+ info.className = 'video-info';
+ const title = document.createElement('div');
+ title.className = 'video-title';
+ title.textContent = video.title;
+ const dur = document.createElement('div');
+ dur.className = 'video-dur';
+ dur.textContent = video.duration;
+ info.appendChild(title);
+ info.appendChild(dur);
+ const statusWrap = document.createElement('div');
+ statusWrap.className = 'video-status';
+ if (!unlocked) {
+ statusWrap.innerHTML = ``;
+ } else if (isActive) {
+ statusWrap.innerHTML = ``;
+ } else if (completed) {
+ statusWrap.innerHTML = ``;
+ } else {
+ statusWrap.innerHTML = ``;
+ }
+ btn.appendChild(num);
+ btn.appendChild(info);
+ btn.appendChild(statusWrap);
+ if (unlocked) btn.addEventListener('click', () => loadVideo(section, i));
+ container.appendChild(btn);
+ });
+}
+
+const arrowSvg = ``;
+
+function loadVideo(section, idx) {
+ currentSection = section;
+ currentVideoIdx = idx;
+ const video = section.videos[idx];
+ const unlocked = isVideoUnlocked(video.id, globalVideoList);
+ const frame = document.getElementById('video-frame');
+ const lockedOverlay = document.getElementById('video-locked-overlay');
+ const completeBtnWrap = document.getElementById('complete-btn-wrap');
+ if (unlocked) {
+ const url = video.youtube.includes('?')
+ ? video.youtube + '&rel=0&modestbranding=1&enablejsapi=1'
+ : video.youtube + '?rel=0&modestbranding=1&enablejsapi=1';
+ frame.src = url;
+ lockedOverlay.classList.remove('visible');
+ completeBtnWrap.classList.remove('visible');
+ if (!isVideoCompleted(video.id)) {
+ clearTimeout(window._completeBtnTimer);
+ window._completeBtnTimer = setTimeout(() => completeBtnWrap.classList.add('visible'), 3000);
+ }
+ } else {
+ frame.src = '';
+ lockedOverlay.classList.add('visible');
+ completeBtnWrap.classList.remove('visible');
+ }
+ document.getElementById('current-video-title').textContent = video.title;
+ document.getElementById('current-video-meta').textContent = `${section.label} · ${video.duration}`;
+ const globalIdx = globalVideoList.findIndex(v => v.id === video.id);
+ const prevExists = globalIdx > 0;
+ const nextExists = globalIdx < globalVideoList.length - 1;
+ const nextUnlocked = nextExists && isVideoUnlocked(globalVideoList[globalIdx+1].id, globalVideoList);
+ document.getElementById('btn-prev-vid').disabled = !prevExists;
+ const nextBtn = document.getElementById('btn-next-vid');
+ nextBtn.disabled = !nextExists || !nextUnlocked;
+ if (nextExists) {
+ const nextVid = globalVideoList[globalIdx + 1];
+ if (nextVid.sectionKey !== section.key) {
+ const nextSec = courseData.sections.find(s => s.key === nextVid.sectionKey);
+ nextBtn.innerHTML = `Next Section: ${nextSec ? nextSec.label : 'Next'} ${arrowSvg}`;
+ } else {
+ nextBtn.innerHTML = `Next Lesson ${arrowSvg}`;
+ }
+ } else {
+ nextBtn.innerHTML = `Next Lesson ${arrowSvg}`;
+ }
+ lsSet('asl_current_section', section.key);
+ lsSet('asl_current_video', video.id);
+ buildVideoList(section, idx);
+ buildSectionTabs(section);
+}
+
+function completeCurrentLesson() {
+ const video = currentSection.videos[currentVideoIdx];
+ markVideoComplete(video.id);
+ document.getElementById('complete-btn-wrap').classList.remove('visible');
+ const globalIdx = globalVideoList.findIndex(v => v.id === video.id);
+ let unlockedLabel = null;
+ let unlockedSection = null;
+ if (globalIdx < globalVideoList.length - 1) {
+ const nextVid = globalVideoList[globalIdx + 1];
+ if (nextVid.sectionKey !== currentSection.key) {
+ const nextSec = courseData.sections.find(s => s.key === nextVid.sectionKey);
+ unlockedLabel = `Section unlocked: ${nextSec ? nextSec.label : 'Next'}`;
+ unlockedSection = nextSec || null;
+ const secIdx = courseData.sections.findIndex(s => s.key === nextVid.sectionKey);
+ const current = parseInt(lsGet(LS_PORTAL_PROGRESS) || '1', 10);
+ if (secIdx + 1 > current) lsSet(LS_PORTAL_PROGRESS, String(secIdx + 1));
+ }
+ }
+ buildVideoList(currentSection, currentVideoIdx);
+ buildSectionTabs(currentSection);
+ const vidGlobalIdx = globalVideoList.findIndex(v => v.id === video.id);
+ if (vidGlobalIdx < globalVideoList.length - 1) {
+ document.getElementById('btn-next-vid').disabled = false;
+ }
+ if (unlockedLabel) showToast(unlockedLabel, unlockedSection);
+}
+
+function showToast(text, section) {
+ const toast = document.getElementById('unlock-toast');
+ document.getElementById('unlock-toast-text').textContent = text;
+ const color = section ? section.color : 'rgba(170,85,255,1)';
+ const [r, g, b] = section ? section.portalColor.split(',').map(Number) : [170, 85, 255];
+ toast.style.borderColor = color;
+ toast.style.color = color;
+ toast.style.background = `rgba(${r},${g},${b},0.08)`;
+ toast.style.boxShadow = `0 0 32px rgba(${r},${g},${b},0.28), 0 8px 32px rgba(0,0,0,0.6)`;
+ toast.classList.add('show');
+ setTimeout(() => toast.classList.remove('show'), 3500);
+}
+
+function switchSection(section) {
+ applyTheme(section);
+ const savedVideoId = lsGet('asl_current_video');
+ const savedIdx = section.videos.findIndex(v => v.id === savedVideoId);
+ const targetIdx = (savedIdx >= 0 && section.key === lsGet('asl_current_section')) ? savedIdx : 0;
+ buildVideoList(section, targetIdx);
+ loadVideo(section, targetIdx);
+ const url = new URL(window.location);
+ url.searchParams.set('section', section.key);
+ window.history.replaceState({}, '', url);
+}
+
+window.addEventListener('message', (e) => {
+ try {
+ const data = JSON.parse(e.data);
+ if (data.event === 'onStateChange' && data.info === 0) {
+ document.getElementById('complete-btn-wrap').classList.add('visible');
+ }
+ } catch(e) {}
+});
+
+document.getElementById('btn-prev-vid').addEventListener('click', () => {
+ const globalIdx = globalVideoList.findIndex(v => v.id === currentSection.videos[currentVideoIdx].id);
+ if (globalIdx > 0) {
+ const prev = globalVideoList[globalIdx - 1];
+ const prevSec = courseData.sections.find(s => s.key === prev.sectionKey);
+ const prevVidIdx = prevSec.videos.findIndex(v => v.id === prev.id);
+ if (prevSec.key !== currentSection.key) applyTheme(prevSec);
+ buildVideoList(prevSec, prevVidIdx);
+ buildSectionTabs(prevSec);
+ loadVideo(prevSec, prevVidIdx);
+ }
+});
+
+document.getElementById('btn-next-vid').addEventListener('click', () => {
+ const globalIdx = globalVideoList.findIndex(v => v.id === currentSection.videos[currentVideoIdx].id);
+ if (globalIdx < globalVideoList.length - 1) {
+ const next = globalVideoList[globalIdx + 1];
+ if (!isVideoUnlocked(next.id, globalVideoList)) return;
+ const nextSec = courseData.sections.find(s => s.key === next.sectionKey);
+ const nextVidIdx = nextSec.videos.findIndex(v => v.id === next.id);
+ if (nextSec.key !== currentSection.key) applyTheme(nextSec);
+ buildSectionTabs(nextSec);
+ buildVideoList(nextSec, nextVidIdx);
+ loadVideo(nextSec, nextVidIdx);
+ }
+});
+
+document.getElementById('complete-btn').addEventListener('click', completeCurrentLesson);
+
+async function init() {
+ courseData = await (await fetch('../course-data.json')).json();
+ globalVideoList = buildGlobalVideoList(courseData.sections);
+ const params = new URLSearchParams(window.location.search);
+ const urlSection = params.get('section');
+ const savedSection = lsGet('asl_current_section');
+ const savedVideo = lsGet('asl_current_video');
+ let section = courseData.sections.find(s => s.key === (urlSection || savedSection)) || courseData.sections[0];
+ if (!isVideoUnlocked(section.videos[0].id, globalVideoList)) section = courseData.sections[0];
+ let vidIdx = 0;
+ if (savedVideo) {
+ const found = section.videos.findIndex(v => v.id === savedVideo);
+ if (found >= 0 && isVideoUnlocked(section.videos[found].id, globalVideoList)) vidIdx = found;
+ }
+ applyTheme(section);
+ buildSectionTabs(section);
+ buildVideoList(section, vidIdx);
+ loadVideo(section, vidIdx);
+ document.getElementById('loading-overlay').classList.add('hidden');
+}
+
+init();
diff --git a/projecthearthstone.in/Courses/ASL course/course_video/style.css b/projecthearthstone.in/Courses/ASL course/course_video/style.css
new file mode 100644
index 0000000..1975425
--- /dev/null
+++ b/projecthearthstone.in/Courses/ASL course/course_video/style.css
@@ -0,0 +1,330 @@
+*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
+
+:root {
+ --col: #aa55ff;
+ --col-dim: rgba(170,85,255,0.18);
+ --col-shadow: rgba(140,60,255,0.12);
+ --bg: #06060b;
+ --sidebar-w: 300px;
+ --bottom-h: 86px;
+}
+
+html, body {
+ width: 100%; height: 100%;
+ background: var(--bg);
+ color: rgba(255,255,255,0.85);
+ overflow: hidden;
+}
+
+#star-canvas {
+ position: fixed; inset: 0;
+ pointer-events: none; z-index: 0;
+}
+
+#app {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ z-index: 1;
+ display: grid;
+ grid-template-columns: var(--sidebar-w) 1fr;
+ grid-template-rows: 1fr var(--bottom-h);
+}
+
+#sidebar {
+ grid-row: 1 / 3;
+ background: rgba(4,4,9,0.7);
+ border-right: 1px solid rgba(255,255,255,0.07);
+ display: flex; flex-direction: column;
+ overflow: hidden;
+}
+
+#sidebar-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid rgba(255,255,255,0.06);
+ flex-shrink: 0;
+ display: flex; flex-direction: column;
+}
+
+#btn-tree {
+ display: flex; align-items: center; gap: 8px;
+ padding: 8px 12px; margin-bottom: 16px;
+ background: rgba(255,255,255,0.05);
+ border: 1px solid rgba(255,255,255,0.12);
+ border-radius: 9px;
+ color: rgba(255,255,255,0.45);
+ font-size: 9px; letter-spacing: 0.28em; text-transform: uppercase;
+ text-decoration: none; cursor: pointer;
+ transition: background 0.2s, color 0.2s, border-color 0.2s;
+ width: fit-content;
+}
+#btn-tree:hover {
+ background: rgba(255,255,255,0.1);
+ border-color: rgba(255,255,255,0.3);
+ color: rgba(255,255,255,0.82);
+}
+#btn-tree svg { flex-shrink: 0; color: inherit; }
+
+#sidebar-section-name {
+ font-size: 16px; font-weight: 700;
+ letter-spacing: 0.12em; text-transform: uppercase;
+ color: var(--col);
+ text-shadow: 0 0 28px var(--col);
+ margin-bottom: 7px;
+}
+#sidebar-section-desc {
+ font-size: 13px; line-height: 1.55;
+ color: rgba(255,255,255,0.32);
+ font-style: italic;
+}
+
+#section-tabs {
+ padding: 8px 10px 6px;
+ border-bottom: 1px solid rgba(255,255,255,0.05);
+ flex-shrink: 0;
+ display: flex; flex-direction: column; gap: 1px;
+}
+
+.section-tab {
+ display: flex; align-items: center; gap: 10px;
+ padding: 9px 12px; border-radius: 7px;
+ cursor: pointer; transition: background 0.18s, opacity 0.18s;
+ border: none; background: none; width: 100%; text-align: left;
+ position: relative;
+}
+.section-tab:not(.locked):hover { background: rgba(255,255,255,0.05); }
+.section-tab.active { background: rgba(255,255,255,0.07); }
+.section-tab.active::before {
+ content: '';
+ position: absolute; left: 0; top: 20%; bottom: 20%;
+ width: 2px; border-radius: 2px;
+ background: var(--col);
+ box-shadow: 0 0 8px var(--col);
+}
+.section-tab.locked { opacity: 0.28; cursor: not-allowed; pointer-events: none; }
+
+.tab-dot {
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
+ border: 1.5px solid rgba(255,255,255,0.2);
+ transition: box-shadow 0.2s;
+}
+.section-tab.active .tab-dot { box-shadow: 0 0 7px currentColor; }
+.section-tab.locked .tab-dot { background: transparent !important; }
+
+.tab-label {
+ font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;
+ color: rgba(255,255,255,0.45); flex: 1;
+}
+.section-tab.active .tab-label { color: rgba(255,255,255,0.88); }
+
+.tab-lock-icon { flex-shrink: 0; opacity: 0.5; }
+.tab-lock-icon svg { display: block; }
+
+#video-list {
+ flex: 1; overflow-y: auto;
+ padding: 10px;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(255,255,255,0.08) transparent;
+ display: flex; flex-direction: column; gap: 2px;
+}
+
+.video-item {
+ display: flex; align-items: center; gap: 12px;
+ padding: 12px; border-radius: 8px;
+ cursor: pointer; border: none; background: none;
+ width: 100%; text-align: left;
+ transition: background 0.18s, opacity 0.18s;
+}
+.video-item:not(.locked):hover { background: rgba(255,255,255,0.06); }
+.video-item.active { background: rgba(255,255,255,0.09); }
+.video-item.locked { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
+
+.video-num {
+ font-size: 10px; color: rgba(255,255,255,0.18);
+ min-width: 22px; letter-spacing: 0.05em; flex-shrink: 0;
+}
+.video-item.active .video-num { color: var(--col); }
+
+.video-info { flex: 1; min-width: 0; }
+.video-title {
+ font-size: 14px; font-weight: 400;
+ color: rgba(255,255,255,0.6);
+ line-height: 1.3; margin-bottom: 3px;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+}
+.video-item.active .video-title { color: rgba(255,255,255,0.95); }
+.video-dur {
+ font-size: 9px; color: rgba(255,255,255,0.18);
+}
+
+.video-status { flex-shrink: 0; display: flex; align-items: center; }
+.video-item.active .vstat-dot {
+ width: 6px; height: 6px; border-radius: 50%;
+ background: var(--col); box-shadow: 0 0 8px var(--col);
+ animation: vidPulse 1.8s ease-in-out infinite;
+}
+@keyframes vidPulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
+
+.vstat-check { color: rgba(255,255,255,0.25); }
+.vstat-check.done { color: var(--col); }
+.vstat-lock { opacity: 0.4; }
+
+#main {
+ grid-row: 1 / 2;
+ display: flex; flex-direction: column;
+ overflow: hidden;
+ background: rgba(0,0,0,0.1);
+ padding: 24px 32px 16px;
+}
+
+#video-frame-wrap {
+ flex: 1;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow:
+ 0 0 0 1px rgba(255,255,255,0.08),
+ 0 8px 48px rgba(0,0,0,0.7),
+ 0 0 80px var(--col-shadow);
+ background: #000;
+ position: relative;
+}
+#video-frame { width: 100%; height: 100%; border: none; display: block; }
+
+#video-locked-overlay {
+ position: absolute; inset: 0;
+ background: rgba(4,4,10,0.88);
+ display: none;
+ flex-direction: column;
+ align-items: center; justify-content: center;
+ gap: 16px; z-index: 5;
+ backdrop-filter: blur(4px);
+}
+#video-locked-overlay.visible { display: flex; }
+.locked-overlay-icon { opacity: 0.5; }
+.locked-overlay-title {
+ font-size: 16px; letter-spacing: 0.3em; text-transform: uppercase;
+ color: rgba(255,255,255,0.3);
+}
+.locked-overlay-hint {
+ font-size: 13px; font-style: italic;
+ color: rgba(255,255,255,0.18);
+ max-width: 320px; text-align: center; line-height: 1.5;
+}
+
+#complete-btn-wrap {
+ position: absolute; bottom: 20px; right: 20px;
+ z-index: 6; opacity: 0; pointer-events: none;
+ transition: opacity 0.4s;
+}
+#complete-btn-wrap.visible { opacity: 1; pointer-events: all; }
+#complete-btn {
+ display: flex; align-items: center; gap: 8px;
+ background: var(--col);
+ border: none; border-radius: 10px;
+ padding: 12px 22px;
+ color: #fff;
+ font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
+ cursor: pointer;
+ box-shadow: 0 0 24px var(--col-shadow), 0 4px 16px rgba(0,0,0,0.5);
+ transition: transform 0.15s, box-shadow 0.15s;
+}
+#complete-btn:hover { transform: scale(1.04); box-shadow: 0 0 36px var(--col), 0 4px 20px rgba(0,0,0,0.5); }
+#complete-btn:active { transform: scale(0.97); }
+
+#bottom-bar {
+ grid-column: 2; grid-row: 2;
+ display: flex; align-items: center;
+ padding: 0 28px; gap: 16px;
+ border-top: 1px solid rgba(255,255,255,0.07);
+ background: rgba(4,4,9,0.82);
+ backdrop-filter: blur(14px);
+}
+
+#bottom-bar-left {
+ display: flex; flex-direction: column; gap: 3px;
+ flex: 1; min-width: 0;
+}
+
+#current-video-title {
+ font-size: 15px; font-weight: 600;
+ letter-spacing: 0.06em;
+ color: rgba(255,255,255,0.88);
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ max-width: 380px;
+}
+#current-video-meta {
+ font-size: 12px; font-style: italic;
+ color: rgba(255,255,255,0.3);
+ white-space: nowrap;
+}
+
+#video-nav { margin-left: auto; display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
+
+.vnav-btn {
+ display: flex; align-items: center; gap: 10px;
+ background: rgba(255,255,255,0.07);
+ border: 1px solid rgba(255,255,255,0.18);
+ border-radius: 12px; padding: 13px 26px;
+ color: rgba(255,255,255,0.65);
+ font-size: 12px; font-weight: 600;
+ letter-spacing: 0.12em; text-transform: uppercase;
+ cursor: pointer; transition: all 0.18s;
+ white-space: nowrap; min-width: 148px; justify-content: center;
+}
+.vnav-btn:hover:not(:disabled) {
+ background: rgba(255,255,255,0.13);
+ color: rgba(255,255,255,0.95);
+ border-color: rgba(255,255,255,0.4);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
+}
+.vnav-btn:active:not(:disabled) { transform: translateY(0); }
+.vnav-btn:disabled { opacity: 0.2; cursor: default; }
+
+.vnav-btn.primary {
+ background: rgba(170,85,255,0.12);
+ border-color: var(--col);
+ color: var(--col);
+ box-shadow: 0 0 20px var(--col-shadow);
+}
+.vnav-btn.primary:hover:not(:disabled) {
+ background: var(--col-dim);
+ box-shadow: 0 0 28px var(--col-shadow), 0 4px 20px rgba(0,0,0,0.4);
+ color: #fff;
+ border-color: var(--col);
+}
+
+#loading-overlay {
+ position: fixed; inset: 0; z-index: 200;
+ background: var(--bg);
+ display: flex; align-items: center; justify-content: center;
+ flex-direction: column; gap: 20px;
+ transition: opacity 0.6s;
+}
+#loading-overlay.hidden { opacity: 0; pointer-events: none; }
+.loading-glyph {
+ font-size: 32px; letter-spacing: 0.5em; text-transform: uppercase;
+ color: rgba(255,255,255,0.07);
+ animation: loadPulse 2s ease-in-out infinite;
+}
+.loading-sub {
+ font-size: 9px; letter-spacing: 0.4em;
+ color: rgba(255,255,255,0.12); text-transform: uppercase;
+}
+@keyframes loadPulse { 0%,100%{opacity:0.07} 50%{opacity:0.4} }
+
+#unlock-toast {
+ position: fixed; bottom: 90px; left: 50%;
+ transform: translateX(-50%) translateY(20px);
+ background: rgba(10,10,18,0.95);
+ border: 1px solid var(--col);
+ border-radius: 12px; padding: 14px 24px;
+ display: flex; align-items: center; gap: 12px;
+ font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
+ color: var(--col);
+ box-shadow: 0 0 32px var(--col-shadow), 0 8px 32px rgba(0,0,0,0.6);
+ z-index: 300;
+ opacity: 0; pointer-events: none;
+ transition: opacity 0.4s, transform 0.4s;
+ white-space: nowrap;
+}
+#unlock-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
diff --git a/projecthearthstone.in/Courses/ASL course/index.html b/projecthearthstone.in/Courses/ASL course/index.html
new file mode 100644
index 0000000..2320eeb
--- /dev/null
+++ b/projecthearthstone.in/Courses/ASL course/index.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+ ASL Mastery — Course Map
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ASL Mastery · Course Map
+
+
+
+
+
+
+
+
+
+
+
1 / 7
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projecthearthstone.in/Courses/ASL course/script.js b/projecthearthstone.in/Courses/ASL course/script.js
new file mode 100644
index 0000000..a80294b
--- /dev/null
+++ b/projecthearthstone.in/Courses/ASL course/script.js
@@ -0,0 +1,789 @@
+let COURSE_DATA = null;
+
+async function loadCourseData() {
+ try {
+ const res = await fetch('course-data.json');
+ COURSE_DATA = await res.json();
+ } catch(e) {}
+}
+
+const STORAGE_KEY = 'asl_progress_v2';
+const TOTAL_PORTALS = 7;
+
+function loadProgress() {
+ try {
+ const n = parseInt(localStorage.getItem(STORAGE_KEY), 10);
+ return isNaN(n) ? 1 : Math.max(1, Math.min(n, TOTAL_PORTALS));
+ } catch(e) { return 1; }
+}
+
+function saveProgress(count) {
+ try { localStorage.setItem(STORAGE_KEY, String(count)); } catch(e) {}
+}
+
+let unlockedCount = loadProgress();
+
+const THEMES = {
+ arcane: { dark:'#0b0220', mid:'#7733cc', bright:'#cc99ff', edge:'#aa55ff', edgeM:'#cc99ff', flash:'rgba(140,60,255,0.22)' },
+ emerald: { dark:'#011510', mid:'#11aa66', bright:'#66ffcc', edge:'#22dd88', edgeM:'#77ffcc', flash:'rgba(20,180,100,0.20)' },
+ hellfire:{ dark:'#160303', mid:'#cc3311', bright:'#ffaa77', edge:'#ff5533', edgeM:'#ffaa77', flash:'rgba(220,60,20,0.22)' },
+ frost: { dark:'#010d18', mid:'#2266cc', bright:'#99ccff', edge:'#4499ff', edgeM:'#aaddff', flash:'rgba(50,130,255,0.22)' },
+ gold: { dark:'#120c00', mid:'#cc8800', bright:'#ffdd77', edge:'#ffaa22', edgeM:'#ffe088', flash:'rgba(220,160,20,0.22)' },
+ abyss: { dark:'#000510', mid:'#003366', bright:'#4499cc', edge:'#1166aa', edgeM:'#66bbee', flash:'rgba(10,80,160,0.22)' },
+ toxic: { dark:'#060f02', mid:'#44aa00', bright:'#aaff44', edge:'#66cc00', edgeM:'#bbff55', flash:'rgba(90,200,10,0.22)' },
+ blood: { dark:'#120002', mid:'#990011', bright:'#ff4455', edge:'#cc0022', edgeM:'#ff6677', flash:'rgba(200,10,30,0.22)' },
+ solar: { dark:'#130800', mid:'#dd5500', bright:'#ffcc44', edge:'#ff8800', edgeM:'#ffdd66', flash:'rgba(240,130,20,0.24)' },
+ void: { dark:'#050508', mid:'#333355', bright:'#9999cc', edge:'#555577', edgeM:'#aaaacc', flash:'rgba(80,80,140,0.22)' },
+};
+
+function hash(n) {
+ const x = Math.sin(n * 127.1 + 311.7) * 43758.5453;
+ return x - Math.floor(x);
+}
+
+function tornEdge(ax, ay, bx, by, nx, ny, depth, count, seed, si) {
+ const pts = [];
+ for (let i = 0; i < count; i++) {
+ const t = (i + 0.5) / count;
+ const px = ax + (bx-ax)*t, py = ay + (by-ay)*t;
+ const h1 = hash(seed + si*100 + i*7.3);
+ const h2 = hash(seed + si*100 + i*13.7 + 5);
+ const h3 = hash(seed + si*100 + i*3.1 + 9);
+ let amp;
+ if (h1 > 0.65) amp = depth * (0.6 + h2 * 0.8);
+ else if (h1 > 0.35) amp = depth * (0.1 + h2 * 0.4) * (h3 > 0.5 ? 1 : -0.7);
+ else amp = -depth * (0.2 + h2 * 0.5);
+ pts.push(px + nx*amp, py + ny*amp);
+ }
+ return pts;
+}
+
+function jaggedPoly(cx, cy, size, depth, density, seed) {
+ const h = size/2;
+ const corners = [[cx-h,cy-h],[cx+h,cy-h],[cx+h,cy+h],[cx-h,cy+h]];
+ const all = [];
+ for (let s = 0; s < 4; s++) {
+ const [ax,ay] = corners[s], [bx,by] = corners[(s+1)%4];
+ const ex=bx-ax, ey=by-ay, el=Math.hypot(ex,ey);
+ let nx=-ey/el, ny=ex/el;
+ if (((ax+bx)/2-cx)*nx + ((ay+by)/2-cy)*ny < 0) { nx=-nx; ny=-ny; }
+ all.push(ax, ay);
+ for (const p of tornEdge(ax,ay,bx,by,nx,ny,depth,density,seed,s)) all.push(p);
+ }
+ const s=[];
+ for (let i=0;i rgbA(mr,mg,mb,a);
+ const brightStr = (a) => rgbA(br2,bg2,bb2,a);
+ const darkStr = (a) => rgbA(dr,dg,db,a);
+ const swirlOff = document.createElement('canvas');
+ swirlOff.width=W; swirlOff.height=H;
+ const sctx = swirlOff.getContext('2d');
+ const vigOff = document.createElement('canvas');
+ vigOff.width=W; vigOff.height=H;
+ const vctx = vigOff.getContext('2d');
+ (function bakeVig(){
+ const v = vctx.createRadialGradient(CX,CY,42, CX,CY,124);
+ v.addColorStop(0,'rgba(0,0,0,0)');
+ v.addColorStop(0.6,'rgba(0,0,0,0.16)');
+ v.addColorStop(1,'rgba(0,0,0,0.72)');
+ vctx.fillStyle=v; vctx.fillRect(0,0,W,H);
+ })();
+ let hoverT=0, burstT=0, swirlBoost=1, isHovered=false;
+ const wisps = Array.from({length:18},(_,i)=>({
+ angle:(i/18)*Math.PI*2, radius:16+(i%6)*14, speed:0.007+(i%5)*0.003,
+ size:7+(i%6)*5, alpha:0.18+(i%4)*0.08, bright:i%3===0,
+ }));
+ function tick(frame) {
+ const t = frame * 0.016;
+ hoverT += ((isHovered?1:0) - hoverT) * 0.07;
+ if (burstT > 0.01) { swirlBoost = 1 + burstT*5; burstT *= 0.88; }
+ else { swirlBoost += (1-swirlBoost)*0.04; }
+ const hB = 1 + hoverT*1.1;
+ ctx.fillStyle = darkStr(1); ctx.fillRect(0,0,W,H);
+ if (hoverT > 0.02) {
+ const ag = ctx.createRadialGradient(CX,CY,58, CX,CY,152);
+ ag.addColorStop(0, midStr(0)); ag.addColorStop(0.5, midStr(0.06*hoverT)); ag.addColorStop(1, darkStr(0));
+ ctx.fillStyle = ag; ctx.fillRect(0,0,W,H);
+ }
+ sctx.clearRect(0,0,W,H);
+ for (let arm=0; arm<3; arm++) {
+ const off2=(arm/3)*Math.PI*2, rot=0.22+arm*0.09;
+ const isBright = arm===1;
+ for (let step=0; step<55; step++) {
+ const frac=step/55;
+ const angle=off2+frac*Math.PI*3.8+t*rot*swirlBoost;
+ const dist=frac*95;
+ const wx=CX+Math.cos(angle)*dist, wy=CY+Math.sin(angle)*dist;
+ const sz=(1-frac)*28+3;
+ const al=(1-frac)*(0.10+hoverT*0.11)*hB+(arm===0?0.018:0);
+ const wg=sctx.createRadialGradient(wx,wy,0, wx,wy,sz);
+ wg.addColorStop(0, isBright ? brightStr(al) : midStr(al));
+ wg.addColorStop(1, midStr(0));
+ sctx.fillStyle=wg;
+ sctx.beginPath(); sctx.arc(wx,wy,sz,0,Math.PI*2); sctx.fill();
+ }
+ }
+ ctx.drawImage(swirlOff,0,0);
+ wisps.forEach((w,i) => {
+ const dir=i%2===0?1:-1;
+ const angle=w.angle+t*w.speed*dir*(Math.floor(i/6)+1)*1.6*swirlBoost;
+ const r2=w.radius+Math.sin(t*0.9+i)*9;
+ const wx=CX+Math.cos(angle)*r2, wy=CY+Math.sin(angle)*r2;
+ const sz=w.size*(0.75+Math.sin(t+i*0.7)*0.25)*(1+hoverT*0.35);
+ const al=w.alpha*hB*(0.65+Math.sin(t*1.3+i)*0.35);
+ const wg=ctx.createRadialGradient(wx,wy,0, wx,wy,sz);
+ wg.addColorStop(0, w.bright ? brightStr(al) : midStr(al));
+ wg.addColorStop(1, midStr(0));
+ ctx.fillStyle=wg;
+ ctx.beginPath(); ctx.arc(wx,wy,sz,0,Math.PI*2); ctx.fill();
+ });
+ ctx.drawImage(vigOff,0,0);
+ const pulse=0.78+Math.sin(t*2.2)*0.22;
+ const coreR=(42+hoverT*14)*pulse;
+ const cg=ctx.createRadialGradient(CX,CY,0, CX,CY,coreR);
+ cg.addColorStop(0, brightStr((0.65+hoverT*0.25)*pulse*hB));
+ cg.addColorStop(0.28, midStr(0.3));
+ cg.addColorStop(0.65, midStr(0.07));
+ cg.addColorStop(1, midStr(0));
+ ctx.fillStyle=cg; ctx.fillRect(0,0,W,H);
+ const pr=(4+hoverT*3)*pulse;
+ const pp=ctx.createRadialGradient(CX,CY,0, CX,CY,pr*3.5);
+ pp.addColorStop(0,'rgba(255,255,255,1)');
+ pp.addColorStop(0.35, brightStr(0.85));
+ pp.addColorStop(1, brightStr(0));
+ ctx.fillStyle=pp; ctx.fillRect(0,0,W,H);
+ if (burstT > 0.01) {
+ const bf=ctx.createRadialGradient(CX,CY,0, CX,CY,135);
+ bf.addColorStop(0, brightStr(Math.min(burstT*1.1,1)));
+ bf.addColorStop(0.15, brightStr(burstT*0.9));
+ bf.addColorStop(0.45, midStr(burstT*0.45));
+ bf.addColorStop(1, midStr(0));
+ ctx.fillStyle=bf; ctx.fillRect(0,0,W,H);
+ }
+ }
+ _tickers.push(tick);
+ return { setHovered(v){isHovered=v;}, burst(){burstT=1;} };
+}
+
+let _uid = 0;
+function initPortal(wrapEl) {
+ const id = ++_uid;
+ const theme = wrapEl.dataset.theme || 'arcane';
+ const cfg = THEMES[theme] || THEMES.arcane;
+ const depth = 24;
+ const isTop = wrapEl.classList.contains('boss');
+ const isLocked = wrapEl.classList.contains('locked');
+ const polySize = isTop ? 190 : 155;
+ const svgSize = isTop ? 280 : 240;
+
+ if (isLocked) {
+ const lid = id;
+ const lockedLabel = wrapEl.dataset.label || 'Locked';
+ const pts = jaggedPoly(120,120,polySize,depth,14,42.7);
+ wrapEl.innerHTML = `
+
+ `;
+ ['cp','ibg','em','eh'].forEach(pfx => {
+ const el = document.getElementById(pfx+lid);
+ if (el) el.setAttribute('points', pts);
+ });
+ return;
+ }
+
+ const allWrapsNow = Array.from(document.querySelectorAll('.portal-wrap'));
+ const portalIdx = allWrapsNow.indexOf(wrapEl);
+ const section = COURSE_DATA && COURSE_DATA.sections ? COURSE_DATA.sections[portalIdx] : null;
+ const sectionLabel = section ? section.label : (wrapEl.dataset.label || theme);
+ const sectionColor = section ? section.color : cfg.bright;
+ const sectionKey = section ? section.key : String(portalIdx + 1);
+
+ wrapEl.innerHTML = `
+
+ `;
+
+ const svg = document.getElementById('sv'+id);
+ const cp = document.getElementById('cp'+id);
+ const ha = document.getElementById('ha'+id);
+ const eg = document.getElementById('eg'+id);
+ const em = document.getElementById('em'+id);
+ const eh = document.getElementById('eh'+id);
+ const canvas = document.getElementById('cv'+id);
+
+ ha.setAttribute('stroke', cfg.bright);
+ eg.setAttribute('stroke', cfg.edge);
+ em.setAttribute('stroke', cfg.edgeM);
+
+ let seedA=Math.random()*999, seedB=seedA+40+Math.random()*60;
+ let pA=jaggedPoly(120,120,polySize,28,14,seedA);
+ let pB=jaggedPoly(120,120,polySize,28,14,seedB);
+ let mT=0, hov=false;
+ let svgScale=1, svgScaleTarget=1;
+ let haOpacity=0, haOpacityTarget=0;
+ let edgeGlowW=7, edgeGlowWTarget=7;
+ let clickPhase='idle', clickT=0;
+
+ function ss(x){return x*x*(3-2*x);}
+ function lerp(a,b,t){return a+(b-a)*t;}
+ function interpPts(a,b,t){
+ const av=a.split(' ').map(s=>s.split(',').map(Number));
+ const bv=b.split(' ').map(s=>s.split(',').map(Number));
+ return av.map(([ax,ay],i)=>
+ ((ax+(bv[i][0]-ax)*t).toFixed(1)+','+(ay+(bv[i][1]-ay)*t).toFixed(1))
+ ).join(' ');
+ }
+ function setPoly(pts){ [cp,ha,eg,em,eh].forEach(el=>el.setAttribute('points',pts)); }
+ setPoly(pA);
+
+ function animLoop(frame){
+ if (frame % 3 === 0) {
+ mT += hov ? 0.038 : 0.028;
+ if (mT>=1){
+ mT=0; seedA=seedB; pA=pB;
+ seedB=seedA+25+Math.random()*45;
+ pB=jaggedPoly(120,120,polySize, hov?36:28, 14, seedB);
+ }
+ setPoly(interpPts(pA,pB,ss(mT)));
+ }
+ if (clickPhase==='compress'){
+ clickT+=0.18; svgScaleTarget=lerp(1,0.82,Math.min(clickT,1));
+ if(clickT>=1){clickPhase='explode';clickT=0;}
+ } else if (clickPhase==='explode'){
+ clickT+=0.14; svgScaleTarget=lerp(0.82,1.22,ss(Math.min(clickT,1)));
+ edgeGlowWTarget=7+(1-clickT)*18;
+ if(clickT>=1){clickPhase='settle';clickT=0;}
+ } else if (clickPhase==='settle'){
+ clickT+=0.06; svgScaleTarget=lerp(1.22,1,ss(Math.min(clickT,1)));
+ if(clickT>=1){clickPhase='idle'; svgScaleTarget=hov?1.07:1;}
+ } else {
+ svgScaleTarget = hov ? 1.07 : 1;
+ }
+ const ambientHalo = 0.12 + Math.sin(Date.now()*0.0018 + id)*0.06;
+ haOpacityTarget = hov ? (0.42+Math.sin(Date.now()*0.003)*0.15) : ambientHalo;
+ svgScale = lerp(svgScale, svgScaleTarget, 0.12);
+ haOpacity = lerp(haOpacity, haOpacityTarget, 0.06);
+ edgeGlowW = lerp(edgeGlowW, edgeGlowWTarget, 0.12);
+ svg.style.transform = `scale(${svgScale.toFixed(4)})`;
+ const glowSize = hov ? (haOpacity*32).toFixed(1) : (ambientHalo*16).toFixed(1);
+ svg.style.filter = hov
+ ? `brightness(${(1.15+haOpacity*0.3).toFixed(3)}) drop-shadow(0 0 ${glowSize}px ${cfg.edge})`
+ : `brightness(1.04) drop-shadow(0 0 ${glowSize}px ${cfg.edge})`;
+ ha.setAttribute('opacity', haOpacity.toFixed(3));
+ eg.setAttribute('stroke-width', edgeGlowW.toFixed(1));
+ }
+ _tickers.push(animLoop);
+
+ const ren = makeRenderer(canvas, cfg);
+
+ svg.addEventListener('mouseenter', ()=>{
+ hov=true; wrapEl.classList.add('hovered'); ren.setHovered(true);
+ seedB=seedA+20+Math.random()*35;
+ pB=jaggedPoly(120,120,polySize,32,14,seedB);
+ });
+ svg.addEventListener('mouseleave', ()=>{
+ hov=false; wrapEl.classList.remove('hovered'); ren.setHovered(false);
+ seedB=seedA+30+Math.random()*50;
+ pB=jaggedPoly(120,120,polySize,depth,14,seedB);
+ });
+ svg.addEventListener('click', ()=>{
+ if (clickPhase!=='idle') return;
+ clickPhase='compress'; clickT=0;
+ ren.burst();
+ flashOverlay.style.background = cfg.flash;
+ flashOverlay.classList.remove('flash');
+ void flashOverlay.offsetWidth;
+ flashOverlay.classList.add('flash');
+ document.body.style.transform='translate(3px,-2px)';
+ setTimeout(()=>{document.body.style.transform='translate(-2px,3px)';},40);
+ setTimeout(()=>{document.body.style.transform='translate(2px,-1px)';},80);
+ setTimeout(()=>{document.body.style.transform='translate(0,0)';},120);
+ try { localStorage.setItem('asl_current_section', sectionKey); } catch(e) {}
+ setTimeout(() => {
+ window.location.href = `course_video/course_video.html?section=${sectionKey}`;
+ }, 480);
+ });
+}
+
+function unlockNextPortal() {
+ const wraps = Array.from(document.querySelectorAll('.portal-wrap'));
+ const nextWrap = wraps[unlockedCount];
+ if (!nextWrap || !nextWrap.classList.contains('locked')) return;
+
+ nextWrap.style.opacity = '0';
+ nextWrap.style.transition = 'opacity 0.9s ease, transform 0.9s cubic-bezier(0.34,1.56,0.64,1)';
+ nextWrap.style.transform = 'translate(-50%,-50%) scale(0.4)';
+ nextWrap.classList.remove('locked');
+ nextWrap.innerHTML = '';
+ initPortal(nextWrap);
+
+ requestAnimationFrame(() => requestAnimationFrame(() => {
+ nextWrap.style.opacity = '1';
+ nextWrap.style.transform = 'translate(-50%,-50%) scale(1)';
+ }));
+
+ unlockedCount++;
+ saveProgress(unlockedCount);
+ updateProgressDots();
+
+ const theme = nextWrap.dataset.theme || 'arcane';
+ const cfg = THEMES[theme] || THEMES.arcane;
+ setTimeout(() => {
+ flashOverlay.style.background = cfg.flash;
+ flashOverlay.classList.remove('flash');
+ void flashOverlay.offsetWidth;
+ flashOverlay.classList.add('flash');
+ }, 200);
+}
+
+function applyInitialLockState() {
+ document.querySelectorAll('.portal-wrap').forEach((wrap, i) => {
+ wrap.classList.toggle('locked', i >= unlockedCount);
+ });
+}
+
+function updateProgressDots() {
+ document.querySelectorAll('.pdot').forEach((dot, i) => {
+ dot.classList.toggle('on', i < unlockedCount);
+ });
+}
+
+const WORLD_W = 3200;
+const WORLD_H = 3200;
+const MM_W = 88;
+const MM_H = 88;
+
+const PORTALS = [
+ { x: 700, y: 2680, color:'34,221,136', label:'Foundations' },
+ { x: 2780, y: 2620, color:'68,153,255', label:'Everyday Vocab' },
+ { x: 1100, y: 2100, color:'255,170,34', label:'Quantity & Time' },
+ { x: 2150, y: 1780, color:'255,85,51', label:'Actions & Feelings'},
+ { x: 680, y: 1480, color:'102,204,0', label:'ASL Grammar' },
+ { x: 2600, y: 1080, color:'180,0,34', label:'Conversation' },
+ { x: 1600, y: 310, color:'140,60,255', label:'Fluency' },
+];
+
+let currentPortalIdx = 0;
+
+const viewport = document.getElementById('viewport');
+const world = document.getElementById('world');
+const mmVPBox = document.getElementById('mm-viewport-box');
+const minimap = document.getElementById('minimap');
+const flashOverlay = document.getElementById('flashOverlay');
+const navIndex = document.getElementById('nav-index');
+
+let tx = 0, ty = 0, camScale = 1;
+let dragging = false, startX, startY, startTx, startTy;
+let isFlying = false;
+let mapMode = false;
+
+world.style.transformOrigin = '0 0';
+
+function commitTransform(animated, duration) {
+ const dur = duration !== undefined ? duration : (animated ? 0.82 : 0);
+ if (animated) {
+ isFlying = true;
+ world.style.transition = `transform ${dur}s cubic-bezier(0.4,0,0.2,1)`;
+ } else {
+ world.style.transition = 'none';
+ }
+ world.style.transform = `translate(${tx}px,${ty}px) scale(${camScale})`;
+ updateMinimap();
+ if (animated) {
+ const onEnd = () => {
+ world.removeEventListener('transitionend', onEnd);
+ world.style.transition = 'none';
+ isFlying = false;
+ };
+ world.addEventListener('transitionend', onEnd);
+ }
+}
+
+function clampCamera() {
+ const sw = WORLD_W * camScale, sh = WORLD_H * camScale;
+ const vw = window.innerWidth, vh = window.innerHeight;
+ tx = Math.max(sw < vw ? (vw-sw)/2 : -(sw-vw), Math.min(sw < vw ? (vw-sw)/2 : 0, tx));
+ ty = Math.max(sh < vh ? (vh-sh)/2 : -(sh-vh), Math.min(sh < vh ? (vh-sh)/2 : 0, ty));
+}
+
+function flyToPoint(wx, wy, targetScale, animated, duration) {
+ camScale = targetScale;
+ tx = window.innerWidth / 2 - wx * camScale;
+ ty = window.innerHeight / 2 - wy * camScale;
+ clampCamera();
+ commitTransform(animated, duration);
+}
+
+function centerOnPortal(idx, animated) {
+ mapMode = false;
+ viewport.classList.remove('map-mode');
+ const p = PORTALS[idx];
+ flyToPoint(p.x, p.y, 1, animated, 0.82);
+ currentPortalIdx = idx;
+ navIndex.textContent = (idx + 1) + ' / ' + PORTALS.length;
+}
+
+function flyToMap() {
+ mapMode = true;
+ viewport.classList.add('map-mode');
+ const pad = 60;
+ const targetScale = Math.min(
+ (window.innerWidth - pad*2) / WORLD_W,
+ (window.innerHeight - pad*2) / WORLD_H
+ );
+ camScale = targetScale;
+ tx = (window.innerWidth - WORLD_W * camScale) / 2;
+ ty = (window.innerHeight - WORLD_H * camScale) / 2;
+ commitTransform(true, 0.85);
+ navIndex.textContent = '· / ' + PORTALS.length;
+}
+
+function exitMapToPortal(idx) {
+ mapMode = false;
+ viewport.classList.remove('map-mode');
+ const p = PORTALS[idx];
+ flyToPoint(p.x, p.y, 1, true, 0.85);
+ currentPortalIdx = idx;
+ navIndex.textContent = (idx + 1) + ' / ' + PORTALS.length;
+}
+
+function applyTransform(animated) {
+ clampCamera();
+ commitTransform(animated);
+}
+
+function updateMinimap() {
+ const scaleX = MM_W / WORLD_W, scaleY = MM_H / WORLD_H;
+ mmVPBox.style.left = (-tx * scaleX) + 'px';
+ mmVPBox.style.top = (-ty * scaleY) + 'px';
+ mmVPBox.style.width = (window.innerWidth * scaleX) + 'px';
+ mmVPBox.style.height = (window.innerHeight * scaleY) + 'px';
+}
+
+PORTALS.forEach(p => {
+ const dot = document.createElement('div');
+ dot.className = 'mm-dot';
+ dot.style.left = (p.x / WORLD_W * MM_W) + 'px';
+ dot.style.top = (p.y / WORLD_H * MM_H) + 'px';
+ dot.style.background = `rgb(${p.color})`;
+ dot.style.boxShadow = `0 0 5px rgb(${p.color})`;
+ minimap.appendChild(dot);
+});
+
+applyInitialLockState();
+flyToMap();
+updateProgressDots();
+
+viewport.addEventListener('mousedown', e => {
+ if (e.target.closest('.portal-wrap')) return;
+ if (isFlying) return;
+ if (mapMode) {
+ const worldX = (e.clientX - tx) / camScale;
+ const worldY = (e.clientY - ty) / camScale;
+ let bestIdx = currentPortalIdx, bestDist = Infinity;
+ PORTALS.forEach((p, i) => {
+ const d = Math.hypot(p.x - worldX, p.y - worldY);
+ if (d < bestDist) { bestDist = d; bestIdx = i; }
+ });
+ exitMapToPortal(bestIdx);
+ return;
+ }
+ dragging = true;
+ startX = e.clientX; startY = e.clientY;
+ startTx = tx; startTy = ty;
+ viewport.classList.add('panning');
+});
+window.addEventListener('mousemove', e => {
+ if (!dragging) return;
+ tx = startTx + (e.clientX - startX);
+ ty = startTy + (e.clientY - startY);
+ applyTransform(false);
+});
+window.addEventListener('mouseup', () => { dragging = false; viewport.classList.remove('panning'); });
+
+let touchStartX, touchStartY, touchStartTx, touchStartTy;
+viewport.addEventListener('touchstart', e => {
+ touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY;
+ touchStartTx = tx; touchStartTy = ty;
+}, { passive: true });
+viewport.addEventListener('touchmove', e => {
+ tx = touchStartTx + (e.touches[0].clientX - touchStartX);
+ ty = touchStartTy + (e.touches[0].clientY - touchStartY);
+ applyTransform(false);
+}, { passive: true });
+
+viewport.addEventListener('wheel', e => {
+ e.preventDefault();
+ tx -= e.deltaX; ty -= e.deltaY;
+ applyTransform(false);
+}, { passive: false });
+
+window.addEventListener('resize', () => { clampCamera(); commitTransform(false); updateMinimap(); });
+
+document.getElementById('btn-home').addEventListener('click', flyToMap);
+document.getElementById('btn-current').addEventListener('click', () => centerOnPortal(unlockedCount - 1, true));
+document.getElementById('btn-prev').addEventListener('click', () => centerOnPortal((currentPortalIdx - 1 + PORTALS.length) % PORTALS.length, true));
+document.getElementById('btn-next').addEventListener('click', () => centerOnPortal((currentPortalIdx + 1) % PORTALS.length, true));
+
+loadCourseData().then(() => {
+ if (COURSE_DATA && COURSE_DATA.sections) {
+ injectWorldDecorations();
+ document.querySelectorAll('.portal-wrap').forEach((el, i) => {
+ const sec = COURSE_DATA.sections[i];
+ if (sec) {
+ el.dataset.theme = sec.theme;
+ el.dataset.label = sec.label;
+ }
+ initPortal(el);
+ });
+ injectCurrentLessonBadge();
+ } else {
+ document.querySelectorAll('.portal-wrap').forEach(el => initPortal(el));
+ }
+});
+
+function injectCurrentLessonBadge() {
+ const savedSection = localStorage.getItem('asl_current_section');
+ const savedVideo = localStorage.getItem('asl_current_video');
+ if (!savedSection || !COURSE_DATA) return;
+
+ const secIdx = COURSE_DATA.sections.findIndex(s => s.key === savedSection);
+ if (secIdx < 0) return;
+
+ const section = COURSE_DATA.sections[secIdx];
+ let vidIdx = savedVideo ? section.videos.findIndex(v => v.id === savedVideo) : 0;
+ if (vidIdx < 0) vidIdx = 0;
+
+ const video = section.videos[vidIdx];
+ if (!video) return;
+
+ const wraps = document.querySelectorAll('.portal-wrap');
+ const targetWrap = wraps[secIdx];
+ if (!targetWrap || targetWrap.classList.contains('locked')) return;
+
+ const badge = document.createElement('div');
+ badge.className = 'portal-current-lesson';
+ badge.innerHTML = `
+ Current Lesson
+ ${video.title}
+ `;
+ badge.style.setProperty('--pcl-color', section.color);
+ targetWrap.appendChild(badge);
+}
+
+function injectWorldDecorations() {
+ const sections = COURSE_DATA.sections;
+ const positions = PORTALS;
+ const svg = document.getElementById('path-svg');
+
+ let svgContent = '';
+
+ const pathControlPoints = [
+ { cx1: 1400, cy1: 2750, cx2: 2200, cy2: 2700 },
+ { cx1: 2400, cy1: 2500, cx2: 1400, cy2: 2200 },
+ { cx1: 1300, cy1: 1950, cx2: 1900, cy2: 1850 },
+ { cx1: 1800, cy1: 1700, cx2: 900, cy2: 1600 },
+ { cx1: 800, cy1: 1300, cx2: 2000, cy2: 1150 },
+ { cx1: 2400, cy1: 900, cx2: 1900, cy2: 600 },
+ ];
+
+ for (let i = 0; i < positions.length - 1; i++) {
+ const a = positions[i], b = positions[i + 1];
+ const rgb = sections[i].portalColor;
+ const cp = pathControlPoints[i];
+ svgContent += `\n`;
+ }
+
+ positions.forEach((p, i) => {
+ const rgb = sections[i].portalColor;
+ const isTop = i === sections.length - 1;
+ const op1 = isTop ? 0.24 : 0.22, op2 = isTop ? 0.24 : 0.14;
+ const sw1 = 1.6, sw2 = isTop ? 1.6 : 1.0;
+ if (isTop) {
+ svgContent += `
+
+\n`;
+ } else {
+ svgContent += `
+\n`;
+ }
+ });
+
+ svg.innerHTML = svgContent;
+}
+
+(function() {
+ const canvas = document.getElementById('star-canvas');
+ const ctx = canvas.getContext('2d');
+ let W, H, stars = [], shootingStars = [];
+ function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
+ window.addEventListener('resize', resize); resize();
+ function initStars() {
+ stars = [];
+ const count = Math.floor((W * H) / 2200);
+ for (let i = 0; i < count; i++) {
+ const tier = Math.random();
+ stars.push({
+ x: Math.random() * W, y: Math.random() * H,
+ r: tier > 0.92 ? 1.4 + Math.random() * 0.8 : tier > 0.7 ? 0.8 + Math.random() * 0.5 : 0.3 + Math.random() * 0.4,
+ baseAlpha: 0.15 + Math.random() * 0.75,
+ twinkleSpeed: 0.5 + Math.random() * 2.5,
+ twinkleOffset: Math.random() * Math.PI * 2,
+ color: (() => { const r = Math.random(); return r>0.93?[180,200,255]:r>0.87?[255,240,200]:r>0.82?[200,160,255]:[220,220,240]; })(),
+ });
+ }
+ }
+ initStars(); window.addEventListener('resize', initStars);
+ function spawnShooting() {
+ if (shootingStars.length < 3 && Math.random() < 0.004) {
+ const angle = -0.3 + Math.random() * 0.4;
+ shootingStars.push({ x: Math.random()*W, y: Math.random()*H*0.5,
+ vx: Math.cos(angle)*(4+Math.random()*6), vy: Math.sin(angle)*(4+Math.random()*6)+1.5,
+ len: 60+Math.random()*100, alpha: 0.8+Math.random()*0.2, life: 1.0 });
+ }
+ }
+ let frame = 0;
+ function draw() {
+ ctx.clearRect(0,0,W,H);
+ const t = frame * 0.016;
+ stars.forEach(s => {
+ const tw = 0.5 + 0.5 * Math.sin(t * s.twinkleSpeed + s.twinkleOffset);
+ const al = s.baseAlpha * (0.45 + 0.55 * tw);
+ const [r,g,b] = s.color;
+ ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2);
+ ctx.fillStyle = `rgba(${r},${g},${b},${al.toFixed(3)})`; ctx.fill();
+ if (s.r > 1.0 && al > 0.5) {
+ const grd = ctx.createRadialGradient(s.x,s.y,0,s.x,s.y,s.r*3.5);
+ grd.addColorStop(0,`rgba(${r},${g},${b},${(al*0.3).toFixed(3)})`);
+ grd.addColorStop(1,`rgba(${r},${g},${b},0)`);
+ ctx.beginPath(); ctx.arc(s.x,s.y,s.r*3.5,0,Math.PI*2); ctx.fillStyle=grd; ctx.fill();
+ }
+ });
+ spawnShooting();
+ shootingStars = shootingStars.filter(s=>s.life>0.01);
+ shootingStars.forEach(s => {
+ s.x+=s.vx; s.y+=s.vy; s.life*=0.94;
+ const g=ctx.createLinearGradient(s.x-s.vx*s.len/6,s.y-s.vy*s.len/6,s.x,s.y);
+ g.addColorStop(0,`rgba(255,255,255,0)`);
+ g.addColorStop(1,`rgba(255,255,255,${(s.alpha*s.life).toFixed(3)})`);
+ ctx.beginPath(); ctx.moveTo(s.x-s.vx*s.len/6,s.y-s.vy*s.len/6); ctx.lineTo(s.x,s.y);
+ ctx.strokeStyle=g; ctx.lineWidth=1.5; ctx.stroke();
+ });
+ frame++; requestAnimationFrame(draw);
+ }
+ draw();
+ })();
+
+ (function() {
+ const layer = document.getElementById('ambient-layer');
+ const blobs = [
+ {color:'60,20,120',size:600,x:10,y:5,dur:38,op:[0.045,0.085],dx:['30px','-20px'],dy:['20px','40px']},
+ {color:'0,40,120',size:500,x:75,y:15,dur:52,op:[0.035,0.065],dx:['-40px','15px'],dy:['30px','-25px']},
+ {color:'80,10,60',size:450,x:45,y:60,dur:45,op:[0.04,0.07],dx:['20px','-35px'],dy:['-30px','20px']},
+ {color:'20,80,60',size:400,x:20,y:80,dur:60,op:[0.025,0.055],dx:['35px','-10px'],dy:['-20px','30px']},
+ {color:'100,30,20',size:480,x:80,y:70,dur:42,op:[0.03,0.06],dx:['-25px','30px'],dy:['25px','-40px']},
+ {color:'40,60,140',size:550,x:55,y:35,dur:56,op:[0.03,0.055],dx:['15px','-30px'],dy:['-35px','15px']},
+ ];
+ blobs.forEach((b,i) => {
+ const el = document.createElement('div'); el.className = 'nebula-blob';
+ el.style.cssText = `width:${b.size}px;height:${b.size*0.7}px;left:${b.x}%;top:${b.y}%;
+ background:radial-gradient(ellipse at center,rgba(${b.color},0.18) 0%,rgba(${b.color},0.06) 50%,transparent 75%);
+ --nb-op-lo:${b.op[0]};--nb-op-hi:${b.op[1]};--dx1:${b.dx[0]};--dy1:${b.dy[0]};--dx2:${b.dx[1]};--dy2:${b.dy[1]};
+ animation-duration:${b.dur}s;animation-delay:${-i*7.3}s;opacity:${b.op[0]};`;
+ layer.appendChild(el);
+ });
+ })();
+
+ (function() {
+ const colors=['140,60,255','34,221,136','68,153,255','255,170,34','255,85,51','102,204,0'];
+ for (let i=0;i<28;i++) {
+ const el=document.createElement('div'); el.className='dust-mote';
+ const size=1+Math.random()*2.5, color=colors[Math.floor(Math.random()*colors.length)];
+ const op=0.1+Math.random()*0.3, dur=12+Math.random()*24;
+ el.style.cssText=`width:${size}px;height:${size}px;left:${Math.random()*100}%;top:${20+Math.random()*70}%;
+ background:rgba(${color},0.8);box-shadow:0 0 ${size*3}px rgba(${color},0.6);
+ --mote-op:${op};--mote-dx:${(Math.random()-0.5)*80}px;--mote-dy:${-40-Math.random()*120}px;
+ animation-duration:${dur}s;animation-delay:${-Math.random()*dur}s;filter:blur(${size>2?0.5:0}px);`;
+ document.body.appendChild(el);
+ }
+ })();
\ No newline at end of file
diff --git a/projecthearthstone.in/Courses/ASL course/style.css b/projecthearthstone.in/Courses/ASL course/style.css
new file mode 100644
index 0000000..a1ddd45
--- /dev/null
+++ b/projecthearthstone.in/Courses/ASL course/style.css
@@ -0,0 +1,364 @@
+*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
+
+html, body {
+ width: 100%; height: 100%;
+ background: #06060b;
+ overflow: hidden;
+}
+
+#viewport { top: 0 !important; }
+ #progress-bar { top: 50%; }
+ #nav-cluster { bottom: 22px; }
+ #minimap { bottom: 22px; }
+
+.flash-overlay {
+ position: fixed; inset: 0;
+ pointer-events: none; opacity: 0; z-index: 999;
+}
+.flash-overlay.flash { animation: flashOut 0.45s ease-out forwards; }
+@keyframes flashOut { 0% { opacity: 0.28; } 100% { opacity: 0; } }
+
+#viewport {
+ position: fixed; inset: 0;
+ overflow: hidden;
+ cursor: grab;
+ user-select: none; -webkit-user-select: none;
+ z-index: 2;
+}
+#viewport.panning { cursor: grabbing; }
+
+#world {
+ position: absolute;
+ width: 3200px; height: 3200px;
+ top: 0; left: 0;
+ will-change: transform;
+}
+
+#tree-img {
+ position: absolute;
+ width: 3100px; height: auto;
+ left: 50px; top: 0px;
+ pointer-events: none;
+ user-select: none; -webkit-user-drag: none;
+ filter: brightness(0.80) saturate(0.88);
+}
+
+#path-svg {
+ position: absolute; top: 0; left: 0;
+ width: 3200px; height: 3200px;
+ pointer-events: none; overflow: visible;
+}
+
+.portal-aura {
+ position: absolute;
+ transform: translate(-50%, -50%);
+ width: 260px; height: 260px;
+ border-radius: 50%;
+ pointer-events: none;
+ z-index: 1;
+}
+.portal-aura.boss-aura { width: 360px; height: 360px; }
+
+.portal-wrap {
+ position: absolute;
+ transform: translate(-50%, -50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+ z-index: 10;
+}
+
+.portal-wrap.boss .portal-svg {
+ width: 280px !important;
+ height: 280px !important;
+}
+
+.portal-svg {
+ cursor: pointer;
+ overflow: visible;
+ will-change: transform, filter;
+ display: block;
+}
+.portal-wrap.locked .portal-svg {
+ cursor: default;
+ filter: none !important;
+ transform: none !important;
+}
+
+.portal-label {
+ font-size: 9px;
+ letter-spacing: 0.35em;
+ text-transform: uppercase;
+ color: rgba(255,255,255,0.30);
+ transition: color 0.4s, letter-spacing 0.4s;
+ pointer-events: none;
+ white-space: nowrap;
+}
+.portal-wrap.hovered .portal-label {
+ color: rgba(255,255,255,0.75);
+ letter-spacing: 0.42em;
+}
+
+.portal-theme-tag { display: none; }
+
+.portal-lock-icon {
+ transition: transform 0.35s cubic-bezier(0.34,1.6,0.64,1), opacity 0.3s;
+ transform-origin: 120px 120px;
+ pointer-events: none;
+}
+.portal-wrap.locked:hover .portal-lock-icon {
+ transform: scale(1.18);
+ opacity: 1 !important;
+}
+
+#hud-title {
+ position: fixed;
+ top: 20px; left: 50%;
+ transform: translateX(-50%);
+ font-size: 10px;
+ letter-spacing: 0.55em;
+ text-transform: uppercase;
+ color: rgba(255,255,255,0.12);
+ pointer-events: none;
+ z-index: 200;
+ white-space: nowrap;
+ text-shadow: 0 0 20px rgba(140,60,255,0.3), 0 0 40px rgba(140,60,255,0.12);
+ animation: titleBreath 6s ease-in-out infinite;
+}
+@keyframes titleBreath {
+ 0%, 100% { opacity: 0.12; }
+ 50% { opacity: 0.22; }
+}
+
+#progress-bar {
+ position: fixed;
+ right: 20px; top: 50%;
+ transform: translateY(-50%);
+ display: flex; flex-direction: column; gap: 9px;
+ z-index: 200; pointer-events: none;
+}
+.pdot {
+ width: 5px; height: 5px; border-radius: 50%;
+ background: rgba(255,255,255,0.08);
+ border: 0.5px solid rgba(255,255,255,0.18);
+ transition: background 0.35s, box-shadow 0.35s;
+}
+.pdot.on {
+ background: rgba(255,255,255,0.55);
+ animation: dotPulse 3s ease-in-out infinite;
+}
+@keyframes dotPulse {
+ 0%, 100% { box-shadow: 0 0 5px rgba(255,255,255,0.25); }
+ 50% { box-shadow: 0 0 10px rgba(255,255,255,0.55); }
+}
+
+#nav-cluster {
+ position: fixed;
+ bottom: 22px; left: 20px;
+ display: flex; flex-direction: column; gap: 8px;
+ z-index: 200;
+}
+
+.nav-row { display: flex; gap: 6px; align-items: center; }
+
+.nav-btn {
+ display: flex; align-items: center; gap: 6px;
+ padding: 0 16px;
+ height: 38px;
+ background: rgba(255,255,255,0.10);
+ border: 1px solid rgba(255,255,255,0.28);
+ border-radius: 8px;
+ color: rgba(255,255,255,0.75);
+ font-size: 9px;
+ letter-spacing: 0.28em;
+ text-transform: uppercase;
+ cursor: pointer;
+ transition: background 0.2s, color 0.2s, border-color 0.2s, transform 0.12s;
+ white-space: nowrap;
+ outline: none;
+}
+.nav-btn:hover {
+ background: rgba(255,255,255,0.18);
+ border-color: rgba(255,255,255,0.55);
+ color: rgba(255,255,255,1);
+ box-shadow: 0 0 12px rgba(255,255,255,0.08);
+}
+.nav-btn:active { transform: scale(0.96); }
+
+.nav-btn.accent {
+ border-color: rgba(140,60,255,0.4);
+ color: rgba(180,130,255,0.7);
+}
+.nav-btn.accent:hover {
+ background: rgba(140,60,255,0.12);
+ border-color: rgba(180,130,255,0.6);
+ color: rgba(210,175,255,0.95);
+}
+
+.nav-btn.nav-arrow {
+ padding: 0 12px;
+ min-width: 70px;
+ justify-content: center;
+}
+
+.nav-index {
+ font-size: 10px;
+ letter-spacing: 0.2em;
+ color: rgba(255,255,255,0.55);
+ min-width: 36px;
+ text-align: center;
+ pointer-events: none;
+}
+
+#minimap {
+ position: fixed;
+ bottom: 22px; right: 20px;
+ width: 88px; height: 88px;
+ background: rgba(0,0,0,0.55);
+ border: 0.5px solid rgba(255,255,255,0.1);
+ border-radius: 6px;
+ overflow: hidden;
+ z-index: 200;
+ pointer-events: none;
+}
+#mm-viewport-box {
+ position: absolute;
+ border: 1px solid rgba(255,255,255,0.35);
+ border-radius: 2px;
+ background: rgba(255,255,255,0.04);
+ pointer-events: none;
+}
+.mm-dot {
+ position: absolute;
+ width: 4px; height: 4px;
+ border-radius: 50%;
+ transform: translate(-50%,-50%);
+}
+
+#world.flying {
+ transition: transform 0.75s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+#viewport.overview-mode { cursor: default; }
+#viewport.overview-mode #world {
+ transition: transform 0.9s cubic-bezier(0.4, 0, 0.2, 1) !important;
+}
+#world.overview-scaled {
+ transform-origin: top left;
+ transition: transform 0.9s cubic-bezier(0.4, 0, 0.2, 1) !important;
+}
+
+#star-canvas {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+ opacity: 0.85;
+}
+
+#ambient-layer {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+ overflow: hidden;
+}
+
+.nebula-blob {
+ position: absolute;
+ border-radius: 50%;
+ filter: blur(80px);
+ mix-blend-mode: screen;
+ animation: nebulaFloat linear infinite;
+ will-change: transform, opacity;
+}
+@keyframes nebulaFloat {
+ 0% { transform: translate(0, 0) scale(1); opacity: var(--nb-op-lo); }
+ 33% { transform: translate(var(--dx1), var(--dy1)) scale(1.08); opacity: var(--nb-op-hi); }
+ 66% { transform: translate(var(--dx2), var(--dy2)) scale(0.96); opacity: var(--nb-op-lo); }
+ 100% { transform: translate(0, 0) scale(1); opacity: var(--nb-op-lo); }
+}
+
+.dust-mote {
+ position: fixed;
+ border-radius: 50%;
+ pointer-events: none;
+ z-index: 1;
+ animation: dustFloat linear infinite;
+ will-change: transform, opacity;
+}
+@keyframes dustFloat {
+ 0% { transform: translateY(0px) translateX(0px); opacity: 0; }
+ 10% { opacity: var(--mote-op); }
+ 90% { opacity: var(--mote-op); }
+ 100% { transform: translateY(var(--mote-dy)) translateX(var(--mote-dx)); opacity: 0; }
+}
+
+#vignette {
+ position: fixed;
+ inset: 0;
+ background: radial-gradient(ellipse at 50% 50%,
+ transparent 30%,
+ rgba(0,0,0,0.25) 65%,
+ rgba(0,0,0,0.65) 100%);
+ pointer-events: none;
+ z-index: 190;
+}
+
+#scanlines {
+ position: fixed;
+ inset: 0;
+ background: repeating-linear-gradient(
+ 0deg,
+ transparent,
+ transparent 2px,
+ rgba(0,0,0,0.04) 2px,
+ rgba(0,0,0,0.04) 4px
+ );
+ pointer-events: none;
+ z-index: 191;
+}
+
+#hud-title, #progress-bar, #nav-cluster, #minimap { z-index: 200; }
+
+@keyframes unlockPulse {
+ 0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.4); }
+ 70% { box-shadow: 0 0 0 40px rgba(255,255,255,0); }
+ 100% { box-shadow: 0 0 0 0 rgba(255,255,255,0); }
+}
+
+.portal-current-lesson {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 3px;
+ padding: 7px 14px;
+ background: rgba(6,6,11,0.82);
+ border: 1px solid var(--pcl-color, rgba(255,255,255,0.2));
+ border-radius: 8px;
+ pointer-events: none;
+ white-space: nowrap;
+ box-shadow: 0 0 18px color-mix(in srgb, var(--pcl-color, white) 30%, transparent);
+ animation: pclFadeIn 0.5s ease both;
+}
+@keyframes pclFadeIn {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+.pcl-eyebrow {
+ font-size: 7px;
+ letter-spacing: 0.32em;
+ text-transform: uppercase;
+ color: var(--pcl-color, rgba(255,255,255,0.4));
+ opacity: 0.65;
+}
+.pcl-title {
+ font-size: 9px;
+ letter-spacing: 0.12em;
+ color: rgba(255,255,255,0.82);
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/projecthearthstone.in/login/get_database.js b/projecthearthstone.in/login/get_database.js
new file mode 100644
index 0000000..e69de29
diff --git a/projecthearthstone.in/login/login.html b/projecthearthstone.in/login/login.html
new file mode 100644
index 0000000..e69de29