CoachIQ
AI-Powered Basketball Scouting

UPLOAD FILM. GET YOUR SCOUTING REPORT.

🎬 Get Started

HOW IT WORKS

Three simple steps to a game-ready scouting report

1
🎬

Upload Game Film

Upload

Upload your game video from any device

2
🏷️

We Tag & Analyze

Review

Our team tags the film and AI generates a detailed scouting report

3
📋

Get Your Report

Coach

Receive an approved scouting report with actionable insights for practice

READY TO COACH SMARTER?

Join coaches who are saving hours on game prep. Upload your first game film and get a scouting report.

See Pricing & Get Started

SEE EXACTLY WHAT YOUR COACHES RECEIVE

Every NextPossession report includes opponent tendencies, key player breakdowns, a 5-priority game plan, and a scripted 2-day practice plan — written in plain coaching language.

View Sample Report →

SIMPLE, TRANSPARENT PRICING

Expert-tagged film analysis. AI-generated coaching intelligence.

Note: One-time payment — your 6-month access starts immediately upon purchase. No auto-renewal.

SEASON PACKAGE

$850/6 mo

One-time payment · 6-month access · 1 Varsity team

  • 10 reports per month
  • AI-powered coaching intelligence
  • Tagged by championship coaches
  • Self Analysis reports included
  • Game plan + practice plan included
  • PDF download
🎬

UPLOAD GAME FILM

Upload your game video and get an AI-powered scouting report reviewed and approved by our team.

