PageSourceSearch

https://www.surva.ai/login

html surva.ai collected 2026-10-06 06:05:49 UTC 22,212 bytes, 602 lines download raw bytes

1
2
3
4<!DOCTYPE html>
5<html lang="en" class="h-full">
6<head>
7    <meta charset="utf-8">
8    <link rel="icon" type="image/png" href="/resources/images/favicon.png">
9    <meta content="initial-scale=1, shrink-to-fit=no, width=device-width" name="viewport">
10    <title>Sign In - Surva.ai</title>
11    <link rel="stylesheet" href="/resources/css/tailwind-output.css">
12    
12<script>
13        (function () {
14            try {
15                var t = localStorage.getItem('surva-theme');
16                var dark = t === 'dark' || (!t && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
17                document.documentElement.classList.toggle('dark', dark);
18            } catch (e) { /* no-op */ }
19        })();
20    </script>
20
21    <link rel="stylesheet" href="/resources/css/surva-dark.css">
22    
22<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
22
23    
23<script>
24        // Only load Google Sign-In on production domains (not localhost)
25        var isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
26        if (!isLocalhost) {
27            var script = document.createElement('script');
28            script.src = 'https://accounts.google.com/gsi/client';
29            script.async = true;
30            script.defer = true;
31            document.head.appendChild(script);
32        }
33    </script>
vendor: 77 bytes, lines 33-35
33
34    
35    <script src="https://www.google.com/recaptcha/enterprise.js?render=
356LfD6UAsAAAAACQHTt2gxLXDyA5uvafdG6fSs6-d
vendor: 21 bytes, lines 35-37
35"></script>
36    
37    
37<style>
38        /* Google Sign-In button styling */
39        #google-signin-wrapper {
40            opacity: 0;
41            transition: opacity 0.3s ease-in-out;
42            width: 100%;
43        }
44        #google-signin-wrapper.loaded {
45            opacity: 1;
46        }
47        .g_id_signin {
48            display: flex;
49            justify-content: center;
50            width: 100%;
51        }
52        .g_id_signin > div {
53            min-height: 44px;
54        }
55        /* Increase Google button border-radius to match Sign In button */
56        .g_id_signin iframe {
57            border-radius: 8px !important;
58        }
59        .g_id_signin > div > div {
60            border-radius: 8px !important;
61        }
62
63        /* Base light mode */
64        body {
65            background-color: #f8fafc;
66            color: #0f172a;
67        }
68
69        /* Hide reCAPTCHA badge */
70        .grecaptcha-badge {
71            visibility: hidden !important;
72        }
73
74        /* Large 150px grid background, fading toward the bottom */
75        .bg-large-grid {
76            position: fixed;
77            inset: 0;
78            z-index: 0;
79            pointer-events: none;
80        }
81
82        .bg-large-grid::before {
83            content: "";
84            position: absolute;
85            inset: 0;
86            background-image:
87                linear-gradient(to right, rgba(0,0,0,0.06) 1px, transparent 1px),
88                linear-gradient(to bottom, rgba(0,0,0,0.06) 1px, transparent 1px);
89            background-size: 150px 150px;
90            mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 40%, rgba(0,0,0,0) 100%);
91        }
92    </style>
93</head>
94<body>
95
96
97
98<a href="#main-content" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-indigo-600 focus:text-white focus:rounded-lg focus:text-sm focus:font-medium focus:shadow-lg focus:outline-none">
99    Skip to main content
100</a>
101
102
103<div id="a11y-status" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
104<div id="a11y-alert" class="sr-only" role="alert" aria-live="assertive" aria-atomic="true"></div>
105
106<style>
107    /* Enhanced focus indicators for keyboard navigation */
108    *:focus-visible {
109        outline: none;
110        box-shadow: 0 0 0 2px #4f46e5;
111    }
112    
113    /* Ensure focus ring is visible on dark backgrounds */
114    .bg-indigo-600 *:focus-visible,
115    .bg-gray-900 *:focus-visible {
116        box-shadow: 0 0 0 2px #ffffff;
117    }
118    
119    /* Reduced motion support */
120    @media (prefers-reduced-motion: reduce) {
121        *, *::before, *::after {
122            animation-duration: 0.01ms !important;
123            animation-iteration-count: 1 !important;
124            transition-duration: 0.01ms !important;
125            scroll-behavior: auto !important;
126        }
127    }
128    
129    /* High contrast mode support */
130    @media (forced-colors: active) {
131        .surva-pill-active,
132        .surva-pill.active {
133            border: 2px solid ButtonText;
134        }
135    }
136</style>
137
138<script>
139// ============================================
140// ACCESSIBILITY UTILITIES
141// ============================================
142
143/**
144 * Announce a message to screen readers via live region.
145 * @param {string} message - Text to announce
146 * @param {string} priority - 'polite' or 'assertive'
147 */
148function announceToScreenReader(message, priority) {
149    var el = priority === 'assertive' 
150        ? document.getElementById('a11y-alert') 
151        : document.getElementById('a11y-status');
152    if (el) {
153        el.textContent = '';
154        // Small delay to ensure the change is picked up by screen readers
155        setTimeout(function() { el.textContent = message; }, 50);
156    }
157}
158
159/**
160 * Trap focus within a container (for modals/dialogs).
161 * Returns a cleanup function to remove the trap.
162 * @param {HTMLElement} container - The modal/dialog element
163 * @returns {Function} cleanup function
164 */
165function trapFocus(container) {
166    var focusableSelectors = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
167    
168    function getFocusableElements() {
169        return Array.from(container.querySelectorAll(focusableSelectors)).filter(function(el) {
170            return el.offsetParent !== null; // visible elements only
171        });
172    }
173    
174    function handleKeyDown(e) {
175        if (e.key !== 'Tab') return;
176        
177        var focusable = getFocusableElements();
178        if (focusable.length === 0) return;
179        
180        var first = focusable[0];
181        var last = focusable[focusable.length - 1];
182        
183        if (e.shiftKey) {
184            if (document.activeElement === first) {
185                e.preventDefault();
186                last.focus();
187            }
188        } else {
189            if (document.activeElement === last) {
190                e.preventDefault();
191                first.focus();
192            }
193        }
194    }
195    
196    function handleEscape(e) {
197        if (e.key === 'Escape') {
198            // Find and click the cancel/close button
199            var closeBtn = container.querySelector('[data-dismiss], [onclick*="hide"], .surva-modal-close');
200            if (closeBtn) closeBtn.click();
201        }
202    }
203    
204    container.addEventListener('keydown', handleKeyDown);
205    container.addEventListener('keydown', handleEscape);
206    
207    // Focus the first focusable element
208    var focusable = getFocusableElements();
209    if (focusable.length > 0) {
210        focusable[0].focus();
211    }
212    
213    return function() {
214        container.removeEventListener('keydown', handleKeyDown);
215        container.removeEventListener('keydown', handleEscape);
216    };
217}
218
219/**
220 * Enhanced modal show/hide with accessibility.
221 * Call instead of just toggling .active class.
222 */
223var _modalFocusCleanup = {};
224var _modalPreviousFocus = {};
225
226function showAccessibleModal(modalId) {
227    var modal = document.getElementById(modalId);
228    if (!modal) return;
229    
230    // Store the element that triggered the modal
231    _modalPreviousFocus[modalId] = document.activeElement;
232    
233    // Show modal
234    modal.classList.add('active');
235    modal.setAttribute('role', 'dialog');
236    modal.setAttribute('aria-modal', 'true');
237    
238    // Set aria-labelledby if a heading exists
239    var heading = modal.querySelector('h3, h2, [data-modal-title]');
240    if (heading) {
241        if (!heading.id) heading.id = modalId + '-title';
242        modal.setAttribute('aria-labelledby', heading.id);
243    }
244    
245    // Trap focus
246    _modalFocusCleanup[modalId] = trapFocus(modal);
247    
248    // Prevent background scrolling
249    document.body.style.overflow = 'hidden';
250    
251    announceToScreenReader('Dialog opened: ' + (heading ? heading.textContent : ''), 'assertive');
252}
253
254function hideAccessibleModal(modalId) {
255    var modal = document.getElementById(modalId);
256    if (!modal) return;
257    
258    // Remove focus trap
259    if (_modalFocusCleanup[modalId]) {
260        _modalFocusCleanup[modalId]();
261        delete _modalFocusCleanup[modalId];
262    }
263    
264    // Hide modal
265    modal.classList.remove('active');
266    
267    // Restore background scrolling
268    document.body.style.overflow = '';
269    
270    // Return focus to trigger element
271    if (_modalPreviousFocus[modalId]) {
272        _modalPreviousFocus[modalId].focus();
273        delete _modalPreviousFocus[modalId];
274    }
275    
276    announceToScreenReader('Dialog closed', 'polite');
277}
278
279/**
280 * Make clickable table rows keyboard accessible.
281 * Adds Enter/Space key support and proper role.
282 */
283function makeRowsAccessible(tableId) {
284    var table = document.getElementById(tableId);
285    if (!table) return;
286    
287    var rows = table.querySelectorAll('tbody tr[onclick], tbody tr[data-href]');
288    rows.forEach(function(row) {
289        row.setAttribute('tabindex', '0');
290        row.setAttribute('role', 'link');
291        row.addEventListener('keydown', function(e) {
292            if (e.key === 'Enter' || e.key === ' ') {
293                e.preventDefault();
294                row.click();
295            }
296        });
297    });
298}
299
300/**
301 * Initialize sortable table headers for keyboard accessibility.
302 */
303function makeSortableHeadersAccessible(tableId) {
304    var table = document.getElementById(tableId);
305    if (!table) return;
306    
307    var headers = table.querySelectorAll('th[onclick]');
308    headers.forEach(function(th) {
309        th.setAttribute('tabindex', '0');
310        th.setAttribute('role', 'columnheader');
311        th.setAttribute('aria-sort', 'none');
312        var onclick = th.getAttribute('onclick');
313        th.addEventListener('keydown', function(e) {
314            if (e.key === 'Enter' || e.key === ' ') {
315                e.preventDefault();
316                th.click();
317            }
318        });
319    });
320}
321
322// Auto-init on DOM ready
323document.addEventListener('DOMContentLoaded', function() {
324    // Make all clickable table rows keyboard accessible
325    document.querySelectorAll('table').forEach(function(table) {
326        if (table.id) {
327            makeRowsAccessible(table.id);
328            makeSortableHeadersAccessible(table.id);
329        }
330    });
331    
332    // Add aria-hidden to all decorative SVGs inside links/buttons that also contain text
333    document.querySelectorAll('a svg, button svg').forEach(function(svg) {
334        var parent = svg.closest('a, button');
335        if (parent) {
336            var hasText = parent.textContent.trim().length > 0;
337            if (hasText && !svg.getAttribute('aria-label')) {
338                svg.setAttribute('aria-hidden', 'true');
339            }
340        }
341    });
342    
343    // Add aria-label to icon-only buttons that have title but no aria-label
344    document.querySelectorAll('button[title]:not([aria-label])').forEach(function(btn) {
345        var textContent = btn.textContent.trim();
346        if (textContent.length === 0 || textContent === '') {
347            btn.setAttribute('aria-label', btn.getAttribute('title'));
348        }
349    });
350});
351</script>
351
352
353
354<div class="bg-large-grid" aria-hidden="true"></div>
355
356<div class="relative z-10 flex min-h-full flex-col justify-center px-6 py-4 lg:px-8">
357    
358    
359
360    <div class="sm:w-full sm:max-w-sm px-6">
361        
362            
363            
364            
365                <a href="/" aria-label="Surva.ai home"><img class="h-8 w-auto dark:hidden" src="/resources/images/surva.png" alt="Surva.ai"><img class="h-8 w-auto hidden dark:block" src="/resources/images/surva-white.png" alt="Surva.ai"></a>
366            
367        
368    </div>
369
370    <div class="sm:mx-auto sm:w-full sm:max-w-sm">
371        <h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">
372            
373                
374                Sign in to your account
375            
376        </h2>
377
378        <!-- Error and Message Handling -->
379        
380        
381    </div>
382
383    <!-- LOGIN FORM -->
384    <main id="loginContainer" class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
385        <div class="rounded-2xl bg-white backdrop-blur-lg shadow-xl ring-1 ring-gray-900/5 p-10">
386        <form id="loginForm" class="space-y-6" action="/signin" method="POST">
387            
388            <input type="hidden" name="_csrf" value="eb4bzLia52QLkF-gxAUV_4uLYQF-13PU8A4OwZY0uaTqAgtjSYki_4r4hQcmqTvG_CghybO_TDlPskT5k29s-KUMgZPTNTlV"/>
389            <input type="hidden" name="g-recaptcha-response" id="recaptchaToken" value=""/>                       
390            <div>
391                <label for="email" class="block text-sm font-medium text-gray-900">Email address</label>
392                <div class="mt-2">
393                    <input maxlength="255" type="email" name="email" id="email" autocomplete="email" required class="block w-full rounded-lg bg-white px-3 py-2 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm">
394                </div>
395            </div>
396
397            <div>
398                <div class="flex items-center justify-between">
399                    <label for="password" class="block text-sm font-medium text-gray-900">Password</label>
400                    <div class="text-sm">
401                        <button type="button" id="forgotPasswordLink" class="font-semibold text-indigo-600 hover:text-indigo-500">Forgot password?</button>
402                    </div>
403                </div>
404                <div class="mt-2">
405                    <input maxlength="255" type="password" name="password" id="password" autocomplete="current-password" required class="block w-full rounded-lg bg-white px-3 py-2 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm">
406                </div>
407            </div>
408
409            <div>
410                <button type="submit" class="flex w-full justify-center rounded-lg bg-indigo-600 hover:bg-indigo-500 px-4 py-2.5 text-sm font-medium text-white shadow"
411                        >
412                    Sign in
413                </button>
414            </div>
415            
416            
417            
418            
419            <div class="relative mt-4" id="google-divider">
420            <div class="absolute inset-0 flex items-center">
421                <div class="w-full border-t border-gray-300"></div>
422            </div>
423            <div class="relative flex justify-center text-sm font-medium">
424                <span class="bg-white px-4 text-gray-500">Or</span>
425            </div>
426        </div>
427        <div id="server-error" class="error-message text-red-600 text-sm font-medium hidden mb-2 ml-2" role="alert"></div>
428    
429        <!-- Google Sign-In Button (hidden on localhost) -->
430        <div class="mt-4" id="google-signin-wrapper">
431            <div id="g_id_onload"
432                data-client_id="195542994343-d8cmt54tpccg6pmlqgs50r65cg9no78p.apps.googleusercontent.com"
433                data-context="signin"
434                data-ux_mode="popup"
435                data-callback="handleCredentialResponse"
436                data-auto_prompt="false">
437            </div>
438
439            <div class="g_id_signin" id="google-signin-btn"
440                data-type="standard"
441                data-shape="rectangular"
442                data-theme="outline"
443                data-text="signin_with"
444                data-size="large"
445                data-width="300"
446                data-logo_alignment="left">
447           </div>
448        </div>
449            
450            
451            
452
453        </form>
454
455        
456            
457            <p class="mt-8 text-center text-sm text-gray-500">
458                Not a member?
459                <a href="/signup" class="font-semibold text-indigo-600 hover:text-indigo-500">Start free trial</a>
460            </p>
461            
462            
463        
464        </div>
465    </main>
466            
467    <!-- FORGOT PASSWORD FORM -->
468    <div id="forgotPasswordContainer" class="hidden mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
469        <div class="rounded-2xl bg-white backdrop-blur-lg shadow-xl ring-1 ring-gray-900/5 p-10">
470        <form id="forgotPasswordForm" class="space-y-6" action="/forgotPassword" method="POST">
471            <div>
472                <label for="forgot-email" class="block text-sm font-medium text-gray-900">Email address</label>
473                <div class="mt-2">
474                    <input maxlength="255" type="email" name="forgot-email" id="forgot-email" autocomplete="email" required class="block w-full rounded-lg bg-white px-3 py-2 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm">
475                </div>
476            </div>
477
478            <div>
479                <button type="submit" class="flex w-full justify-center rounded-lg bg-indigo-600 hover:bg-indigo-500 focus-visible:outline focus-visible-outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm"
480                        >
481                    Reset Password
482                </button>
483            </div>
484
485            <p class="mt-4 text-center text-sm text-gray-500">
486                <button type="button" id="backToLogin" class="font-semibold text-indigo-600 hover:text-indigo-500">Back to login</button>
487            </p>
488        </form>
489        </div>
490    </div>
491</div>
492
493<script>
494    $(document).ready(function () {
495        // Check if on localhost - hide Google Sign-In if so
496        var isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
497        if (isLocalhost) {
498            $("#google-signin-wrapper").hide();
499            $("#google-divider").hide();
500        } else {
501            // Set Google button width dynamically based on wrapper width (inside form padding)
502            var wrapper = document.getElementById('google-signin-wrapper');
503            if (wrapper) {
504                var wrapperWidth = wrapper.offsetWidth;
505                var googleBtn = document.getElementById('google-signin-btn');
506                if (googleBtn && wrapperWidth > 0) {
507                    // Google button max width is 400, min is 200
508                    var btnWidth = Math.min(400, Math.max(200, wrapperWidth));
509                    googleBtn.setAttribute('data-width', btnWidth);
510                }
511            }
512        }
513        
514        $("#forgotPasswordLink").click(function (e) {
515            e.preventDefault();
516            $("#loginContainer").hide();
517            $("#forgotPasswordContainer").fadeIn();
518        });
519
520        $("#backToLogin").click(function (e) {
521            e.preventDefault();
522            $("#forgotPasswordContainer").hide();
523            $("#loginContainer").fadeIn();
524        });
525        
526        // Wait for Google Sign-In button to fully render before showing (only on production)
527        function waitForGoogleButton() {
528            if (isLocalhost) return; // Skip on localhost
529            
530            var wrapper = document.getElementById('google-signin-wrapper');
531            var iframe = wrapper ? wrapper.querySelector('iframe') : null;
532            
533            // Check if iframe exists and has reasonable dimensions (Google finished rendering)
534            if (iframe && iframe.offsetWidth > 100) {
535                // Add a small extra delay to ensure Google's internal rendering is complete
536                setTimeout(function() {
537                    $("#google-signin-wrapper").addClass("loaded");
538                }, 300);
539            } else {
540                // Keep checking every 100ms
541                setTimeout(waitForGoogleButton, 100);
542            }
543        }
544        
545        // Start checking after a brief initial delay
546        setTimeout(waitForGoogleButton, 200);
547        
548        // Handle login form submission with reCAPTCHA
549        $("#loginForm").on("submit", function(e) {
550            
551            e.preventDefault();
552            var form = this;
553            grecaptcha.enterprise.ready(function() {
554                grecaptcha.enterprise.execute('6LfD6UAsAAAAACQHTt2gxLXDyA5uvafdG6fSs6-d', {action: 'LOGIN'}).then(function(token) {
555                    document.getElementById('recaptchaToken').value = token;
556                    form.submit();
557                }).catch(function(err) {
558                    console.error('reCAPTCHA error:', err);
559                    form.submit(); // Submit anyway if reCAPTCHA fails
560                });
561            });
562            
563        });
564    });
565    
566    function handleCredentialResponse(response) {
567        fetch("/signinGoogle", {
568            method: "POST",
569            headers: { "Content-Type": "application/x-www-form-urlencoded" },
570            body: new URLSearchParams({ token: response.credential })
571        })
572        .then(res => res.text())  // Use text first to safely handle JSON parsing
573        .then(text => {
574            let data;
575            try {
576                data = JSON.parse(text);  // Safely parse JSON
577            } catch (err) {
578                throw new Error("Server response is not valid JSON: " + text);
579            }
580
581            if (data.error) {
582                showErrorMessage(data.error);
583            } else if (data.redirectUrl) {
584                window.location.href = data.redirectUrl;
585            } else {
586                showErrorMessage("Unexpected response from server.");
587            }
588        })
589        .catch(error => {
590            console.error("Google Sign-In Error:", error);
591            showErrorMessage("An unexpected error occurred. Please try again.");
592        });
593    }
594
595
596    function showErrorMessage(message) {
597        $("#server-error").html(message).removeClass("hidden");
598    }
599</script>
599
600
601</body>
602</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.