PageSourceSearch

https://www.lovetorally.com/login

html lovetorally.com collected 2026-10-10 00:19:31 UTC 59,708 bytes, 1,304 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Rally</title>
7    
8    <!-- Theme Color -->
9    <meta name="theme-color" content="#000000">
10    <meta name="msapplication-navbutton-color" content="#000000">
11    <meta name="apple-mobile-web-app-status-bar-style" content="black">
12    
13    <!-- Favicon -->
14    <link rel="icon" type="image/png" href="/static/images/rally_favicon.png?v=2025">
15    <link rel="apple-touch-icon" href="/static/img/apple-touch-icon-green-small.png?v=2025">
16    
17    <!-- Basic Meta Tags -->
18    <meta name="description" content="One app to run your season. Built by players, for players.">
19    <meta name="author" content="Rally">
20    <meta name="robots" content="index, follow">
21    
22    <!-- Open Graph Meta Tags -->
23    <meta property="og:type" content="website">
24    <meta property="og:title" content="Rally">
25    <meta property="og:description" content="One app to run your season. Built by players, for players.">
26    <meta property="og:image" content="https://www.lovetorally.com/preview.png?v=20260719og">
27    <meta property="og:image:secure_url" content="https://www.lovetorally.com/preview.png?v=20260719og">
28    <meta property="og:image:alt" content="Rally - Platform Tennis Management">
29    <meta property="og:image:type" content="image/png">
30    <meta property="og:image:width" content="1200">
31    <meta property="og:image:height" content="630">
32    <meta property="og:url" content="http://www.lovetorally.com/login">
33    <meta property="og:site_name" content="Rally">
34    <meta property="og:locale" content="en_US">
35    
36    <!-- Twitter Card Meta Tags -->
37    <meta name="twitter:card" content="summary_large_image">
38    <meta name="twitter:title" content="Rally">
39    <meta name="twitter:description" content="One app to run your season. Built by players, for players.">
40    <meta name="twitter:image" content="https://www.lovetorally.com/preview.png?v=20260719og">
41    <meta name="twitter:image:alt" content="Rally - Platform Tennis Management">
42    <meta name="twitter:site" content="@rally">
43    <meta name="twitter:creator" content="@rally">
44    
45    <!-- Additional Social Media Tags -->
46    <meta property="fb:app_id" content="">
47    <meta name="application-name" content="Rally">
48    <meta name="apple-mobile-web-app-title" content="Rally">
49    <meta name="apple-mobile-web-app-capable" content="yes">
50    <meta name="apple-mobile-web-app-status-bar-style" content="black">
51    <meta name="format-detection" content="telephone=no">
52    
53    <!-- Canonical URL -->
54    <link rel="canonical" href="https://lovetorally.com/login">
55    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
56    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
57    <link href="/static/css/styles.css" rel="stylesheet">
58    <!-- Activity tracking -->
59    
59<script src="/static/js/activity-tracker.js"></script>
59
60    <style>
61        :root {
62            --primary-color: #007417; /* Rally green */
63            --text-color: #333;
64            --border-color: #dee2e6;
65        }
66
67        body {
68            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
69            background: linear-gradient(135deg, #085454, #0f172a);
70            display: flex;
71            justify-content: center;
72            align-items: center;
73            min-height: 100vh;
74            margin: 0;
75        }
76
77        .auth-container {
78            background: white;
79            border-radius: 12px;
80            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
81            width: 100%;
82            max-width: 800px;
83            text-align: center;
84            position: static;
85            align-self: center;
86        }
87
88        .auth-content {
89            padding: 0 60px 60px 60px;
90        }
91
92        .logo-header {
93            background: #000;
94            border-radius: 12px 12px 0 0;
95            min-height: 120px;
96            display: flex;
97            flex-direction: column;
98            align-items: center;
99            justify-content: center;
100            text-align: center;
101            padding: 0;
102        }
103
104        .logo-img {
105            width: 256px;
106            height: auto;
107            margin-bottom: 0;
108            filter: none;
109        }
110
111        .form-control {
112            border-radius: 8px;
113            padding: 12px;
114            margin-bottom: 20px;
115            border: 1px solid var(--border-color);
116            font-size: 1.15rem;
117        }
118
119        .form-control:focus {
120            border-color: var(--primary-color);
121            box-shadow: 0 0 0 0.2rem rgba(0, 116, 23, 0.15);
122        }
123
124        .form-control:disabled {
125            background-color: #f8f9fa;
126            border-color: #e9ecef;
127            color: #6c757d;
128            cursor: not-allowed;
129        }
130
131        .form-control:disabled + label {
132            color: #adb5bd;
133        }
134
135        .btn-primary {
136            background-color: #085454 !important;
137            color: #fff !important;
138            border: none !important;
139            border-radius: 8px;
140            padding: 12px;
141            width: 100%;
142            font-weight: 500;
143            transition: all 0.3s ease;
144            font-size: 1.15rem;
145            position: relative;
146            overflow: hidden;
147        }
148
149        .btn-primary:hover:not(:disabled) {
150            background-color: #064040 !important;
151            transform: translateY(-1px);
152            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
153        }
154
155        .btn-primary:disabled {
156            background-color: #6c757d !important;
157            cursor: not-allowed;
158            transform: none;
159            box-shadow: none;
160        }
161
162        .btn-primary:disabled:hover {
163            background-color: #6c757d !important;
164            transform: none;
165        }
166
167        /* Loading animation styles */
168        .btn-loading {
169            position: relative;
170            color: transparent !important;
171        }
172
173        .btn-loading::after {
174            content: '';
175            position: absolute;
176            width: 20px;
177            height: 20px;
178            top: 50%;
179            left: 50%;
180            margin-left: -10px;
181            margin-top: -10px;
182            border: 2px solid transparent;
183            border-top: 2px solid #ffffff;
184            border-radius: 50%;
185            animation: spin 1s linear infinite;
186        }
187
188        @keyframes spin {
189            0% { transform: rotate(0deg); }
190            100% { transform: rotate(360deg); }
191        }
192
193        /* Pulse animation for button press */
194        .btn-pulse {
195            animation: pulse 0.3s ease-in-out;
196        }
197
198        @keyframes pulse {
199            0% { transform: scale(1); }
200            50% { transform: scale(0.98); }
201            100% { transform: scale(1); }
202        }
203
204        .error-message {
205            color: #dc3545;
206            margin-top: 10px;
207            display: none;
208        }
209
210        .auth-error-container {
211            margin-bottom: 20px;
212            padding: 15px;
213            border-radius: 8px;
214            background-color: #fff3f3;
215            border: 1px solid #ffcdd2;
216            display: none;
217        }
218
219        .auth-error-container.active {
220            display: block;
221        }
222
223        .auth-error-message {
224            color: #dc3545;
225            margin: 0;
226            font-size: 14px;
227            text-align: center;
228        }
229
230        .form-floating {
231            margin-bottom: 20px;
232        }
233
234        .form-floating label {
235            color: #6c757d;
236        }
237
238        .password-match {
239            border-color: #28a745 !important;
240            box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.15) !important;
241        }
242
243        .password-mismatch {
244            border-color: #dc3545 !important;
245            box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.15) !important;
246        }
247
248        .password-feedback {
249            font-size: 0.875rem;
250            margin-top: 5px;
251            margin-bottom: 15px;
252        }
253
254        .password-feedback.match {
255            color: #28a745;
256        }
257
258        .password-feedback.mismatch {
259            color: #dc3545;
260        }
261
262        .password-feedback i {
263            margin-right: 5px;
264        }
265
266        .auth-tabs {
267            display: flex;
268            margin-bottom: 30px;
269            border-bottom: 1px solid var(--border-color);
270        }
271
272        .auth-tab {
273            flex: 1;
274            padding: 15px;
275            cursor: pointer;
276            transition: all 0.3s ease;
277        }
278
279        .auth-tab.active {
280            border-bottom: 2px solid var(--primary-color);
281            color: var(--primary-color);
282        }
283
284        .auth-form {
285            display: none;
286        }
287
288        .auth-form.active {
289            display: block;
290        }
291
292        .form-row {
293            display: flex;
294            gap: 15px;
295        }
296
297        .form-row .form-floating {
298            flex: 1;
299        }
300
301        .auth-hero { display: none; }
302        .white-logo { filter: brightness(0) invert(1); }
303
304        .rally-footer {
305            position: fixed;
306            left: 0;
307            bottom: 0;
308            width: 100%;
309            background: #fff;
310            color: #888;
311            font-size: 11px;
312            text-align: center;
313            padding: 8px 0 6px 0;
314            box-shadow: 0 -2px 8px rgba(0,0,0,0.03);
315            z-index: 100;
316            line-height: 1.4;
317        }
318
319        .rally-footer a {
320            color: #6c757d;
321            text-decoration: none;
322            margin: 0 8px;
323            font-size: 10px;
324        }
325
326        .rally-footer a:hover {
327            color: #333;
328            text-decoration: underline;
329        }
330
331        .login-stack {
332            display: flex;
333            flex-direction: column;
334            align-items: center;
335            width: 100vw;
336            min-height: 100vh;
337        }
338        .auth-container {
339            position: static;
340        }
341        
342        /* Rally logo color styling */
343        .rally-logo {
344            transform: translateY(3px);
345        }
346
347        /* Forgot Password Link Styling */
348        .forgot-password-link {
349            color: #6c757d;
350            text-decoration: none;
351            font-size: 0.9rem;
352            transition: color 0.2s ease;
353        }
354
355        .forgot-password-link:hover {
356            color: var(--primary-color);
357            text-decoration: underline;
358        }
359
360    </style>
361</head>
362<body>
363    <div class="login-stack">
364        <div style="height:87.5px;"></div>
365        <div style="display:flex; justify-content:center; align-items:center;">
366                                <img src="/static/rallylogo.png" alt="Rally" class="logo-img rally-logo">
367        </div>
368        <div style="height:87.5px;"></div>
369        
370        
371        <div class="auth-container">
372            <div class="auth-content">
373                <div class="auth-error-container" id="authErrorContainer">
374                    <p class="auth-error-message" id="authErrorMessage"></p>
375                </div>
376                
377                <div class="auth-tabs">
378                    <div class="auth-tab active" onclick="showForm('login')">Sign In</div>
379                    <div class="auth-tab " onclick="showForm('register')">Register</div>
380                </div>
381                
382                <form id="loginForm" class="auth-form active">
383                    <div class="form-floating">
384                        <input type="email" class="form-control" id="loginEmail" placeholder="[email protected]" required>
385                        <label for="loginEmail">Email address</label>
386                    </div>
387                    
388                    <div class="form-floating" style="position: relative;">
389                        <input type="password" class="form-control" id="loginPassword" placeholder="Password" required>
390                        <label for="loginPassword">Password</label>
391                        <button type="button" id="loginPasswordToggle" onclick="toggleLoginPassword()" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #6c757d; font-size: 12px; cursor: pointer; z-index: 10;">SHOW</button>
392                    </div>
393                    
394                    <div class="error-message" id="loginErrorMessage"></div>
395                    
396                    <button type="submit" class="btn btn-primary">
397                        <i class="fas fa-sign-in-alt me-2"></i>Let's go!
398                    </button>
399                    
400                    <!-- Separator Line -->
401                    <div style="margin: 24px 0; text-align: center;">
402                        <div style="height: 1px; background-color: #e5e7eb; width: 100%;"></div>
403                    </div>
404                    
405                    <!-- Forgot Password and Support Chips -->
406                    <div style="display: flex; flex-direction: row; justify-content: center; gap: 12px; margin-top: 20px;">
407                        <a href="/forgot-password" style="
408                            display: flex; 
409                            align-items: center; 
410                            justify-content: center;
411                            width: 180px;
412                            padding: 8px 12px; 
413                            border-radius: 25px; 
414                            background: #045454; 
415                            color: white; 
416                            text-decoration: none;
417                            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
418                            font-size: 10px;
419                            font-weight: 600;
420                            transition: all 0.2s ease;
421                        " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
422                            <i class="fas fa-key" style="margin-right: 6px; font-size: 12px; color: white;"></i>
423                            <span style="color: white; font-size: 10px; font-weight: 600;">Forgot Password?</span>
424                        </a>
425                        
426                        <a href="/mobile/support" style="
427                            display: flex; 
428                            align-items: center; 
429                            justify-content: center;
430                            width: 180px;
431                            padding: 8px 12px; 
432                            border-radius: 25px; 
433                            background: #045454; 
434                            color: white; 
435                            text-decoration: none;
436                            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
437                            font-size: 10px;
438                            font-weight: 600;
439                            transition: all 0.2s ease;
440                        " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
441                            <i class="fas fa-question-circle" style="margin-right: 6px; font-size: 12px; color: white;"></i>
442                            <span style="color: white; font-size: 10px; font-weight: 600;">Get support</span>
443                        </a>
444                    </div>
445                </form>
446                
447                <form id="registerForm" class="auth-form ">
448                    <!-- Account Information Section -->
449                    <div class="form-section mb-4" style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; border-left: 3px solid #007bff;">
450                        <h6 class="section-title mb-3" style="color: #495057; font-weight: 600; font-size: 0.9rem; margin-bottom: 1rem;">About you</h6>
451                        
452                        <div class="form-row" style="margin-bottom: 0;">
453                            <div class="form-floating">
454                                <input type="text" class="form-control" id="firstName" placeholder="First Name" required>
455                                <label for="firstName">First Name</label>
456                            </div>
457                            
458                            <div class="form-floating">
459                                <input type="text" class="form-control" id="lastName" placeholder="Last Name" required>
460                                <label for="lastName">Last Name</label>
461                            </div>
462                        </div>
463                        
464                        <div class="form-floating mb-3">
465                            <input type="email" class="form-control" id="registerEmail" placeholder="[email protected]" required>
466                            <label for="registerEmail">Email address</label>
467                        </div>
468                        
469                        <div class="form-floating mb-3">
470                            <input type="tel" class="form-control" id="phoneNumber" placeholder="(555) 123-4567" required>
471                            <label for="phoneNumber">Phone Number</label>
472                        </div>
473                        
474                        <div class="form-floating mb-3" style="position: relative;">
475                            <input type="password" class="form-control" id="registerPassword" placeholder="Password" required>
476                            <label for="registerPassword">Password</label>
477                            <button type="button" id="registerPasswordToggle" onclick="toggleRegisterPassword()" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #6c757d; font-size: 12px; cursor: pointer; z-index: 10;">SHOW</button>
478                        </div>
479                        
480                        <div class="form-floating mb-3" style="position: relative;">
481                            <input type="password" class="form-control" id="confirmPassword" placeholder="Confirm Password" required>
482                            <label for="confirmPassword">Confirm Password</label>
483                            <button type="button" id="confirmPasswordToggle" onclick="toggleConfirmPassword()" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #6c757d; font-size: 12px; cursor: pointer; z-index: 10;">SHOW</button>
484                        </div>
485                        
486                        <div class="password-feedback" id="passwordFeedback" style="display: none;">
487                            <i class="fas fa-check-circle"></i>
488                            <span id="passwordFeedbackText"></span>
489                        </div>
490                    </div>
491                    
492                    <!-- Organization Settings Section -->
493                    <div class="form-section mb-4" style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; border-left: 3px solid #28a745;">
494                        <h6 class="section-title mb-3" style="color: #495057; font-weight: 600; font-size: 0.9rem; margin-bottom: 1rem;">Where you play</h6>
495                        
496                        <div class="form-floating mb-3">
497                            <select class="form-control" id="league" required>
498                                <option value="">Select your league</option>
499                            </select>
500                            <label for="league">League</label>
501                        </div>
502                        
503                        <div class="form-floating mb-3">
504                            <select class="form-control" id="club" required>
505                                <option value="">Select your club</option>
506                            </select>
507                            <label for="club">Club</label>
508                        </div>
509                        
510                        <div class="form-floating mb-3">
511                            <select class="form-control" id="series" required>
512                                <option value="">Select your series</option>
513                            </select>
514                            <label for="series">Series/Division</label>
515                        </div>
516                    </div>
517                    
518                    <!-- Player Preferences Section -->
519                    <div class="form-section mb-4" style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; border-left: 3px solid #ffc107;">
520                        <h6 class="section-title mb-3" style="color: #495057; font-weight: 600; font-size: 0.9rem; margin-bottom: 1rem;">How you play</h6>
521                        
522                        <div class="form-floating mb-3">
523                            <select class="form-control" id="adDeuce" required>
524                                <option value="">Select preference...</option>
525                                <option value="Ad">Ad</option>
526                                <option value="Deuce">Deuce</option>
527                                <option value="Either">Either</option>
528                            </select>
529                            <label for="adDeuce">Ad/Deuce Preference</label>
530                        </div>
531                        
532                        <div class="form-floating mb-3">
533                            <select class="form-control" id="dominantHand" required>
534                                <option value="">Select hand...</option>
535                                <option value="Righty">Righty</option>
536                                <option value="Lefty">Lefty</option>
537                            </select>
538                            <label for="dominantHand">Dominant Hand</label>
539                        </div>
540                    </div>
541                    
542                    <div class="form-check mb-3" style="text-align: left;">
543                        <input class="form-check-input" type="checkbox" id="textNotifications" required>
544                        <label class="form-check-label" for="textNotifications" style="font-size: 0.85rem;">
545                            I agree to receive text message notifications from my team captain through Rally.  Reply STOP to unsubscribe.
546                        </label>
547                    </div>
548
549                    <!-- Apple App Store Guideline 1.2 / 5.1.4 / COPPA:
550                         age >= 13 attestation must be a separate, explicit checkbox. -->
551                    <div class="form-check mb-3" style="text-align: left;">
552                        <input class="form-check-input" type="checkbox" id="ageConfirmed" required>
553                        <label class="form-check-label" for="ageConfirmed" style="font-size: 0.85rem;">
554                            I confirm I am at least 13 years old.
555                        </label>
556                    </div>
557
558                    <!-- Apple App Store Guideline 1.2 / 5.1.1: explicit Terms acceptance at signup. -->
559                    <div class="form-check mb-4" style="text-align: left;">
560                        <input class="form-check-input" type="checkbox" id="termsAccepted" required>
561                        <label class="form-check-label" for="termsAccepted" style="font-size: 0.85rem;">
562                            I agree to the
563                            <a href="/terms" target="_blank" rel="noopener">Terms of Use</a>
564                            and
565                            <a href="/privacy" target="_blank" rel="noopener">Privacy Policy</a>.
566                        </label>
567                    </div>
568
569                    <div class="error-message" id="registerErrorMessage"></div>
570                    
571                    <button type="submit" class="btn btn-primary">
572                        <i class="fas fa-user-plus me-2"></i>Let's go!
573                    </button>
574                </form>
575            </div>
576        </div>
577        
578        <!-- Extra spacing for auto-scroll functionality -->
579        <div style="height: 200px;"></div>
580    </div>
581
582    
582<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
582
583    
583<script>
584        // Button state management functions
585        function setButtonLoading(button, isLoading) {
586            if (isLoading) {
587                button.disabled = true;
588                button.classList.add('btn-loading');
589                button.classList.add('btn-pulse');
590            } else {
591                button.disabled = false;
592                button.classList.remove('btn-loading');
593                button.classList.remove('btn-pulse');
594            }
595        }
596
597        function resetButtonState(button) {
598            button.disabled = false;
599            button.classList.remove('btn-loading');
600            button.classList.remove('btn-pulse');
601        }
602
603        // Load leagues, clubs and series on page load with filtering
604        document.addEventListener('DOMContentLoaded', async () => {
605            // Set the correct tab based on the current URL or server-provided default
606            const currentPath = window.location.pathname;
607            const currentHash = window.location.hash;
608            let activeTab = 'login';
609            
610            // Override with URL if user directly navigated to a specific page
611            if (currentPath === '/register') {
612                activeTab = 'register';
613            } else if (currentPath === '/login') {
614                activeTab = 'login';
615            }
616            
617            // Override with hash if present (e.g., /login#register)
618            if (currentHash === '#register') {
619                activeTab = 'register';
620            } else if (currentHash === '#login') {
621                activeTab = 'login';
622            }
623            
624            // Ensure the correct tab is displayed
625            showForm(activeTab);
626            
627            // Handle browser back/forward navigation
628            window.addEventListener('popstate', function(event) {
629                const currentPath = window.location.pathname;
630                if (currentPath === '/register') {
631                    showForm('register');
632                } else if (currentPath === '/login') {
633                    showForm('login');
634                }
635            });
636            
637            try {
638                await loadLeaguesAndFilters();
639            } catch (error) {
640                console.error('Error loading registration data:', error);
641                // Retry after a short delay in case of temporary network issues
642                setTimeout(async () => {
643                    try {
644                        await loadLeaguesAndFilters();
645                    } catch (retryError) {
646                        console.error('Retry failed:', retryError);
647                    }
648                }, 2000);
649            }
650        });
651
652        // Load leagues first, then set up filtering
653        async function loadLeaguesAndFilters() {
654            try {
655                // Load leagues first
656                const leaguesResponse = await fetch('/api/get-leagues');
657                if (!leaguesResponse.ok) {
658                    throw new Error('Failed to load leagues');
659                }
660                
661                const leagues = await leaguesResponse.json();
662                const leagueSelect = document.getElementById('league');
663                
664                leagueSelect.innerHTML = '<option value="">Select your league</option>';
665                
666                if (leagues.leagues && Array.isArray(leagues.leagues)) {
667                    leagues.leagues.forEach(league => {
668                        const option = document.createElement('option');
669                        option.value = league.league_id;
670                        option.textContent = league.league_name;
671                        leagueSelect.appendChild(option);
672                    });
673                }
674                
675                // Add event listener for league changes
676                leagueSelect.addEventListener('change', function() {
677                    const selectedLeague = this.value;
678                    const seriesSelect = document.getElementById('series');
679                    
680                    if (selectedLeague) {
681                        loadClubsByLeague(selectedLeague);
682                    } else {
683                        loadClubsByLeague();
684                    }
685
686                    // Series options depend on club; reset until a club is selected
687                    seriesSelect.disabled = true;
688                    seriesSelect.innerHTML = '<option value="">Select your club first</option>';
689                    
690                    // Clear club and series selections when league changes
691                    document.getElementById('club').value = '';
692                    document.getElementById('series').value = '';
693                });
694
695                // Reload series when club changes so users only see series their club fields
696                document.getElementById('club').addEventListener('change', function() {
697                    const selectedLeague = document.getElementById('league').value;
698                    const selectedClub = this.value;
699                    const seriesSelect = document.getElementById('series');
700
701                    if (selectedLeague && selectedClub) {
702                        seriesSelect.disabled = false;
703                        loadSeriesByLeague(selectedLeague, selectedClub);
704                    } else {
705                        seriesSelect.disabled = true;
706                        seriesSelect.innerHTML = '<option value="">Select your club first</option>';
707                    }
708
709                    document.getElementById('series').value = '';
710                });
711                
712                // Load all clubs initially, but leave series empty until league is selected
713                loadClubsByLeague();
714                
715                // Initialize series dropdown as disabled until club is selected
716                const seriesSelect = document.getElementById('series');
717                seriesSelect.disabled = true;
718                seriesSelect.innerHTML = '<option value="">Select your club first</option>';
719                
720            } catch (error) {
721                console.error('Error loading leagues:', error);
722                // Fallback to loading all clubs without league filtering
723                loadClubsByLeague();
724            }
725        }
726
727        // Load clubs filtered by league
728        function loadClubsByLeague(leagueId = null) {
729            const url = leagueId ? `/api/get-clubs-by-league?league_id=${leagueId}` : '/api/get-clubs-by-league';
730            
731            fetch(url).then(r => r.json()).then(data => {
732                const clubSelect = document.getElementById('club');
733                clubSelect.innerHTML = '<option value="">Select your club</option>';
734                
735                if (data.clubs && Array.isArray(data.clubs)) {
736                    data.clubs.forEach(club => {
737                        const option = document.createElement('option');
738                        option.value = club;
739                        option.textContent = club;
740                        clubSelect.appendChild(option);
741                    });
742                }
743            }).catch(error => {
744                console.error('Error loading clubs:', error);
745                // Fallback to all clubs
746                fetch('/api/get-clubs').then(r => r.json()).then(data => {
747                    const clubSelect = document.getElementById('club');
748                    clubSelect.innerHTML = '<option value="">Select your club</option>';
749                    if (data.clubs && Array.isArray(data.clubs)) {
750                        data.clubs.forEach(club => {
751                            const option = document.createElement('option');
752                            option.value = club;
753                            option.textContent = club;
754                            clubSelect.appendChild(option);
755                        });
756                    }
757                });
758            });
759        }
760
761        // Load series filtered by league and optionally by club (registration UX)
762        function loadSeriesByLeague(leagueId = null, clubName = null) {
763            // ✅ ID-BASED: Use the enhanced API endpoint that returns series objects with IDs
764            let url = leagueId ? `/api/get-user-facing-series-by-league?league_id=${encodeURIComponent(leagueId)}` : '/api/get-user-facing-series-by-league';
765            if (clubName) {
766                url += `${url.includes('?') ? '&' : '?'}club_name=${encodeURIComponent(clubName)}`;
767            }
768            
769            console.log('🔍 Loading ID-based series for league:', leagueId, 'club:', clubName || '(all)');
770            
771            fetch(url).then(r => r.json()).then(data => {
772                const seriesSelect = document.getElementById('series');
773                seriesSelect.innerHTML = '<option value="">Select your series</option>';
774                
775                if (data.series && Array.isArray(data.series)) {
776                    console.log('📊 Received series data:', data.series);
777                    
778                    data.series.forEach(seriesObj => {
779                        const option = document.createElement('option');
780                        
781                        // ✅ DEFENSIVE: Check if we have the new ID-based format
782                        if (typeof seriesObj === 'object' && seriesObj !== null && seriesObj.id) {
783                            // ✅ NEW: ID-based format with series objects
784                            const displayName = seriesObj.display || seriesObj.name || `Series ${seriesObj.id}`;
785                            
786                            option.value = JSON.stringify({
787                                id: seriesObj.id,
788                                name: seriesObj.name || '',
789                                display: displayName
790                            });
791                            option.textContent = displayName;  // Show user-friendly name
792                            
793                            // Add data attributes for debugging
794                            option.setAttribute('data-series-id', seriesObj.id);
795                            option.setAttribute('data-series-name', seriesObj.name || '');
796                            option.setAttribute('data-series-display', displayName);
797                            
798                            console.log(`  ✅ ID-based series: ${displayName} (ID: ${seriesObj.id})`);
799                        } else if (typeof seriesObj === 'string') {
800                            // 📜 LEGACY: String format (backward compatibility)
801                            option.value = seriesObj;
802                            option.textContent = seriesObj;
803                            console.log(`  📜 Legacy series: ${seriesObj}`);
804                        } else {
805                            // 🚨 FALLBACK: Handle unexpected format
806                            console.error('  ❌ Unexpected series format:', seriesObj);
807                            const fallbackText = String(seriesObj) || 'Unknown Series';
808                            option.value = fallbackText;
809                            option.textContent = fallbackText;
810                        }
811                        
812                        seriesSelect.appendChild(option);
813                    });
814                    
815                    console.log(`✅ Populated series dropdown with ${data.series.length} options`);
816                }
817            }).catch(error => {
818                console.error('❌ Error loading ID-based series:', error);
819                // Fallback to original API if new one fails
820                const fallbackUrl = leagueId ? `/api/get-series-by-league?league_id=${leagueId}` : '/api/get-series-by-league';
821                console.log('🔄 Falling back to legacy series API');
822                
823                fetch(fallbackUrl).then(r => r.json()).then(data => {
824                    const seriesSelect = document.getElementById('series');
825                    seriesSelect.innerHTML = '<option value="">Select your series</option>';
826                    if (data.series && Array.isArray(data.series)) {
827                        data.series.forEach(series => {
828                            const option = document.createElement('option');
829                            option.value = series;
830                            option.textContent = series;
831                            seriesSelect.appendChild(option);
832                        });
833                        console.log(`📜 Fallback: Populated series dropdown with ${data.series.length} legacy options`);
834                    }
835                }).catch(fallbackError => {
836                    console.error('❌ Fallback series loading also failed:', fallbackError);
837                    
838                    // 🆘 EMERGENCY FALLBACK: Hard-coded APTA Chicago series for immediate fix
839                    if (leagueId === 'APTA_CHICAGO') {
840                        console.log('🆘 Using emergency hard-coded APTA Chicago series');
841                        const seriesSelect = document.getElementById('series');
842                        seriesSelect.innerHTML = '<option value="">Select your series</option>';
843                        
844                        const emergencySeries = [
845                            'Series 1', 'Series 2', 'Series 3', 'Series 4', 'Series 5',
846                            'Series 6', 'Series 7', 'Series 8', 'Series 9', 'Series 10',
847                            'Series 11', 'Series 12', 'Series 13', 'Series 14', 'Series 15',
848                            'Series 16', 'Series 17', 'Series 18', 'Series 19', 'Series 20'
849                        ];
850                        
851                        emergencySeries.forEach(series => {
852                            const option = document.createElement('option');
853                            option.value = series;
854                            option.textContent = series;
855                            seriesSelect.appendChild(option);
856                        });
857                        
858                        console.log('🆘 Emergency fallback populated with 20 APTA Chicago series');
859                    }
860                });
861            });
862        }
863
864        // Password validation functions
865        function validatePasswordMatch() {
866            const password = document.getElementById('registerPassword').value;
867            const confirmPassword = document.getElementById('confirmPassword').value;
868            const passwordField = document.getElementById('registerPassword');
869            const confirmField = document.getElementById('confirmPassword');
870            const feedback = document.getElementById('passwordFeedback');
871            const feedbackText = document.getElementById('passwordFeedbackText');
872            const feedbackIcon = feedback.querySelector('i');
873            
874            // Only show feedback if both fields have content
875            if (password.length === 0 && confirmPassword.length === 0) {
876                feedback.style.display = 'none';
877                passwordField.classList.remove('password-match', 'password-mismatch');
878                confirmField.classList.remove('password-match', 'password-mismatch');
879                return true;
880            }
881            
882            // Only validate if confirm password has been touched
883            if (confirmPassword.length === 0) {
884                feedback.style.display = 'none';
885                passwordField.classList.remove('password-match', 'password-mismatch');
886                confirmField.classList.remove('password-match', 'password-mismatch');
887                return true;
888            }
889            
890            feedback.style.display = 'block';
891            
892            if (password === confirmPassword && password.length > 0) {
893                // Passwords match
894                passwordField.classList.remove('password-mismatch');
895                passwordField.classList.add('password-match');
896                confirmField.classList.remove('password-mismatch');
897                confirmField.classList.add('password-match');
898                
899                feedback.classList.remove('mismatch');
900                feedback.classList.add('match');
901                feedbackIcon.className = 'fas fa-check-circle';
902                feedbackText.textContent = 'Passwords match';
903                return true;
904            } else {
905                // Passwords don't match
906                passwordField.classList.remove('password-match');
907                passwordField.classList.add('password-mismatch');
908                confirmField.classList.remove('password-match');
909                confirmField.classList.add('password-mismatch');
910                
911                feedback.classList.remove('match');
912                feedback.classList.add('mismatch');
913                feedbackIcon.className = 'fas fa-times-circle';
914                feedbackText.textContent = 'Passwords do not match';
915                return false;
916            }
917        }
918
919        // Add event listeners for real-time password validation
920        document.addEventListener('DOMContentLoaded', function() {
921            const registerPassword = document.getElementById('registerPassword');
922            const confirmPassword = document.getElementById('confirmPassword');
923            
924            if (registerPassword && confirmPassword) {
925                registerPassword.addEventListener('input', validatePasswordMatch);
926                confirmPassword.addEventListener('input', validatePasswordMatch);
927                
928                // Also validate on blur events
929                registerPassword.addEventListener('blur', validatePasswordMatch);
930                confirmPassword.addEventListener('blur', validatePasswordMatch);
931            }
932            
933            // Auto-scroll to bottom when text notifications checkbox is clicked
934            const textNotificationsCheckbox = document.getElementById('textNotifications');
935            if (textNotificationsCheckbox) {
936                textNotificationsCheckbox.addEventListener('change', function() {
937                    if (this.checked) {
938                        // Auto-scroll down by 100px when checkbox is checked
939                        setTimeout(() => {
940                            window.scrollBy({
941                                top: 100,
942                                behavior: 'smooth'
943                            });
944                        }, 100); // Small delay to ensure checkbox state is updated
945                    }
946                });
947            }
948        });
949
950        // Function to smoothly scroll to the top of the page
951        function scrollToTop() {
952            window.scrollTo({
953                top: 0,
954                behavior: 'smooth'
955            });
956        }
957
958        function showForm(formType) {
959            document.querySelectorAll('.auth-tab').forEach(tab => {
960                tab.classList.remove('active');
961            });
962            document.querySelectorAll('.auth-form').forEach(form => {
963                form.classList.remove('active');
964            });
965            
966            document.querySelector(`.auth-tab:nth-child(${formType === 'login' ? 1 : 2})`).classList.add('active');
967            document.getElementById(`${formType}Form`).classList.add('active');
968            
969            // Reset button states when switching forms
970            const loginButton = document.querySelector('#loginForm button[type="submit"]');
971            const registerButton = document.querySelector('#registerForm button[type="submit"]');
972            if (loginButton) resetButtonState(loginButton);
973            if (registerButton) resetButtonState(registerButton);
974            
975            // Update URL to match the current tab (without page reload)
976            const newPath = formType === 'register' ? '/register' : '/login';
977            if (window.location.pathname !== newPath) {
978                window.history.pushState(null, '', newPath);
979            }
980            
981            // If switching to register form, ensure dropdowns are properly initialized
982            if (formType === 'register') {
983                setTimeout(() => {
984                    // Only reload if leagues haven't been loaded yet
985                    const leagueSelect = document.getElementById('league');
986                    if (leagueSelect.options.length <= 1) {
987                        loadLeaguesAndFilters();
988                    } else {
989                        // Ensure series dropdown is disabled if no league is selected
990                        const seriesSelect = document.getElementById('series');
991                        if (!leagueSelect.value) {
992                            seriesSelect.disabled = true;
993                            seriesSelect.innerHTML = '<option value="">Select your series</option>';
994                        }
995                    }
996                }, 100);
997            }
998        }
999
1000        document.getElementById('loginForm').addEventListener('submit', async function(e) {
1001            e.preventDefault();
1002            
1003            const loginButton = this.querySelector('button[type="submit"]');
1004            setButtonLoading(loginButton, true);
1005            
1006            const email = document.getElementById('loginEmail').value;
1007            const password = document.getElementById('loginPassword').value;
1008            const errorMessage = document.getElementById('loginErrorMessage');
1009            const authErrorContainer = document.getElementById('authErrorContainer');
1010            const authErrorMessage = document.getElementById('authErrorMessage');
1011            
1012            try {
1013                const response = await fetch('/api/login', {
1014                    method: 'POST',
1015                    headers: {
1016                        'Content-Type': 'application/json'
1017                    },
1018                    credentials: 'include',
1019                    body: JSON.stringify({ email, password })
1020                });
1021                
1022                const data = await response.json();
1023                if (response.ok && data.status === 'success') {
1024                    if (data.redirect) {
1025                        window.location.href = data.redirect;
1026                    } else {
1027                        window.location.href = '/';
1028                    }
1029                } else {
1030                    resetButtonState(loginButton);
1031                    if (response.status === 401) {
1032                        authErrorMessage.innerHTML = 'Your email or password is incorrect. If you forgot your password, no worries … click <a href="/forgot-password">here</a> to reset it. Or click <a href="/mobile/support">here</a> for support.';
1033                    } else {
1034                        authErrorMessage.textContent = data.error || 'An error occurred. Please try again.';
1035                    }
1036                    authErrorContainer.classList.add('active');
1037                    errorMessage.style.display = 'none';
1038                    scrollToTop();
1039                }
1040            } catch (error) {
1041                resetButtonState(loginButton);
1042                authErrorMessage.textContent = 'An error occurred. Please try again.';
1043                authErrorContainer.classList.add('active');
1044                errorMessage.style.display = 'none';
1045                scrollToTop();
1046            }
1047        });
1048
1049        document.getElementById('registerForm').addEventListener('submit', async function(e) {
1050            e.preventDefault();
1051            
1052            const registerButton = this.querySelector('button[type="submit"]');
1053            setButtonLoading(registerButton, true);
1054            
1055            const email = document.getElementById('registerEmail').value;
1056            let phoneNumber = document.getElementById('phoneNumber').value.trim();
1057            
1058            // ✅ PHONE NORMALIZATION: Use centralized phone validation
1059            if (phoneNumber) {
1060                const phoneResult = normalizePhoneNumber(phoneNumber);
1061                if (!phoneResult.success) {
1062                    errorMessage.textContent = phoneResult.error;
1063                    errorMessage.style.display = 'block';
1064                    setButtonLoading(registerButton, false);
1065                    return;
1066                }
1067                phoneNumber = phoneResult.normalized;
1068            }
1069            
1070            const password = document.getElementById('registerPassword').value;
1071            const confirmPassword = document.getElementById('confirmPassword').value;
1072            const firstName = document.getElementById('firstName').value;
1073            const lastName = document.getElementById('lastName').value;
1074            const league = document.getElementById('league').value;
1075            const club = document.getElementById('club').value;
1076            const series = document.getElementById('series').value;
1077            const adDeuce = document.getElementById('adDeuce').value;
1078            const dominantHand = document.getElementById('dominantHand').value;
1079            const errorMessage = document.getElementById('registerErrorMessage');
1080            const authErrorContainer = document.getElementById('authErrorContainer');
1081            const authErrorMessage = document.getElementById('authErrorMessage');
1082            
1083            // Validate password matching before submission
1084            if (!validatePasswordMatch()) {
1085                resetButtonState(registerButton);
1086                authErrorMessage.textContent = 'Passwords do not match. Please check your password entries.';
1087                authErrorContainer.classList.add('active');
1088                scrollToTop();
1089                return;
1090            }
1091            
1092            // Validate that passwords match
1093            if (password !== confirmPassword) {
1094                resetButtonState(registerButton);
1095                authErrorMessage.textContent = 'Passwords do not match';
1096                authErrorContainer.classList.add('active');
1097                scrollToTop();
1098                return;
1099            }
1100            
1101            // Validate that league is selected
1102            if (!league) {
1103                resetButtonState(registerButton);
1104                authErrorMessage.textContent = 'Please select a league';
1105                authErrorContainer.classList.add('active');
1106                scrollToTop();
1107                return;
1108            }
1109            
1110            try {
1111                // ✅ ID-BASED: Enhanced debug logging and series data extraction
1112                console.log('🔍 ID-BASED Registration Debug - Form Values:');
1113                console.log('  Email:', email);
1114                console.log('  First Name:', firstName);
1115                console.log('  Last Name:', lastName);
1116                console.log('  League:', league);
1117                console.log('  Club:', club);
1118                console.log('  Series (raw):', series);
1119                
1120                // ✅ ID-BASED: Extract series data from dropdown
1121                const seriesSelect = document.getElementById('series');
1122                const selectedOption = seriesSelect.options[seriesSelect.selectedIndex];
1123                let seriesId = null;
1124                let seriesDisplay = null;
1125                
1126                // Check if we have ID-based series data
1127                if (selectedOption && selectedOption.value) {
1128                    try {
1129                        // Try to parse as JSON (new ID-based format)
1130                        const seriesData = JSON.parse(selectedOption.value);
1131                        if (seriesData.id) {
1132                            seriesId = seriesData.id;
1133                            seriesDisplay = seriesData.display || selectedOption.textContent;
1134                            console.log('  ✅ ID-BASED Series Data:', seriesData);
1135                        }
1136                    } catch (e) {
1137                        // Fallback: treat as series name (backward compatibility)
1138                        seriesDisplay = selectedOption.value;
1139                        console.log('  📜 LEGACY Series Name:', seriesDisplay);
1140                    }
1141                }
1142                
1143                console.log('  Series ID:', seriesId);
1144                console.log('  Series Display:', seriesDisplay);
1145                console.log('  Series Value (raw):', selectedOption?.value);
1146                
1147                // Apple Guideline 1.2 / 5.1.4 / COPPA: pull explicit attestations.
1148                const termsAccepted = document.getElementById('termsAccepted')?.checked === true;
1149                const ageConfirmed = document.getElementById('ageConfirmed')?.checked === true;
1150
1151                // ✅ ID-BASED: Build registration payload with series_id priority
1152                const registrationPayload = {
1153                    email,
1154                    phoneNumber,
1155                    password,
1156                    firstName,
1157                    lastName,
1158                    league,
1159                    club,
1160                    adDeuce,
1161                    dominantHand,
1162                    terms_accepted: termsAccepted,
1163                    terms_version: '2026.06',
1164                    age_at_least_13: ageConfirmed
1165                };
1166
1167                // Add series data based on what we extracted
1168                if (seriesId) {
1169                    // NEW: ID-based registration (preferred)
1170                    registrationPayload.series_id = seriesId;
1171                    registrationPayload.series_display = seriesDisplay;
1172                    console.log('  📤 Sending ID-based registration with series_id:', seriesId);
1173                } else {
1174                    // FALLBACK: Name-based registration (backward compatibility)
1175                    registrationPayload.series = series;
1176                    console.log('  📤 Sending legacy registration with series name:', series);
1177                }
1178
1179                const response = await fetch('/api/register', {
1180                    method: 'POST',
1181                    headers: {
1182                        'Content-Type': 'application/json'
1183                    },
1184                    credentials: 'include',
1185                    body: JSON.stringify(registrationPayload)
1186                });
1187                
1188                const data = await response.json();
1189                
1190                // Debug logging for the API response
1191                console.log('🔍 Registration API Response:');
1192                console.log('  Status Code:', response.status);
1193                console.log('  Response Data:', data);
1194                
1195                if (response.ok && data.status === 'success') {
1196                    console.log('✅ Registration successful, redirecting...');
1197                    if (data.redirect) {
1198                        window.location.href = data.redirect;
1199                    } else {
1200                        window.location.href = '/';
1201                    }
1202                    authErrorContainer.classList.remove('active');
1203                    errorMessage.style.display = 'none';
1204                } else {
1205                    console.log('❌ Registration failed:', data.error);
1206                    resetButtonState(registerButton);
1207                    const errorText = data.error || 'Registration failed';
1208                    // If error mentions support, add hyperlink
1209                    if (errorText.includes('support') || errorText.
1209includes('Support')) {
1210                        // Replace "Please contact support for assistance" with a hyperlink
1211                        authErrorMessage.innerHTML = errorText.replace(
1212                            /(Please contact support for assistance)/gi,
1213                            'Please <a href="/mobile/support">contact support</a> for assistance'
1214                        );
1215                    } else {
1216                        authErrorMessage.textContent = errorText;
1217                    }
1218                    authErrorContainer.classList.add('active');
1219                    errorMessage.style.display = 'none';
1220                    scrollToTop();
1221                }
1222            } catch (error) {
1223                resetButtonState(registerButton);
1224                authErrorMessage.textContent = 'An error occurred. Please try again.';
1225                authErrorContainer.classList.add('active');
1226                errorMessage.style.display = 'none';
1227                scrollToTop();
1228            }
1229        });
1230        
1231        // Auto-scroll functionality for registration form
1232        function addAutoScrollOnSelect(selectId, scrollAmount) {
1233            var el = document.getElementById(selectId);
1234            if (el) {
1235                el.addEventListener('change', function() {
1236                    window.scrollBy({ top: scrollAmount, behavior: 'smooth' });
1237                });
1238            }
1239        }
1240        
1241        // Auto-scroll for specific fields
1242        document.addEventListener('DOMContentLoaded', function() {
1243            // Add auto-scroll for registration form fields
1244            addAutoScrollOnSelect('confirmPassword', 100);
1245            addAutoScrollOnSelect('league', 100);
1246            addAutoScrollOnSelect('club', 100);
1247            addAutoScrollOnSelect('series', 100);
1248            addAutoScrollOnSelect('adDeuce', 100);
1249            addAutoScrollOnSelect('dominantHand', 100);
1250        });
1251
1252        // Password visibility toggle functions
1253        function toggleLoginPassword() {
1254            const passwordField = document.getElementById('loginPassword');
1255            const toggleButton = document.getElementById('loginPasswordToggle');
1256            
1257            if (passwordField.type === 'password') {
1258                passwordField.type = 'text';
1259                toggleButton.textContent = 'HIDE';
1260            } else {
1261                passwordField.type = 'password';
1262                toggleButton.textContent = 'SHOW';
1263            }
1264        }
1265
1266        function toggleRegisterPassword() {
1267            const passwordField = document.getElementById('registerPassword');
1268            const toggleButton = document.getElementById('registerPasswordToggle');
1269            
1270            if (passwordField.type === 'password') {
1271                passwordField.type = 'text';
1272                toggleButton.textContent = 'HIDE';
1273            } else {
1274                passwordField.type = 'password';
1275                toggleButton.textContent = 'SHOW';
1276            }
1277        }
1278
1279        function toggleConfirmPassword() {
1280            const passwordField = document.getElementById('confirmPassword');
1281            const toggleButton = document.getElementById('confirmPasswordToggle');
1282            
1283            if (passwordField.type === 'password') {
1284                passwordField.type = 'text';
1285                toggleButton.textContent = 'HIDE';
1286            } else {
1287                passwordField.type = 'password';
1288                toggleButton.textContent = 'SHOW';
1289            }
1290        }
1291    </script>
1291
1292    
1293    <!-- Phone Validation Script -->
1294    
1294<script src="/static/js/phone-validation.js"></script>
1294
1295    
1296    <!-- Footer for branding -->
1297    <footer class="rally-footer">
1298        © 2025 Rally LLC. All rights reserved.
1299        <br>
1300        <a href="/terms">Terms of Use</a> | <a href="/privacy">Privacy Policy</a>
1301    </footer>
1302<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4bc70e2c01a94c73b74392e4234840661791215815920" integrity="sha512-L0ha0OXavK/8okipN9F8BtP84dg9DUhPERbBXzwI6dgTA55d2+yweo3pn5CSFYs45/r8md2+xvUPtTdvTNRfjA==" data-cf-beacon='{"version":"2024.11.0","token":"eb101b44180840dd91d7ebf5aec62a82","r":1,"spa":2}' crossorigin="anonymous"></script>
1302
1303</body>
1304</html> 

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.