🎬 Upload Game Film
'); printWindow.document.close(); printWindow.print(); } function copyReportLink() { if (viewingReportId) { var link = window.location.origin + '/reports/' + viewingReportId; navigator.clipboard.writeText(link).then(function() { showToast('Link copied!'); }).catch(function() { showToast('Could not copy link', 'error'); }); } else { showToast('No report to share', 'error'); } } function showToast(msg, type) { var t = document.createElement('div'); t.className = 'toast ' + (type || 'success'); t.innerHTML = '' + msg + ''; document.getElementById('toast-container').appendChild(t); setTimeout(function() { t.remove(); }, 4000); } function showSection(s) { // Clear polling when navigating away if (reportPollInterval) { clearInterval(reportPollInterval); reportPollInterval = null; } isPolling = false; pollRetryCount = 0; document.querySelectorAll('[id$="-section"]').forEach(function(el) { el.classList.add('section-hidden'); }); var t = document.getElementById(s + '-section'); if (t) t.classList.remove('section-hidden'); if (s === 'dashboard') loadDashboard(); document.getElementById('user-dropdown').classList.remove('active'); window.scrollTo(0, 0); } function openAuthModal(mode) { authMode = mode || 'signup'; switchAuthTab(authMode); document.getElementById('auth-modal').classList.add('active'); } function closeAuthModal() { document.getElementById('auth-modal').classList.remove('active'); } function switchAuthTab(mode) { authMode = mode; document.getElementById('tab-login').classList.toggle('active', mode === 'login'); document.getElementById('tab-signup').classList.toggle('active', mode === 'signup'); document.getElementById('name-group').style.display = mode === 'signup' ? 'block' : 'none'; document.getElementById('level-group').style.display = mode === 'signup' ? 'block' : 'none'; document.getElementById('school-group').style.display = mode === 'signup' ? 'block' : 'none'; document.getElementById('auth-submit-btn').textContent = mode === 'login' ? 'Sign In' : 'Create Account'; } async function handleAuth(e) { e.preventDefault(); var email = document.getElementById('auth-email').value; var password = document.getElementById('auth-password').value; var name = document.getElementById('auth-name').value || 'Coach'; var coachingLevel = document.getElementById('auth-level').value || 'high_school'; var school = document.getElementById('auth-school').value || ''; if (supabaseClient) { try { var result; if (authMode === 'login') { result = await supabaseClient.auth.signInWithPassword({ email: email, password: password }); } else { result = await supabaseClient.auth.signUp({ email: email, password: password, options: { data: { name: name, school: school, coaching_level: coachingLevel }, }, }); } if (result.error) throw result.error; // For signup: create the user profile in the backend if (authMode === 'signup') { var session = result.data.session; // If email confirmation is required, save profile data // and let handleAuthSession complete it when user confirms if (!session) { localStorage.setItem('coachiq_pending_profile', JSON.stringify({ name: name, coaching_level: coachingLevel, school: school, })); closeAuthModal(); showToast('Check your email to confirm your account!'); return; } // Session available — create profile now var profileRes = await fetch(CONFIG.API_URL + '/api/auth/signup', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + session.access_token }, body: JSON.stringify({ name: name, coaching_level: coachingLevel, school: school }) }); if (!profileRes.ok) { var body = await profileRes.json().catch(function() { return {}; }); // 409 = profile already exists, that's fine if (profileRes.status !== 409) { throw new Error(body.error || 'Failed to create profile'); } } } closeAuthModal(); showToast(authMode === 'login' ? 'Welcome back!' : 'Account created!'); // Auth state change listener will handle nav update var redirect = sessionStorage.getItem('coachiq_redirect'); if (redirect) { sessionStorage.removeItem('coachiq_redirect'); window.location.href = redirect; } else if (authMode === 'signup') { window.location.href = '/checkout?plan=season'; } else { window.location.href = '/dashboard'; } } catch (err) { showToast(err.message || 'Authentication failed', 'error'); } } else { // Fallback: localStorage currentUser = { email: email, name: name, subscription: 'free' }; localStorage.setItem('coachiq_user', JSON.stringify(currentUser)); updateUserUI(); closeAuthModal(); showToast(authMode === 'login' ? 'Welcome back!' : 'Account created!'); var redirect = sessionStorage.getItem('coachiq_redirect'); if (redirect) { sessionStorage.removeItem('coachiq_redirect'); window.location.href = redirect; } else { window.location.href = '/dashboard'; } } } function updateUserUI() { // Legacy compat: delegate to auth-aware functions if (currentUser) { updateNavForAuth( { email: currentUser.email, user_metadata: { full_name: currentUser.name } }, subscriptionData ); } else { updateNavForGuest(); } } async function logout() { if (supabaseClient) { try { await supabaseClient.auth.signOut(); } catch (e) { console.error('Sign-out error:', e); } } currentUser = null; subscriptionData = null; localStorage.removeItem('coachiq_user'); localStorage.removeItem('coachiq_banner_dismissed'); localStorage.removeItem('coachiq_banner_scans'); updateNavForGuest(); document.getElementById('user-dropdown').classList.remove('active'); closeMobileMenu(); showSection('home'); showToast('Signed out'); } function toggleUserMenu() { document.getElementById('user-dropdown').classList.toggle('active'); } function togglePricingBtn(checkbox) { var btn = checkbox.closest('.pricing-card').querySelector('.btn'); if (checkbox.checked) { btn.classList.remove('btn-disabled-tos'); btn.style.pointerEvents = ''; btn.style.cursor = 'pointer'; } else { btn.classList.add('btn-disabled-tos'); btn.style.pointerEvents = 'none'; btn.style.cursor = ''; } } function subscribe(plan) { if (!currentUser) { openAuthModal('signup'); return; } window.location.href = '/checkout?plan=' + encodeURIComponent(plan); } // ── Waitlist Modal ── function openWaitlistModal() { document.getElementById('waitlist-form').style.display = ''; document.getElementById('waitlist-success').style.display = 'none'; document.getElementById('waitlist-form').reset(); document.getElementById('waitlist-modal').classList.add('active'); } function closeWaitlistModal() { document.getElementById('waitlist-modal').classList.remove('active'); } async function submitWaitlist(e) { e.preventDefault(); var btn = document.getElementById('waitlist-submit-btn'); btn.disabled = true; btn.textContent = 'Submitting...'; var data = { first_name: document.getElementById('waitlist-first-name').value.trim(), last_name: document.getElementById('waitlist-last-name').value.trim(), school_name: document.getElementById('waitlist-school').value.trim(), sport: document.getElementById('waitlist-sport').value, email: document.getElementById('waitlist-email').value.trim() }; try { if (!supabaseClient) throw new Error('Supabase not initialized'); var result = await supabaseClient.from('waitlist').insert([data]); if (result.error) throw result.error; document.getElementById('waitlist-form').style.display = 'none'; document.getElementById('waitlist-success').style.display = ''; } catch (err) { console.error('Waitlist submission error:', err); showToast('Something went wrong. Please try again.'); btn.disabled = false; btn.textContent = 'Join the Waitlist'; } } async function loadDashboard() { // Dashboard is now a simple CTA — no data to load } async function viewReport(reportId) { viewingReportId = reportId; // Clear any existing poll and reset state if (reportPollInterval) { clearInterval(reportPollInterval); reportPollInterval = null; } isPolling = false; pollRetryCount = 0; showSection('report'); document.getElementById('report-loading').style.display = 'block'; document.getElementById('report-processing').style.display = 'none'; document.getElementById('report-content').style.display = 'none'; document.getElementById('report-error').style.display = 'none'; await loadReport(reportId); } async function loadReport(reportId) { // Prevent overlapping requests if (isPolling) return; isPolling = true; try { var res = await fetch(CONFIG.API_URL + '/api/reports/' + reportId); if (!res.ok) { throw new Error('Server returned ' + res.status); } var data = await res.json(); document.getElementById('report-loading').style.display = 'none'; document.getElementById('view-report-title').textContent = '📋 ' + (data.opponentName || 'Report'); if (data.status === 'complete' && data.report) { // Stop polling - report is ready if (reportPollInterval) { clearInterval(reportPollInterval); reportPollInterval = null; } pollRetryCount = 0; displayReportContent(data.report); } else if (data.status === 'failed') { // Stop polling - report failed if (reportPollInterval) { clearInterval(reportPollInterval); reportPollInterval = null; } pollRetryCount = 0; document.getElementById('report-error').style.display = 'block'; document.getElementById('report-error-message').textContent = data.error || 'Analysis failed'; } else { // Still processing - show progress and continue polling document.getElementById('report-processing').style.display = 'block'; document.getElementById('report-processing-text').textContent = data.progressText || 'Processing...'; document.getElementById('report-progress-fill').style.width = (data.progress || 0) + '%'; pollRetryCount++; if (pollRetryCount >= MAX_POLL_RETRIES) { // Stop after max retries if (reportPollInterval) { clearInterval(reportPollInterval); reportPollInterval = null; } document.getElementById('report-processing-text').textContent = 'Still processing. Refresh page to check status.'; } else if (!reportPollInterval) { // Start polling if not already polling (5 second interval) reportPollInterval = setInterval(function() { loadReport(reportId); }, 5000); } } } catch (e) { console.error('Error loading report:', e); pollRetryCount++; if (pollRetryCount >= MAX_POLL_RETRIES) { if (reportPollInterval) { clearInterval(reportPollInterval); reportPollInterval = null; } document.getElementById('report-loading').style.display = 'none'; document.getElementById('report-error').style.display = 'block'; document.getElementById('report-error-message').textContent = 'Failed to load report. Please try again later.'; } // Otherwise let the interval retry } finally { isPolling = false; } } function displayReportContent(report) { // Store report data for PDF export currentReportData = report; // Stop any polling if (reportPollInterval) { clearInterval(reportPollInterval); reportPollInterval = null; } isPolling = false; pollRetryCount = 0; document.getElementById('report-processing').style.display = 'none'; document.getElementById('report-content').style.display = 'block'; document.getElementById('report-date').textContent = 'Generated ' + new Date(report.generatedAt).toLocaleDateString(); // Opponent Banner document.getElementById('opponent-banner-name').textContent = (report.opponent || 'OPPONENT').toUpperCase(); // Team Info Banner var teamInfo = report.teamInfo; if (teamInfo && (teamInfo.opponent?.jerseyColor || teamInfo.yourTeam?.jerseyColor)) { document.getElementById('team-info-banner').style.display = 'block'; document.getElementById('team-opponent-name').textContent = (teamInfo.opponent?.name || report.opponent) + ' (' + (teamInfo.opponent?.jerseyColor || 'unknown').toUpperCase() + ' jerseys)'; document.getElementById('team-your-name').textContent = (teamInfo.yourTeam?.name || 'Your Team') + ' (' + (teamInfo.yourTeam?.jerseyColor || 'unknown').toUpperCase() + ' jerseys)'; document.getElementById('opponent-banner-color').textContent = '(' + (teamInfo.opponent?.jerseyColor || '').toUpperCase() + ' jerseys)'; // Set color swatches var colorMap = { 'dark': '#1a1a1a', 'black': '#1a1a1a', 'white': '#ffffff', 'red': '#dc2626', 'blue': '#2563eb', 'navy': '#1e3a8a', 'light-blue': '#38bdf8', 'green': '#16a34a', 'dark-green': '#15803d', 'yellow': '#eab308', 'orange': '#ea580c', 'purple': '#9333ea', 'maroon': '#7f1d1d', 'gray': '#6b7280', 'grey': '#6b7280', 'pink': '#ec4899' }; var oppColor = colorMap[teamInfo.opponent?.jerseyColor?.toLowerCase()] || '#888'; var yourColor = colorMap[teamInfo.yourTeam?.jerseyColor?.toLowerCase()] || '#888'; document.getElementById('team-opponent-color').style.background = oppColor; document.getElementById('team-opponent-color').style.border = oppColor === '#ffffff' ? '2px solid #ccc' : 'none'; document.getElementById('team-your-color').style.background = yourColor; document.getElementById('team-your-color').style.border = yourColor === '#ffffff' ? '2px solid #ccc' : 'none'; } else { document.getElementById('team-info-banner').style.display = 'none'; document.getElementById('opponent-banner-color').textContent = ''; } // Top Stats (all OPPONENT stats) document.getElementById('stat-defense').textContent = report.defense?.primary?.scheme || report.defense?.primary || '--'; document.getElementById('stat-pace').textContent = report.paceAnalysis?.paceRating || report.paceAndTempo?.paceRating || report.pace?.rating || '--'; document.getElementById('stat-possessions').textContent = report.paceAnalysis?.estimatedPossessionsPerGame || report.paceAndTempo?.possessionsPerGameEstimate || '--'; document.getElementById('stat-reversals').textContent = report.ballMovementMetrics?.reversalsPerPossession || report.ballMovementAnalytics?.reversalsPerPossession || '--'; document.getElementById('stat-passes').textContent = report.ballMovementMetrics?.passesPerPossession || report.ballMovementAnalytics?.passesBeforeShot || '--'; var toConv = report.turnoverToScoreAnalysis?.turnoverConversionRate || report.turnoverAnalysis?.turnoverConversion?.conversionRate; document.getElementById('stat-toconversion').textContent = toConv ? toConv + '%' : '--'; document.getElementById('stat-frames').textContent = report.framesAnalyzed || '--'; document.getElementById('stat-confidence').textContent = (report.confidence || '--') + '%'; // Must Stop Box var tv = report.teamValueIdentification; if (tv?.mostValuableAspect) { document.getElementById('must-stop-box').style.display = 'block'; document.getElementById('must-stop-text').textContent = tv.mostValuableAspect; } else { document.getElementById('must-stop-box').style.display = 'none'; } // Defense Section var defHtml = ''; if (report.defense?.primary?.details) defHtml += '

