PageSourceSearch

https://app.viewrankai.com/login

html viewrankai.com collected 2026-10-02 01:47:43 UTC 27,507 bytes, 760 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="UTF-8">
5  
5<script>
6    // Keeps a password out of the address bar. Runs first, before anything
7    // else on the page, because the sign-in code only takes over the form once
8    // the sign-in library has loaded, which can take a few seconds. Until 29 Sep
9    // 2026 pressing the button in that gap made the browser send the form the
10    // old way, which writes the email and password into the page address, and
11    // from there into history and analytics. It happened to seven people.
12    (function () {
13      // Anyone who arrives with a password already in the address (an old
14      // bookmark, a shared link) has it removed before analytics can read it.
15      try {
16        var params = new URLSearchParams(window.location.search);
17        var secret = ['password', 'verification-code'];
18        var found = secret.filter(function (name) { return params.has(name); });
19        if (found.length) {
20          found.forEach(function (name) { params.delete(name); });
21          var query = params.toString();
22          history.replaceState(null, '', window.location.pathname + (query ? '?' + query : '') + window.location.hash);
23        }
24      } catch (error) { /* an old browser keeps the address as it is */ }
25
26      // No form on this page is ever sent by the browser itself. A press that
27      // arrives before the sign-in code is ready is held, and run the moment
28      // it is ready, so the person only sees a short wait.
29      var ready = false;
30      var waiting = null;
31      document.addEventListener('submit', function (event) {
32        event.preventDefault();
33        if (ready) return;
34        waiting = event.target;
35        var button = waiting.querySelector('[type="submit"]');
36        if (button) { button.disabled = true; button.textContent = 'One moment...'; }
37      }, true);
38
39      // The page calls this once its own form handlers are attached.
40      window.METLY_FORMS_READY = function () {
41        if (ready) return;
42        ready = true;
43        var form = waiting;
44        waiting = null;
45        if (!form) return;
46        var button = form.querySelector('[type="submit"]');
47        if (button) button.disabled = false;
48        if (typeof form.requestSubmit === 'function') form.requestSubmit();
49        else form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
50      };
51    }());
52  </script>
52
53  <meta name="viewport" content="width=device-width, initial-scale=1.0">
54  <!-- App page on app.viewrankai.com: keep it out of search. -->
55  <meta name="robots" content="noindex, follow">
56  <title>Login - ViewRank AI</title>
57  <link rel="icon" href="/favicon.svg" type="image/svg+xml">
58  <link rel="icon" href="/favicon.ico" sizes="32x32">
59  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
60  <link rel="preconnect" href="https://fonts.googleapis.com">
61  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
62  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&display=swap" rel="stylesheet">
63  <style>
64    :root {
65      --primary: #E87B35;
66      --primary-hover: #D66B28;
67      --bg: #FAFAFA;
68      --surface: #FFFFFF;
69      --surface-muted: #F5F5F5;
70      --border: #E8E8E8;
71      --text: #1A1A1A;
72      --text-secondary: #6B6B6B;
73      --text-muted: #9A9A9A;
74      --radius: 12px;
75      --radius-sm: 8px;
76      --shadow-lg: 0 20px 40px rgba(15, 23, 42, 0.12);
77    }
78    * { margin: 0; padding: 0; box-sizing: border-box; }
79    body {
80      min-height: 100vh;
81      display: flex;
82      align-items: center;
83      justify-content: center;
84      background: radial-gradient(circle at top, #FFF4ED 0, #FAFAFA 45%, #F3F4F6 100%);
85      font-family: 'DM Sans', -apple-system, system-ui, sans-serif;
86      color: var(--text);
87    }
88    .auth-card {
89      background: var(--surface);
90      border-radius: var(--radius);
91      box-shadow: var(--shadow-lg);
92      padding: 32px 32px 28px;
93      width: 100%;
94      max-width: 420px;
95      border: 1px solid var(--border);
96    }
97    .logo-row {
98      display: flex;
99      align-items: center;
100      gap: 10px;
101      justify-content: center;
102      margin-bottom: 16px;
103    }
104    .logo-icon {
105      width: 32px;
106      height: 32px;
107      border-radius: 8px;
108      flex-shrink: 0;
109      display: block;
110      object-fit: cover;
111    }
112    .logo-text {
113      font-weight: 700;
114      font-size: 18px;
115      color: var(--text);
116    }
117    .logo-badge {
118      font-size: 10px;
119      font-weight: 500;
120      padding: 2px 6px;
121      border-radius: 999px;
122      background: var(--surface-muted);
123      color: var(--text-muted);
124    }
125    .auth-title {
126      text-align: center;
127      font-size: 20px;
128      font-weight: 600;
129      margin-bottom: 4px;
130    }
131    .auth-subtitle {
132      text-align: center;
133      font-size: 13px;
134      color: var(--text-secondary);
135      margin-bottom: 20px;
136    }
137    .alert {
138      display: none;
139      padding: 10px 12px;
140      border-radius: var(--radius-sm);
141      font-size: 12px;
142      margin-bottom: 14px;
143    }
144    .alert-error {
145      background: #FEF2F2;
146      border: 1px solid #FCA5A5;
147      color: #B91C1C;
148    }
149    .alert-success {
150      background: #F0FDF4;
151      border: 1px solid #86EFAC;
152      color: #166534;
153    }
154    .form-group {
155      margin-bottom: 16px;
156    }
157    .form-label {
158      display: block;
159      font-size: 13px;
160      font-weight: 500;
161      color: var(--text-secondary);
162      margin-bottom: 6px;
163    }
164    .form-input {
165      width: 100%;
166      padding: 10px 12px;
167      border-radius: var(--radius-sm);
168      border: 1px solid var(--border);
169      font-size: 14px;
170      font-family: inherit;
171      color: var(--text);
172      background: #F9FAFB;
173      transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
174    }
175    .form-input::placeholder {
176      color: var(--text-muted);
177    }
178    .form-input:focus {
179      outline: none;
180      border-color: var(--primary);
181      background: #fff;
182      box-shadow: 0 0 0 3px rgba(232, 123, 53, 0.15);
183    }
184    .auth-button {
185      width: 100%;
186      margin-top: 4px;
187      padding: 10px 14px;
188      border-radius: var(--radius-sm);
189      border: none;
190      background: var(--primary);
191      color: #fff;
192      font-weight: 600;
193      font-size: 14px;
194      cursor: pointer;
195      transition: background 0.15s ease, transform 0.05s ease, box-shadow 0.15s ease;
196      box-shadow: 0 8px 20px rgba(232, 123, 53, 0.25);
197    }
198    .auth-button:hover {
199      background: var(--primary-hover);
200      transform: translateY(-1px);
201    }
202    .auth-button:disabled {
203      opacity: 0.6;
204      cursor: default;
205      box-shadow: none;
206    }
207    .auth-button.secondary {
208      margin-top: 10px;
209      background: #fff;
210      color: var(--primary);
211      border: 1px solid rgba(232, 123, 53, 0.3);
212      box-shadow: none;
213    }
214    .auth-button.secondary:hover {
215      background: #FFF4ED;
216    }
217    .form-hint {
218      margin-top: 6px;
219      font-size: 12px;
220      color: var(--text-muted);
221      line-height: 1.4;
222    }
223    /* Google sign in. Hidden until Clerk confirms the connection is live, so
224       the page never carries a button that can only fail. */
225    .social-auth { display: none; }
226    .social-button {
227      width: 100%;
228      display: flex;
229      align-items: center;
230      justify-content: center;
231      gap: 10px;
232      padding: 10px 14px;
233      border-radius: var(--radius-sm);
234      border: 1px solid var(--border);
235      background: #fff;
236      color: var(--text);
237      font-family: inherit;
238      font-weight: 600;
239      font-size: 14px;
240      cursor: pointer;
241      transition: background 0.15s ease, border-color 0.15s ease;
242    }
243    .social-button:hover {
244      background: var(--surface-muted);
245      border-color: #D8D8D8;
246    }
247    .social-button:disabled { opacity: 0.6; cursor: default; }
248    .social-button svg { width: 18px; height: 18px; flex-shrink: 0; }
249    .auth-divider {
250      display: flex;
251      align-items: center;
252      gap: 12px;
253      margin: 16px 0;
254      font-size: 12px;
255      color: var(--text-muted);
256    }
257    .auth-divider::before,
258    .auth-divider::after {
259      content: '';
260      flex: 1;
261      height: 1px;
262      background: var(--border);
263    }
264    .auth-footer {
265      margin-top: 18px;
266      text-align: center;
267      font-size: 13px;
268      color: var(--text-secondary);
269    }
270    .auth-footer a {
271      color: var(--primary);
272      font-weight: 500;
273      text-decoration: none;
274    }
275    .auth-footer a:hover {
276      text-decoration: underline;
277    }
278    .form-link-row {
279      display: flex;
280      justify-content: flex-end;
281      margin-top: -6px;
282      margin-bottom: 14px;
283    }
284    .text-button {
285      border: none;
286      background: none;
287      color: var(--primary);
288      font: inherit;
289      font-size: 13px;
290      font-weight: 500;
291      cursor: pointer;
292      padding: 0;
293    }
294    .text-button:hover {
295      text-decoration: underline;
296    }
297    .reset-panel {
298      display: none;
299      margin-top: 18px;
300      padding-top: 18px;
301      border-top: 1px solid var(--border);
302    }
303    .reset-copy {
304      font-size: 12px;
305      line-height: 1.5;
306      color: var(--text-secondary);
307      margin-bottom: 14px;
308    }
309  </style>
310</head>
311<body>
312  <div class="auth-card">
313    <div class="logo-row">
314      <img class="logo-icon" src="/ViewRank_logo.png" alt="" width="32" height="32">
315      <span class="logo-text">ViewRank AI</span>
316    </div>
317    <h1 class="auth-title">Welcome back</h1>
318    <p class="auth-subtitle">Sign in to view and analyze your Reels.</p>
319
320    <div id="error-message" class="alert alert-error">
321      <span id="error-text"></span>
322    </div>
323    <div id="success-message" class="alert alert-success">
324      <span id="success-text"></span>
325    </div>
326
327    <div class="social-auth" id="social-auth">
328      <button type="button" id="google-button" class="social-button">
329        <svg viewBox="0 0 48 48" aria-hidden="true" focusable="false">
330          <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
331          <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
332          <path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
333          <path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
334        </svg>
335        <span id="google-button-text">Continue with Google</span>
336      </button>
337      <div class="auth-divider">or</div>
338    </div>
339
340    <form id="login-form" method="post" action="#">
341      <div class="form-group">
342        <label for="email" class="form-label">Email</label>
343        <input
344          type="email"
345          id="email"
346          name="email"
347          required
348          class="form-input"
349          placeholder="[email protected]"
350        >
351      </div>
352
353      <div class="form-group">
354        <label for="password" class="form-label">Password</label>
355        <input
356          type="password"
357          id="password"
358          name="password"
359          required
360          class="form-input"
361          placeholder="••••••••"
362        >
363      </div>
364      <div class="form-link-row">
365        <button type="button" id="show-reset-btn" class="text-button">Forgot password?</button>
366      </div>
367
368      <button type="submit" id="login-button" class="auth-button">
369        Sign In
370      </button>
371    </form>
372
373    <form id="verify-form" method="post" action="#" style="display:none;">
374      <div class="form-group">
375        <label for="verification-code" class="form-label">Verification code</label>
376        <input
377          type="text"
378          id="verification-code"
379          name="verification-code"
380          required
381          inputmode="numeric"
382          autocomplete="one-time-code"
383          class="form-input"
384          placeholder="Enter the code from your email"
385        >
386        <p id="verify-hint" class="form-hint">Check your email and paste the code ViewRank sent you.</p>
387      </div>
388
389      <button type="submit" id="verify-button" class="auth-button">
390        Verify and Sign In
391      </button>
392      <button type="button" id="resend-code-button" class="auth-button secondary">
393        Send a new code
394      </button>
395      <button type="button" id="back-to-login-button" class="auth-button secondary">
396        Back to sign in
397      </button>
398    </form>
399
400    <div id="reset-panel" class="reset-panel">
401      <p class="reset-copy">Enter your email to receive a ViewRank reset code, then set a new password.</p>
402      <form id="reset-request-form" method="post" action="#">
403        <div class="form-group">
404          <label for="reset-email" class="form-label">Email</label>
405          <input type="email" id="reset-email" required class="form-input" placeholder="[email protected]">
406        </div>
407        <button type="submit" id="reset-request-button" class="auth-button">Send Reset Code</button>
408      </form>
409      <form id="reset-complete-form" method="post" action="#" style="display:none;">
410        <div class="form-group">
411          <label for="reset-code" class="form-label">Reset code</label>
412          <input type="text" id="reset-code" required class="form-input" placeholder="Enter emailed code">
413        </div>
414        <div class="form-group">
415          <label for="new-password" class="form-label">New password</label>
416          <input type="password" id="new-password" required minlength="8" class="form-input" placeholder="New password">
417        </div>
418        <button type="submit" id="reset-complete-button" class="auth-button">Reset Password</button>
419      </form>
420    </div>
421
422    <div class="auth-footer">
423      Don’t have an account?
424      <a href="https://viewrankai.com/#pricing">Choose a plan</a>
425    </div>
426  </div>
427
428  
428<script src="js/config.js?v=20260609-checkout"></script>
428
429  
429<script src="js/analytics.js?v=20260927-errors"></script>
429
430  
430<script src="js/auth.js?v=20260918-subscribe"></script>
430
431  
431<script>
432    document.addEventListener('DOMContentLoaded', async () => {
433      let session = null;
434      try {
435        session = await window.METLY_AUTH.getSession();
436      } catch (error) {
437        console.warn('Auth not ready:', error);
438      }
439      if (session) {
440        await window.METLY_AUTH.redirectAfterAuth();
441        return;
442      }
443
444      const loginForm = document.getElementById('login-form');
445      const verifyForm = document.getElementById('verify-form');
446      const loginButton = document.getElementById('login-button');
447      const verifyButton = document.getElementById('verify-button');
448      const resendCodeButton = document.getElementById('resend-code-button');
449      const backToLoginButton = document.getElementById('back-to-login-button');
450      const verificationCodeInput = document.getElementById('verification-code');
451      const verifyHint = document.getElementById('verify-hint');
452      const errorMessage = document.getElementById('error-message');
453      const errorText = document.getElementById('error-text');
454      const successMessage = document.getElementById('success-message');
455      const successText = document.getElementById('success-text');
456      const socialAuth = document.getElementById('social-auth');
457      const googleButton = document.getElementById('google-button');
458      const googleButtonText = document.getElementById('google-button-text');
459      let googleAvailable = false;
460
461      if (new URLSearchParams(window.location.search).get('expired') === '1') {
462        errorText.textContent = 'Your session expired. Please log in again.';
463        errorMessage.style.display = 'block';
464      }
465      const resetPanel = document.getElementById('reset-panel');
466      const resetRequestForm = document.getElementById('reset-request-form');
467      const resetCompleteForm = document.getElementById('reset-complete-form');
468      const resetRequestButton = document.getElementById('reset-request-button');
469      const resetCompleteButton = document.getElementById('reset-complete-button');
470
471      function showError(message) {
472        successMessage.style.display = 'none';
473        errorText.textContent = message;
474        errorMessage.style.display = 'block';
475      }
476
477      function showSuccess(message) {
478        errorMessage.style.display = 'none';
479        successText.textContent = message;
480        successMessage.style.display = 'block';
481      }
482
483      function showVerificationStep(result) {
484        loginForm.style.display = 'none';
485        // Google is not an option once an email sign in is part way through.
486        socialAuth.style.display = 'none';
487        verifyForm.style.display = 'block';
488        errorMessage.style.display = 'none';
489
490        const email = result.email || document.getElementById('email').value.trim();
491        const isDeviceCheck = result.kind === 'device';
492        const channel = result.channel || 'email';
493
494        if (channel === 'authenticator') {
495          successText.textContent = 'Enter the 6-digit code from your authenticator app.';
496          verifyHint.textContent = 'Open your authenticator app and enter the current code.';
497          resendCodeButton.style.display = 'none';
498        } else if (channel === 'phone') {
499          successText.textContent = isDeviceCheck
500            ? `We sent a verification code to your phone to confirm this device.`
501            : `Enter the verification code sent to your phone.`;
502          verifyHint.textContent = 'Check your phone and paste the code ViewRank sent you.';
503          resendCodeButton.style.display = 'block';
504        } else {
505          successText.textContent = isDeviceCheck
506            ? `We sent a verification code to ${email} to confirm this device.`
507            : `We sent a verification code to ${email}.`;
508          verifyHint.textContent = 'Check your email and paste the code ViewRank sent you.';
509          resendCodeButton.style.display = 'block';
510        }
511
512        successMessage.style.display = 'block';
513        verificationCodeInput.value = '';
514        verificationCodeInput.focus();
515      }
516
517      function showLoginStep() {
518        verifyForm.style.display = 'none';
519        loginForm.style.display = 'block';
520        if (googleAvailable) socialAuth.style.display = 'block';
521        successMessage.style.display = 'none';
522        errorMessage.style.display = 'none';
523        verificationCodeInput.value = '';
524        loginGuard.reset();
525        verifyGuard.reset();
526      }
527
528      // Holding the Enter key repeats a form's submit event every few
529      // milliseconds, and disabling the button does not stop it. Every form
530      // on this page runs one attempt at a time. The auth calls have no
531      // timeout of their own, so a call that never answers releases the form
532      // after STALL_MS and invites a retry instead of leaving a dead button.
533      const STALL_MS = 20000;
534
535      const track = (name, props) => {
536        if (window.METLY_ANALYTICS) window.METLY_ANALYTICS.track(name, props);
537      };
538
539      const failureReason = (error) =>
540        String((error && error.message) || 'unknown').replace(/\s+/g, ' ').trim().slice(0, 100);
541
542      // Ask Clerk whether Google is connected before offering it.
543      window.METLY_AUTH.googleEnabled().then((enabled) => {
544        googleAvailable = enabled;
545        if (enabled && verifyForm.style.display !== 'block') {
546          socialAuth.style.display = 'block';
547        }
548      });
549
550      const releaseGoogleButton = () => {
551        googleButton.disabled = false;
552        googleButtonText.textContent = 'Continue with Google';
553      };
554
555      googleButton.addEventListener('click', async () => {
556        if (googleButton.disabled) return;
557        errorMessage.style.display = 'none';
558        googleButton.disabled = true;
559        googleButtonText.textContent = 'Taking you to Google...';
560        track('google_auth_started', { mode: 'login' });
561
562        // The browser leaves this page when this works, so the timer below
563        // only ever fires when Clerk has not answered. Without it the button
564        // stays disabled for good and the person has nothing left to press.
565        const startedAt = Date.now();
566        let stalled = false;
567        const stallTimer = setTimeout(() => {
568          stalled = true;
569          releaseGoogleButton();
570          showError('Google is taking longer than usual. Please try again, or use your email and password below.');
571          track('google_auth_stalled', { mode: 'login', waited_ms: STALL_MS });
572        }, STALL_MS);
573
574        try {
575          await window.METLY_AUTH.continueWithGoogle('login');
576          clearTimeout(stallTimer);
577        } catch (error) {
578          clearTimeout(stallTimer);
579          track('google_auth_failed', {
580            mode: 'login',
581            reason: failureReason(error),
582            duration_ms: Date.now() - startedAt
583          });
584          if (!stalled) {
585            showError(error.message || 'Could not continue with Google. Please try again.');
586            releaseGoogleButton();
587          }
588        }
589      });
590
591      // Wires one form (or button) to run one attempt at a time.
592      //   step:     label carried on the login_* events
593      //   run:      does the work; return true to keep the button locked
594      //             because a redirect is about to happen
595      //   fallback: error shown when the call fails without a message
596      //   cooldownMs: after a success, keep the button locked this long
597      //             (stops a mashed "send a new code" sending several)
598      function guard({ element, event = 'submit', button, busyText, idleText, step, run, fallback, countFirst = false, cooldownMs = 0 }) {
599        let inFlight = false;
600        let attempts = 0;
601
602        const unlock = () => {
603          inFlight = false;
604          button.disabled = false;
605          button.textContent = idleText;
606        };
607
608        element.addEventListener(event, async (e) => {
609          if (e && e.preventDefault) e.preventDefault();
610          if (inFlight) return;
611          inFlight = true;
612          button.disabled = true;
613          button.textContent = busyText;
614          errorMessage.style.display = 'none';
615
616          attempts += 1;
617          if (countFirst && attempts === 1) track('login_submitted', { step });
618
619          const startedAt = Date.now();
620          let stalled = false;
621          const stallTimer = setTimeout(() => {
622            stalled = true;
623            unlock();
624            showError('This is taking longer than usual. Please try again.');
625            track('login_stalled', { step, attempt: attempts, waited_ms: STALL_MS });
626          }, STALL_MS);
627
628          let keepLocked = false;
629          let failed = false;
630          try {
631            keepLocked = (await run()) === true;
632          } catch (error) {
633            failed = true;
634            track('login_failed', {
635              step,
636              attempt: attempts,
637              reason: failureReason(error),
638              duration_ms: Date.now() - startedAt
639            });
640            showError(error.message || fallback);
641          } finally {
642            clearTimeout(stallTimer);
643            // After a stall the form is already open again and a fresh
644            // attempt may be running: leave its state alone.
645            if (stalled || keepLocked) return;
646            if (cooldownMs && !failed) {
647              button.textContent = idleText;
648              setTimeout(unlock, cooldownMs);
649            } else {
650              unlock();
651            }
652          }
653        });
654
655        return { reset: unlock };
656      }
657
658      const loginGuard = guard({
659        element: loginForm,
660        button: loginButton,
661        busyText: 'Signing in...',
662        idleText: 'Sign In',
663        step: 'password',
664        countFirst: true,
665        fallback: 'Failed to sign in. Please check your credentials.',
666        run: async () => {
667          successMessage.style.display = 'none';
668          const email = document.getElementById('email').value;
669          const password = document.getElementById('password').value;
670          const result = await window.METLY_AUTH.login(email, password);
671          if (result?.status === 'needs_verification') {
672            showVerificationStep(result);
673          }
674          // Stay locked: either the redirect follows, or the code step is
675          // showing and the back button reopens this form.
676          return true;
677        }
678      });
679
680      const verifyGuard = guard({
681        element: verifyForm,
682        button: verifyButton,
683        busyText: 'Verifying...',
684        idleText: 'Verify and Sign In',
685        step: 'code',
686        fallback: 'Failed to verify code.',
687        run: async () => {
688          await window.METLY_AUTH.verifyLoginCode(verificationCodeInput.value.trim());
689          // Stay locked: the redirect to the workspace follows.
690          return true;
691        }
692      });
693
694      guard({
695        element: resendCodeButton,
696        event: 'click',
697        button: resendCodeButton,
698        busyText: 'Sending...',
699        idleText: 'Send a new code',
700        step: 'resend',
701        cooldownMs: 10000,
702        fallback: 'Failed to resend verification code.',
703        run: async () => {
704          await window.METLY_AUTH.resendLoginVerificationCode();
705          showSuccess('A new verification code has been sent.');
706        }
707      });
708
709      backToLoginButton.addEventListener('click', () => {
710        showLoginStep();
711      });
712
713      document.getElementById('show-reset-btn')?.addEventListener('click', () => {
714        resetPanel.style.display = resetPanel.style.display === 'block' ? 'none' : 'block';
715        const email = document.getElementById('email').value.trim();
716        if (email) document.getElementById('reset-email').value = email;
717      });
718
719      guard({
720        element: resetRequestForm,
721        button: resetRequestButton,
722        busyText: 'Sending...',
723        idleText: 'Send Reset Code',
724        step: 'reset_request',
725        fallback: 'Failed to send reset code.',
726        run: async () => {
727          successMessage.style.display = 'none';
728          await window.METLY_AUTH.startPasswordReset(document.getElementById('reset-email').value.trim());
729          resetRequestForm.style.display = 'none';
730          resetCompleteForm.style.display = 'block';
731          showSuccess('Reset code sent. Check your email, then enter the code and a new password.');
732          // Stay locked: this form is hidden from here on.
733          return true;
734        }
735      });
736
737      guard({
738        element: resetCompleteForm,
739        button: resetCompleteButton,
740        busyText: 'Resetting...',
741        idleText: 'Reset Password',
742        step: 'reset_complete',
743        fallback: 'Failed to reset password.',
744        run: async () => {
745          successMessage.style.display = 'none';
746          await window.METLY_AUTH.completePasswordReset(
747            document.getElementById('reset-code').value.trim(),
748            document.getElementById('new-password').value
749          );
750          // Stay locked: the redirect to the workspace follows.
751          return true;
752        }
753      });
754
755      // Every handler is attached: a press held while the page loaded runs now.
756      if (window.METLY_FORMS_READY) window.METLY_FORMS_READY();
757    });
758  </script>
758
759</body>
760</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.