Spaces:
Running
Running
| // DepthMap Trait Architect - Core Logic | |
| // ------------------------------ | |
| // Data Model | |
| // ------------------------------ | |
| const TRAITS = [ | |
| { | |
| id: 'curiosity', | |
| name: 'Curiosity', | |
| description: 'Your drive to explore, ask questions, and seek novelty.', | |
| growthPaths: [ | |
| 'Schedule a weekly micro-adventure outside your routine.', | |
| 'Ask one “why” or “how” question before moving to solutions.', | |
| 'Follow a topic for 20 minutes/day until you see progress.' | |
| ], | |
| prompts: [ | |
| 'What topic have you always wanted to explore but never started?', | |
| 'What would change if you replaced one habitual task with a small experiment?' | |
| ] | |
| }, | |
| { | |
| id: 'discipline', | |
| name: 'Discipline', | |
| description: 'Your capacity to follow through with commitments despite mood or temptation.', | |
| growthPaths: [ | |
| 'Define a 5-minute “start ritual” for key habits.', | |
| 'Tie a friction to the undesired behavior and a reward to the desired one.', | |
| 'Use calendar blocking for your top 3 priorities.' | |
| ], | |
| prompts: [ | |
| 'Which small daily action would create the most compound progress?', | |
| 'What is the smallest unit of your goal you can commit to consistently?' | |
| ] | |
| }, | |
| { | |
| id: 'integrity', | |
| name: 'Integrity', | |
| description: 'Your alignment between values, decisions, and actions.', | |
| growthPaths: [ | |
| 'Write down your top 3 values and review weekly.', | |
| 'Say “no” to one misaligned request this week.', | |
| 'Make one promise today and keep it exactly as stated.' | |
| ], | |
| prompts: [ | |
| 'Where do you feel most authentic in your current routines?', | |
| 'What would living by your values look like in the next 30 days?' | |
| ] | |
| }, | |
| { | |
| id: 'boldness', | |
| name: 'Boldness', | |
| description: 'Your willingness to act decisively, take risks, and lead.', | |
| growthPaths: [ | |
| 'Pitch one idea in the next 48 hours.', | |
| 'Publicly commit to a small but meaningful milestone.', | |
| 'Have one uncomfortable conversation you’ve been avoiding.' | |
| ], | |
| prompts: [ | |
| 'What would you attempt if failure were safe and instructive?', | |
| 'Which decision, made today, would future you thank you for?' | |
| ] | |
| }, | |
| { | |
| id: 'empathy', | |
| name: 'Empathy', | |
| description: 'Your ability to understand, relate to, and care for others.', | |
| growthPaths: [ | |
| 'Practice “perspective labeling” in one conversation daily.', | |
| 'Ask “what matters to you?” before “what do you think?”', | |
| 'Write a thank-you note recognizing someone’s specific contribution.' | |
| ], | |
| prompts: [ | |
| 'Where are others likely feeling unseen?', | |
| 'How would you support your past self in your current situation?' | |
| ] | |
| }, | |
| { | |
| id: 'resilience', | |
| name: 'Resilience', | |
| description: 'Your capacity to recover, adapt, and grow from setbacks.', | |
| growthPaths: [ | |
| 'Design a “restart routine” for after tough days.', | |
| 'List three internal resources you can调用 in stress.', | |
| 'Celebrate one small recovery win daily.' | |
| ], | |
| prompts: [ | |
| 'What have you bounced back from that you’re proud of?', | |
| 'What would a resilient week look like if it started tomorrow?' | |
| ] | |
| }, | |
| { | |
| id: 'focus', | |
| name: 'Focus', | |
| description: 'Your ability to concentrate deeply and protect attention.', | |
| growthPaths: [ | |
| 'Choose a single task and timebox 25 minutes.', | |
| 'Turn off non-critical notifications for deep work blocks.', | |
| 'Create a visible “focus cue” to signal start and end.' | |
| ], | |
| prompts: [ | |
| 'What would you accomplish if you protected one hour daily for 7 days?', | |
| 'What pulls your attention most often, and how can you redesign that?' | |
| ] | |
| }, | |
| { | |
| id: 'creativity', | |
| name: 'Creativity', | |
| description: 'Your ability to generate novel, useful ideas and solutions.', | |
| growthPaths: [ | |
| 'Generate 10 rough ideas before judging any.', | |
| 'Recombine two unrelated concepts into a new idea.', | |
| 'Build a tiny prototype to test one idea this week.' | |
| ], | |
| prompts: [ | |
| 'What constraints, removed, would unlock new options for you?', | |
| 'How can you make space for “play” in your problem-solving?' | |
| ] | |
| } | |
| ]; | |
| const QUESTIONS = [ | |
| // Curiosity | |
| { id: 'q1', text: 'I enjoy exploring new topics for fun.', traitId: 'curiosity' }, | |
| { id: 'q2', text: 'I frequently seek sources beyond my usual information diet.', traitId: 'curiosity' }, | |
| // Discipline | |
| { id: 'q3', text: 'I follow through on commitments even when I don’t feel like it.', traitId: 'discipline' }, | |
| { id: 'q4', text: 'I prefer systems and routines that reduce decision fatigue.', traitId: 'discipline' }, | |
| // Integrity | |
| { id: 'q5', text: 'I act according to my values even when it’s inconvenient.', traitId: 'integrity' }, | |
| { id: 'q6', text: 'I keep promises to myself as seriously as to others.', traitId: 'integrity' }, | |
| // Boldness | |
| { id: 'q7', text: 'I am comfortable making decisions with incomplete information.', traitId: 'boldness' }, | |
| { id: 'q8', text: 'I volunteer to lead or take point when needed.', traitId: 'boldness' }, | |
| // Empathy | |
| { id: 'q9', text: 'I regularly consider others’ perspectives and emotions.', traitId: 'empathy' }, | |
| { id: 'q10', text: 'I make space for people to feel heard and understood.', traitId: 'empathy' }, | |
| // Resilience | |
| { id: 'q11', text: 'I recover quickly from setbacks without spiraling.', traitId: 'resilience' }, | |
| { id: 'q12', text: 'I can reframe challenges into learnings or experiments.', traitId: 'resilience' }, | |
| // Focus | |
| { id: 'q13', text: 'I can concentrate for extended periods without distraction.', traitId: 'focus' }, | |
| { id: 'q14', text: 'I protect my attention deliberately, especially during deep work.', traitId: 'focus' }, | |
| // Creativity | |
| { id: 'q15', text: 'I enjoy brainstorming even when ideas are rough or weird.', traitId: 'creativity' }, | |
| { id: 'q16', text: 'I connect unrelated ideas to form novel solutions.', traitId: 'creativity' } | |
| ]; | |
| const ANSWER_OPTIONS = [ | |
| { label: 'Not at all', value: 0 }, | |
| { label: 'Slightly', value: 1 }, | |
| { label: 'Moderately', value: 2 }, | |
| { label: 'Very', value: 3 }, | |
| { label: 'Extremely', value: 4 } | |
| ]; | |
| // ------------------------------ | |
| // State | |
| // ------------------------------ | |
| const state = { | |
| step: 0, // hero: -1, test: 0..15, results: 16+ | |
| answers: new Array(QUESTIONS.length).fill(null), | |
| scores: {}, // { traitId: 0..23 } | |
| selfBiasScore: 0, // 0..1 | |
| deepUnlocked: false | |
| }; | |
| // Elements | |
| const heroEl = document.getElementById('hero'); | |
| const testEl = document.getElementById('test'); | |
| const mapEl = document.getElementById('map'); | |
| const hexGridEl = document.getElementById('hexGrid'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const loadDemoBtn = document.getElementById('loadDemoBtn'); | |
| const backToHeroBtn = document.getElementById('backToHero'); | |
| const prevBtn = document.getElementById('prevBtn'); | |
| const nextBtn = document.getElementById('nextBtn'); | |
| const progressEl = document.getElementById('progress'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const retakeBtn = document.getElementById('retakeBtn'); | |
| const exportBtn = document.getElementById('exportBtn'); | |
| const panel = document.getElementById('panel'); | |
| const panelBackdrop = document.getElementById('panelBackdrop'); | |
| const panelContent = document.getElementById('panelContent'); | |
| const panelClose = document.getElementById('panelClose'); | |
| const panelTitle = document.getElementById('panelTitle'); | |
| const panelSubtitle = document.getElementById('panelSubtitle'); | |
| const panelDesc = document.getElementById('panelDesc'); | |
| const panelScore = document.getElementById('panelScore'); | |
| const panelScoreBar = document.getElementById('panelScoreBar'); | |
| const panelGrowth = document.getElementById('panelGrowth'); | |
| const panelPrompts = document.getElementById('panelPrompts'); | |
| const panelBadge = document.getElementById('panelBadge'); | |
| const panelUnlock = document.getElementById('panelUnlock'); | |
| const panelUnlockText = document.getElementById('panelUnlockText'); | |
| const insightBanner = document.getElementById('insightBanner'); | |
| const insightText = document.getElementById('insightText'); | |
| // ------------------------------ | |
| // Helpers | |
| // ------------------------------ | |
| const clamp = (v, min, max) => Math.min(max, Math.max(min, v)); | |
| const sum = (arr) => arr.reduce((a,b) => a+b, 0); | |
| function formatNumber(n, digits=0) { | |
| return Number(n).toFixed(digits); | |
| } | |
| function computeScores() { | |
| // Map answers (0..4) per question -> scaled to (0..23) for 2 questions per trait | |
| const traitTotals = {}; | |
| const traitCounts = {}; | |
| state.answers.forEach((val, idx) => { | |
| const q = QUESTIONS[idx]; | |
| const v = val ?? 0; | |
| traitTotals[q.traitId] = (traitTotals[q.traitId] ?? 0) + v; | |
| traitCounts[q.traitId] = (traitCounts[q.traitId] ?? 0) + 1; | |
| }); | |
| const scores = {}; | |
| for (const trait of TRAITS) { | |
| const avg = (traitTotals[trait.id] ?? 0) / (traitCounts[trait.id] || 1); | |
| scores[trait.id] = Math.round(avg * (23/4)); // 0..23 | |
| } | |
| state.scores = scores; | |
| } | |
| function computeSelfBias() { | |
| // Count responses 3 or 4 (Very/Extremely) | |
| const high = state.answers.filter(v => v === 3 || v === 4).length; | |
| state.selfBiasScore = (state.answers.length ? high / state.answers.length : 0); | |
| state.deepUnlocked = state.selfBiasScore < 0.6; // unlocked if <60% high responses | |
| } | |
| // HSL gradient by score: high=gold, mid=blue, low=desaturated | |
| function scoreToHSL(score) { | |
| const s = clamp(score / 23, 0, 1); | |
| let hue, sat, light; | |
| if (s >= 0.6) { | |
| // Gold tones (45..35) | |
| const t = (s - 0.6) / 0.4; | |
| hue = 45 - 10 * t; | |
| sat = 75 + 10 * t; // 75..85 | |
| light = 52 - 7 * t; // 52..45 | |
| } else if (s >= 0.3) { | |
| // Blues (210..230) | |
| const t = (s - 0.3) / 0.3; | |
| hue = 210 + 20 * t; // 210..230 | |
| sat = 70 + 5 * t; // 70..75 | |
| light = 52 + 3 * t; // 52..55 | |
| } else { | |
| // Desaturated blue-gray (210..220) | |
| const t = s / 0.3; | |
| hue = 210 + 10 * t; | |
| sat = 10 + 10 * t; // 10..20 | |
| light = 40 + 5 * t; // 40..45 | |
| } | |
| return { hue, sat, light, str: `hsl(${hue} ${sat}% ${light}%)` }; | |
| } | |
| function scoreLabel(score) { | |
| if (score >= 16) return 'High'; | |
| if (score >= 8) return 'Balanced'; | |
| return 'Emerging'; | |
| } | |
| function showSection(section) { | |
| heroEl.classList.add('hidden'); | |
| testEl.classList.add('hidden'); | |
| mapEl.classList.add('hidden'); | |
| if (section === 'hero') heroEl.classList.remove('hidden'); | |
| if (section === 'test') testEl.classList.remove('hidden'); | |
| if (section === 'map') mapEl.classList.remove('hidden'); | |
| } | |
| // ------------------------------ | |
| // Renderers | |
| // ------------------------------ | |
| function renderTest() { | |
| progressEl.textContent = `${Math.max(0, state.step + 1)} / ${QUESTIONS.length}`; | |
| const q = QUESTIONS[state.step]; | |
| const container = document.getElementById('questions'); | |
| container.innerHTML = ''; | |
| const block = document.createElement('div'); | |
| block.className = 'rounded-xl border border-white/10 bg-white/5 p-6'; | |
| const header = document.createElement('div'); | |
| header.className = 'flex items-start justify-between gap-3 mb-4'; | |
| header.innerHTML = ` | |
| <div> | |
| <div class="text-sm text-slate-400">Question ${state.step + 1} of ${QUESTIONS.length}</div> | |
| <h3 class="text-xl font-semibold mt-1">${q.text}</h3> | |
| </div> | |
| <div class="px-2 py-1 text-xs rounded-md bg-white/5 border border-white/10 text-slate-300"> | |
| ${TRAITS.find(t => t.id === q.traitId)?.name ?? ''} | |
| </div> | |
| `; | |
| block.appendChild(header); | |
| const options = document.createElement('div'); | |
| options.className = 'grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3'; | |
| ANSWER_OPTIONS.forEach((opt, idx) => { | |
| const id = `opt-${state.step}-${idx}`; | |
| const checked = state.answers[state.step] === opt.value; | |
| const item = document.createElement('label'); | |
| item.setAttribute('for', id); | |
| item.className = `flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition | |
| ${checked | |
| ? 'border-gold-500/60 bg-gold-500/10' | |
| : 'border-white/10 bg-white/5 hover:bg-white/10'}`; | |
| item.innerHTML = ` | |
| <input id="${id}" type="radio" name="q-${state.step}" value="${opt.value}" ${checked ? 'checked' : ''} class="sr-only" /> | |
| <div class="w-5 h-5 rounded-full border ${checked ? 'border-gold-500' : 'border-white/30'} flex items-center justify-center"> | |
| <div class="w-2.5 h-2.5 rounded-full ${checked ? 'bg-gold-500' : 'bg-transparent'}"></div> | |
| </div> | |
| <div class="text-sm">${opt.label}</div> | |
| `; | |
| item.addEventListener('click', () => { | |
| state.answers[state.step] = opt.value; | |
| renderTest(); // re-render to reflect selection | |
| }); | |
| options.appendChild(item); | |
| }); | |
| block.appendChild(options); | |
| container.appendChild(block); | |
| // Controls | |
| prevBtn.disabled = state.step === 0; | |
| nextBtn.textContent = state.step === QUESTIONS.length - 1 ? 'Finish' : 'Next'; | |
| } | |
| function renderMap() { | |
| // Compute scores if not already | |
| computeScores(); | |
| computeSelfBias(); | |
| // Insight banner | |
| if (state.selfBiasScore >= 0.85) { | |
| insightBanner.classList.remove('hidden'); | |
| insightText.innerHTML = ` | |
| Self-Reflection Prompt: Your responses trend very high. Before unlocking deeper growth paths, consider: | |
| “Which areas feel genuinely strong right now, and which might I be overlooking or minimizing?” | |
| `; | |
| } else if (!state.deepUnlocked) { | |
| insightBanner.classList.remove('hidden'); | |
| insightText.textContent = `Your responses suggest sincere reflection. Keep exploring — deeper pathways will open soon.`; | |
| } else { | |
| insightBanner.classList.add('hidden'); | |
| } | |
| // Build hexes in a simple honeycomb layout (3 rows x 3 cols) | |
| // Adjust columns/rows responsively | |
| const cols = 3; | |
| const rows = 3; | |
| const colGap = 28; | |
| const rowGap = 24; | |
| // Dynamic hex size by container | |
| const containerWidth = hexGridEl.clientWidth || 800; | |
| const hexW = Math.min(160, Math.max(120, Math.floor(containerWidth / (cols + 0.5)))); | |
| const hexH = Math.floor(hexW * 1.15); | |
| const stepX = Math.floor(hexW * 0.75 + colGap); | |
| const stepY = Math.floor(hexH * 0.5 + rowGap); | |
| const width = (cols - 1) * stepX + hexW + 32; | |
| const height = (rows - 1) * stepY + hexH + 32; | |
| hexGridEl.innerHTML = ''; | |
| hexGridEl.style.width = `${width}px`; | |
| hexGridEl.style.height = `${height}px`; | |
| // Center within container | |
| const offsetX = Math.floor((hexGridEl.clientWidth - width) / 2); | |
| const offsetY = 12; | |
| const orderedTraits = [...TRAITS]; // maintain order | |
| orderedTraits.forEach((trait, i) => { | |
| const col = i % cols; | |
| const row = Math.floor(i / cols); | |
| const left = offsetX + col * stepX + 16; | |
| const top = offsetY + row * stepY + (col % 2 === 1 ? Math.floor(stepY / 2) : 0); | |
| const score = state.scores[trait.id] ?? 0; | |
| const { hue, sat, light, str } = scoreToHSL(score); | |
| const label = scoreLabel(score); | |
| const hex = document.createElement('button'); | |
| hex.className = 'hex'; | |
| hex.style.left = `${left}px`; | |
| hex.style.top = `${top}px`; | |
| hex.style.width = `${hexW}px`; | |
| hex.style.height = `${hexH}px`; | |
| hex.setAttribute('aria-label', `${trait.name}, score ${score}, ${label}`); | |
| hex.setAttribute('data-trait', trait.id); | |
| // Visual layers | |
| const ring = document.createElement('div'); | |
| ring.className = 'hex-ring'; | |
| ring.style.background = `linear-gradient(180deg, ${str}, rgba(0,0,0,0))`; | |
| const inner = document.createElement('div'); | |
| inner.className = 'hex-inner'; | |
| inner.style.background = `linear-gradient(180deg, ${str}, rgba(255,255,255,0.15))`; | |
| inner.style.border = '1px solid rgba(255,255,255,0.18)'; | |
| inner.style.boxShadow = `inset 0 0 0 1px rgba(255,255,255,0.08), 0 12px 30px -16px ${str}`; | |
| const labelEl = document.createElement('div'); | |
| labelEl.className = 'hex-label'; | |
| labelEl.textContent = trait.name; | |
| const scoreEl = document.createElement('div'); | |
| scoreEl.className = 'hex-score'; | |
| scoreEl.textContent = `${score}`; | |
| // Reveal score only if deep unlocked | |
| if (state.deepUnlocked) { | |
| hex.classList.add('revealed'); | |
| } | |
| inner.appendChild(labelEl); | |
| inner.appendChild(scoreEl); | |
| hex.appendChild(ring); | |
| hex.appendChild(inner); | |
| // Click handler to open panel | |
| hex.addEventListener('click', () => openPanel(trait.id)); | |
| hexGridEl.appendChild(hex); | |
| }); | |
| } | |
| function openPanel(traitId) { | |
| const trait = TRAITS.find(t => t.id === traitId); | |
| const score = state.scores[traitId] ?? 0; | |
| const { str } = scoreToHSL(score); | |
| const label = scoreLabel(score); | |
| panelTitle.textContent = trait.name; | |
| panelSubtitle.textContent = `${label} • ${score}/23`; | |
| panelDesc.textContent = trait.description; | |
| panelScore.textContent = `${score} / 23`; | |
| panelScoreBar.style.width = `${Math.round((score / 23) * 100)}%`; | |
| panelScoreBar.style.background = str; | |
| panelBadge.style.background = str; | |
| // Growth paths and prompts | |
| panelGrowth.innerHTML = ''; | |
| panelPrompts.innerHTML = ''; | |
| if (state.deepUnlocked) { | |
| trait.growthPaths.forEach(g => { | |
| const li = document.createElement('li'); | |
| li.textContent = g; | |
| panelGrowth.appendChild(li); | |
| }); | |
| trait.prompts.forEach(p => { | |
| const li = document.createElement('li'); | |
| li.textContent = p; | |
| panelPrompts.appendChild(li); | |
| }); | |
| panelUnlock.classList.add('hidden'); | |
| } else { | |
| // Lock deeper content; show reflection prompt | |
| panelGrowth.innerHTML = ` | |
| <li class="text-slate-300/80">Unlock deeper guidance through sincere self-assessment.</li> | |
| `; | |
| panelPrompts.innerHTML = ` | |
| <li class="text-slate-300/80">What would someone who knows you well say is your biggest opportunity right now?</li> | |
| `; | |
| panelUnlock.classList.remove('hidden'); | |
| panelUnlockText.innerHTML = ` | |
| Depth unlocks when your ratings reflect a mix of genuine strengths and growth edges. | |
| Consider reassessing with nuanced honesty. | |
| `; | |
| } | |
| // Open panel | |
| panelBackdrop.classList.remove('opacity-0'); | |
| panelBackdrop.classList.add('opacity-100'); | |
| panelContent.classList.remove('translate-x-full'); | |
| } | |
| function closePanel() { | |
| panelBackdrop.classList.add('opacity-0'); | |
| panelBackdrop.classList.remove('opacity-100'); | |
| panelContent.classList.add('translate-x-full'); | |
| } | |
| // ------------------------------ | |
| // Navigation and Actions | |
| // ------------------------------ | |
| function startTest() { | |
| state.step = 0; | |
| state.answers = new Array(QUESTIONS.length).fill(null); | |
| showSection('test'); | |
| renderTest(); | |
| } | |
| function nextStep() { | |
| if (state.step < QUESTIONS.length - 1) { | |
| state.step++; | |
| renderTest(); | |
| } else { | |
| // Finish -> compute and show map | |
| computeScores(); | |
| computeSelfBias(); | |
| showSection('map'); | |
| renderMap(); | |
| } | |
| } | |
| function prevStep() { | |
| if (state.step > 0) { | |
| state.step--; | |
| renderTest(); | |
| } | |
| } | |
| function backToHero() { | |
| showSection('hero'); | |
| } | |
| function retakeAssessment() { | |
| state.step = 0; | |
| state.answers = new Array(QUESTIONS.length).fill(null); | |
| showSection('test'); | |
| renderTest(); | |
| } | |
| function resetAll() { | |
| state.step = 0; | |
| state.answers = new Array(QUESTIONS.length).fill(null); | |
| state.scores = {}; | |
| state.selfBiasScore = 0; | |
| state.deepUnlocked = false; | |
| showSection('hero'); | |
| } | |
| function loadDemo() { | |
| // A plausible distribution: mid-high on some, low on others | |
| const demoAnswers = [ | |
| 3, 2, // curiosity | |
| 2, 3, // discipline | |
| 1, 2, // integrity | |
| 3, 2, // boldness | |
| 3, 3, // empathy | |
| 2, 1, // resilience | |
| 2, 2, // focus | |
| 3, 2, // creativity | |
| ].slice(0, QUESTIONS.length); | |
| state.answers = demoAnswers.concat(new Array(Math.max(0, QUESTIONS.length - demoAnswers.length)).fill(1)); | |
| computeScores(); | |
| computeSelfBias(); | |
| showSection('map'); | |
| renderMap(); | |
| } | |
| function exportSnapshot() { | |
| const payload = { | |
| timestamp: new Date().toISOString(), | |
| selfBias: Number(state.selfBiasScore.toFixed(2)), | |
| deepUnlocked: state.deepUnlocked, | |
| traits: TRAITS.map(t => ({ | |
| id: t.id, name: t.name, score: state.scores[t.id] ?? 0 | |
| })) | |
| }; | |
| const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `depthmap_snapshot_${Date.now()}.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| a.remove(); | |
| } | |
| // ------------------------------ | |
| // Event Wiring | |
| // ------------------------------ | |
| startBtn.addEventListener('click', startTest); | |
| loadDemoBtn.addEventListener('click', loadDemo); | |
| backToHeroBtn.addEventListener('click', backToHero); | |
| prevBtn.addEventListener('click', prevStep); | |
| nextBtn.addEventListener('click', nextStep); | |
| resetBtn.addEventListener('click', resetAll); | |
| retakeBtn.addEventListener('click', retakeAssessment); | |
| exportBtn.addEventListener('click', exportSnapshot); | |
| panelClose.addEventListener('click', closePanel); | |
| panelBackdrop.addEventListener('click', closePanel); | |
| // Keyboard: ESC to close | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') closePanel(); | |
| }); | |
| // Handle resize for grid layout | |
| window.addEventListener('resize', () => { | |
| if (!mapEl.classList.contains('hidden')) { | |
| renderMap(); | |
| } | |
| }); | |
| // Initial section | |
| showSection('hero'); |