' + report.defense.primary.details + '

'; if (report.defense?.breakdown) report.defense.breakdown.forEach(function(d) { defHtml += '
' + d.name + '' + d.percentage + '%
'; }); if (report.defense?.weaknesses?.length > 0) { defHtml += '

Weaknesses to exploit:

'; report.defense.weaknesses.forEach(function(w) { defHtml += '

• ' + (typeof w === 'string' ? w : w.weakness + (w.howToExploit ? ' — ' + w.howToExploit : '')) + '

'; }); } document.getElementById('defense-breakdown').innerHTML = defHtml || '

No data

'; // Offense Section var offHtml = ''; if (report.offense?.primary?.details) offHtml += '

' + report.offense.primary.details + '

'; if (report.offense?.setsAndActions?.length > 0) report.offense.setsAndActions.forEach(function(s) { offHtml += '
' + s.name + '' + s.frequency + '%
'; }); else if (report.offense?.topPlays) report.offense.topPlays.forEach(function(p) { offHtml += '
' + p.name + '' + p.percentage + '%
'; }); document.getElementById('offense-breakdown').innerHTML = offHtml || '

No data

'; // Set Efficiency & Ball Movement var effHtml = ''; var setData = report.offensiveSetTracking?.setBySetBreakdown || report.offensiveSetEfficiency?.setBreakdown || []; if (setData.length > 0) { effHtml += '

📊 SET-BY-SET BREAKDOWN

'; setData.forEach(function(s) { effHtml += '
'; effHtml += '
' + (s.setName || s.name) + '' + (s.pointsPerPossession || s.ppp || '--') + ' PPP
'; effHtml += '
'; effHtml += 'Run ' + (s.timesRun || '--') + 'x'; effHtml += '' + (s.pointsScored || '--') + ' pts'; if (s.percentageOfPossessions) effHtml += '' + s.percentageOfPossessions + '% of poss'; effHtml += '
'; if (s.primaryBeneficiary) effHtml += '

Primary: ' + s.primaryBeneficiary + '

'; if (s.bestDefense) effHtml += '

🛡️ Counter: ' + s.bestDefense + '

'; effHtml += '
'; }); } var bm = report.ballMovementMetrics || report.ballMovementAnalytics; if (bm) { effHtml += '

🏀 BALL MOVEMENT

'; if (bm.totalBallReversals) effHtml += '
Total ball reversals' + bm.totalBallReversals + '
'; if (bm.reversalsPerPossession) effHtml += '
Reversals per possession' + bm.reversalsPerPossession + '
'; if (bm.passesPerPossession) effHtml += '
Passes before shot' + bm.passesPerPossession + '
'; if (bm.reversalImpact) effHtml += '

💡 ' + bm.reversalImpact + '

'; } document.getElementById('efficiency-breakdown').innerHTML = effHtml || '

No data

'; // Pace & Turnovers var paceHtml = ''; var pa = report.paceAnalysis || report.paceAndTempo; if (pa) { paceHtml += '

⏱️ PACE & TEMPO

'; if (pa.possessionsObserved) paceHtml += '
Possessions observed' + pa.possessionsObserved + '
'; if (pa.estimatedPossessionsPerGame || pa.possessionsPerGameEstimate) paceHtml += '
Est. possessions/game' + (pa.estimatedPossessionsPerGame || pa.possessionsPerGameEstimate) + '
'; if (pa.paceCategory) paceHtml += '
Pace category' + pa.paceCategory.toUpperCase() + '
'; if (pa.averagePossessionLength) paceHtml += '
Avg possession length' + pa.averagePossessionLength + '
'; } var ta = report.turnoverToScoreAnalysis || report.turnoverAnalysis; if (ta) { paceHtml += '

🔄 TURNOVER ANALYSIS

'; if (ta.turnoversConvertedToScores != null) paceHtml += '
Turnovers → scores' + ta.turnoversConvertedToScores + '/' + ta.opponentTurnoversObserved + '
'; if (ta.turnoverConversionRate) paceHtml += '
Conversion rate' + ta.turnoverConversionRate + '%
'; if (ta.keyInsight) paceHtml += '

🎯 ' + ta.keyInsight + '

'; } document.getElementById('pace-breakdown').innerHTML = paceHtml || '

No data

'; // Team Value Section var valueHtml = ''; if (tv) { if (tv.offensiveIdentity?.whatMakesThemDangerous) { valueHtml += '
⚡ Offensive Identity
'; valueHtml += '

' + tv.offensiveIdentity.whatMakesThemDangerous + '

'; } if (tv.goToPlays?.needABucket) { valueHtml += '

🏀 GO-TO PLAYS

'; valueHtml += '
Need a bucket' + tv.goToPlays.needABucket + '
'; if (tv.goToPlays.lastShot) valueHtml += '
Last shot' + tv.goToPlays.lastShot + '
'; } if (tv.xFactor?.description) { valueHtml += '
'; valueHtml += '

⭐ X-FACTOR

'; valueHtml += '

' + tv.xFactor.description + '

'; } } document.getElementById('value-breakdown').innerHTML = valueHtml || '

No data

'; // Players Section var playerHtml = ''; var playerData = report.playerSetAssignments || report.playerSpecificSets || []; if (playerData.length > 0) { playerData.forEach(function(ps) { playerHtml += '
'; playerHtml += '
👤 ' + ps.player + '
'; if (ps.totalTouches) playerHtml += '

' + ps.totalTouches + '

'; if (ps.mostEffectiveAction) playerHtml += '

✓ Best action: ' + ps.mostEffectiveAction + '

'; var sets = ps.setsRunForThisPlayer || ps.designedPlays || []; sets.forEach(function(set) { playerHtml += '
' + (set.set || set.setName) + '' + (set.ppp || set.effectiveness || '--') + '
'; }); playerHtml += '
'; }); } else if (report.keyPlayers?.length > 0) { report.keyPlayers.forEach(function(p) { playerHtml += '
' + p.identifier + ' (' + p.position + ')' + p.threatLevel + '
'; }); } document.getElementById('players-breakdown').innerHTML = playerHtml || '

No data

'; // Practice Plan Section — link to Practice Planner instead of inline drills var practiceHtml = ''; var drills = report.recommendations?.practiceEmphasis || []; // Build a summary of insights from the scouting report to pre-fill the Practice Planner var insightParts = []; if (report.opponent) insightParts.push('Opponent: ' + report.opponent); if (report.teamWeaknesses?.length > 0) { insightParts.push('Weaknesses identified: ' + report.teamWeaknesses.map(function(w) { return w.weakness; }).join(', ')); } if (report.recommendations?.keysToVictory?.length > 0) { insightParts.push('Keys to victory: ' + report.recommendations.keysToVictory.join('; ')); } if (drills.length > 0) { insightParts.push('Suggested drill focus areas: ' + drills.map(function(d) { return (d.drill || d.name) + (d.purpose ? ' (' + d.purpose + ')' : ''); }).join('; ')); } var insightsSummary = insightParts.join('\n'); practiceHtml += '
'; practiceHtml += '
'; document.getElementById('practice-breakdown').innerHTML = practiceHtml; // Recommendations Section var recHtml = ''; if (report.recommendations?.keysToVictory?.length > 0) { recHtml += '

🔑 KEYS TO VICTORY

'; report.recommendations.keysToVictory.forEach(function(k, i) { recHtml += '

' + (i + 1) + '. ' + k + '

'; }); } if (report.recommendations?.offensiveGamePlan?.primaryStrategy) { recHtml += '

⚡ OFFENSIVE STRATEGY

'; recHtml += '

' + report.recommendations.offensiveGamePlan.primaryStrategy + '

'; } if (report.recommendations?.defensiveGamePlan?.recommendedScheme) { recHtml += '

🛡️ DEFENSIVE STRATEGY

'; recHtml += '

Recommended: ' + report.recommendations.defensiveGamePlan.recommendedScheme + '

'; } document.getElementById('recommendations').innerHTML = recHtml || '

No recommendations

'; } window.onbeforeunload = function(e) { // Clean up polling if (reportPollInterval) { clearInterval(reportPollInterval); reportPollInterval = null; } }; document.addEventListener('DOMContentLoaded', function() { initSupabase(); if (supabaseClient) { // Check current session supabaseClient.auth.getSession().then(function(result) { var session = result.data.session; handleAuthSession(session); }).catch(function() { // Fallback to localStorage var saved = localStorage.getItem('coachiq_user'); if (saved) { currentUser = JSON.parse(saved); updateUserUI(); } }); // Listen for auth state changes supabaseClient.auth.onAuthStateChange(function(_event, session) { handleAuthSession(session); }); } else { // No Supabase — fall back to localStorage var saved = localStorage.getItem('coachiq_user'); if (saved) { currentUser = JSON.parse(saved); updateUserUI(); } } // Route to section based on URL path (e.g. /pricing → pricing section) var pathSection = window.location.pathname.replace(/^\//, '').replace(/\/$/, ''); var validSections = ['pricing', 'dashboard']; if (pathSection && validSections.indexOf(pathSection) !== -1) { showSection(pathSection); } // Open auth modal if redirected from a protected page var urlParams = new URLSearchParams(window.location.search); var authParam = urlParams.get('auth'); if (authParam === 'login' || authParam === 'signup') { openAuthModal(authParam); history.replaceState(null, '', '/'); } // ── Feature flag: payments vs. waitlist ── if (!paymentsEnabled) { document.getElementById('subscription-note-banner').style.display = 'none'; document.querySelectorAll('.payments-only').forEach(function(el) { el.style.display = 'none'; }); document.querySelectorAll('.waitlist-only').forEach(function(el) { el.style.display = ''; }); } // Close waitlist modal on outside click document.getElementById('waitlist-modal').addEventListener('click', function(e) { if (e.target === this) closeWaitlistModal(); }); // Close dropdown on outside click document.addEventListener('click', function(e) { var userMenu = document.getElementById('user-section'); var dropdown = document.getElementById('user-dropdown'); if (userMenu && dropdown && !userMenu.contains(e.target)) { dropdown.classList.remove('active'); } }); });