PageSourceSearch

https://cadient.ai/auth/login?next=/?

html cadient.ai collected 2026-10-01 12:01:34 UTC 34,854 bytes, 726 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5    <link rel="icon" type="image/png" href="/static/images/favicon_cadientai.png">
6    <meta charset="UTF-8">
7    <meta name="viewport" content="width=device-width, initial-scale=1.0">
8    <title>Cadient - Login</title>
9
10    <!-- Tailwind CSS -->
11    
12    <link rel="stylesheet" href="/static/css/tailwind-auth.css?v=c3e3d9026a">
13    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
14
15    <!-- Lucide Icons (New Standard) -->
16    
16<script src="https://unpkg.com/[email protected]"></script>
16
17
18    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
19
20    <style>
21        body {
22            font-family: 'Inter', sans-serif;
23        }
24
25        .fade-in {
26            animation: fadeIn 0.5s ease-in-out;
27        }
28
29        @keyframes fadeIn {
30            from {
31                opacity: 0;
32                transform: translateY(10px);
33            }
34
35            to {
36                opacity: 1;
37                transform: translateY(0);
38            }
39        }
40
41        input::-ms-reveal,
42        input::-ms-clear {
43            display: none;
44        }
45    </style>
46
47    
48    
49    <link rel="canonical" href="https://cadient.ai/auth/login">
50    
51
52<meta name="robots" content="noindex, nofollow">
53    
54
55
56<script>
57    window.dataLayer = window.dataLayer || [];
58    function gtag() { dataLayer.push(arguments); }
59    (function () {
60        // Single source: app.py parses GTM_CONTAINER_IDS once into the GTM_IDS
61        // Jinja global, which _gtm_noscript.html reads too — see the note there.
62        // Fails closed (no containers) if it is somehow undefined.
63        var GTM_IDS = ["GTM-WWP5NR36", "GTM-PDRS4M23"];
64        // Configured, not hardcoded, and each vendor is skipped below when its
65        // ID is blank. ANALYTICS_ENABLED stays the master switch for the whole
66        // stack; these decide WHICH vendors that switch turns on.
67        //
68        // That distinction is what makes a non-prod environment testable. These
69        // two used to be literals, so the only way to exercise any vendor on dev
70        // was ANALYTICS_ENABLED=true, which also pointed dev traffic at the
71        // PRODUCTION GA4 property and Clarity project — quietly polluting the
72        // baseline the SEO programme is measured against. Now dev can enable the
73        // stack with these blank and run a single vendor in isolation.
74        //
75        // The defaults are the live prod IDs, so an environment that sets
76        // neither behaves exactly as before.
77        var GA_ID = "G-1T86EMJJXX";
78        var CLARITY_ID = "vfab1wc1af";
79        var started = false;
80
81        // ── Embedded (iframe) awareness ────────────────────────────────────────
82        // Set by pages that are normally rendered inside an iframe on one of our
83        // own pages — the scheduler at /lp/schedule. Such a page is a WIDGET, not
84        // a destination: the visitor never navigated to /lp/schedule, so a
85        // page_view against that URL is a phantom hit that inflates its views and
86        // distorts engagement for the property (LEAD_AUDIT.md F19).
87        //
88        // The old fix was to load NOTHING when embedded. That killed the phantom
89        // page_view but also blinded us on the same page's direct loads — and
90        // /lp/schedule has 19 real hrefs pointing at it, plus cross-origin
91        // WordPress embeds where the parent may carry no tags of ours at all. So
92        // it traded a known over-count for an unknown blind spot on the surface
93        // that books demos.
94        //
95        // What is actually unwanted is the automatic page_view, not the stack. So
96        // load everything either way and decide the page_view at RUNTIME, which
97        // is also the only place the answer is knowable: whether we are framed is
98        // not a fact the server has.
99        //
100        // IMPORTANT — this only covers the page_view that OUR gtag config sends.
101        // If a GTM container holds a GA4 tag firing on All Pages, that tag sends
102        // its own and no template change can stop it. `embedded` is pushed to the
103        // dataLayer BEFORE gtm.js loads so the container can carry a trigger
104        // exception on it; until someone adds that exception in the GTM UI, the
105        // container's own page_view still fires here. See LEAD_AUDIT.md F19 and
106        // the unresolved two-container question above.
107        var embedded = false && (function () {
108            try { return window.self !== window.top; } catch (e) { return true; }
109        })();
110
111        function loadAnalytics() {
112            if (started) return; started = true;
113            // Google Tag Manager
114            dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
115            if (embedded) { dataLayer.push({ embedded: true }); }
116            GTM_IDS.forEach(function (id) {
117                var s = document.createElement('script');
118                s.async = true;
119                s.src = 'https://www.googletagmanager.com/gtm.js?id=' + id;
120                document.head.appendChild(s);
121            });
122            // Google Analytics (gtag.js). Skipped entirely without an ID —
123            // loading gtag.js with an empty id still opens a connection to
124            // Google and registers a property-less client, so the guard has to
125            // be here rather than relying on GA to ignore the config call.
126            if (GA_ID) {
127                var g = document.createElement('script');
128                g.async = true;
129                g.src = 'https://www.googletagmanager.com/gtag/js?i
129d=' + GA_ID;
130                document.head.appendChild(g);
131                gtag('js', new Date());
132                // send_page_view:false keeps gtag() live for event hits (a booking is
133                // still measurable) while omitting the phantom navigation hit.
134                gtag('config', GA_ID, { send_page_view: !embedded });
135            }
136            // Microsoft Clarity
137            if (CLARITY_ID) {
138                (function (c, l, a, r, i, t, y) {
139                    c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments) };
140                    t = l.createElement(r); t.async = 1; t.src = 'https://www.clarity.ms/tag/' + i;
141                    y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
142                })(window, document, 'clarity', 'script', CLARITY_ID);
143            }
144        }
145        var evts = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
146        function onFirst() {
147            evts.forEach(function (e) { window.removeEventListener(e, onFirst); });
148            loadAnalytics();
149        }
150        evts.forEach(function (e) { window.addEventListener(e, onFirst, { passive: true }); });
151        window.addEventListener('load', function () {
152            if ('requestIdleCallback' in window) { requestIdleCallback(loadAnalytics, { timeout: 4000 }); }
153            else { setTimeout(loadAnalytics, 3000); }
154        });
155    })();
156</script>
156
157
158
159
160<script>
161    (function () {
162        // Queue stub first and synchronously: it is the whole point of the
163        // vendor's snippet shape. Anything measured before the SDK lands is
164        // held here and replayed, so deferring the network load cannot lose an
165        // event.
166        window.oaiq = window.oaiq || function () {
167            (window.oaiq.q = window.oaiq.q || []).push(arguments);
168        };
169
170        var PIXEL_ID = "QnvACeEzLpeAcz9ok3djRz";
171
172        // Same question the page_view asks: are we the scheduler running
173        // inside one of our own pages? If so the parent owns measurement.
174        var embedded = (function () {
175            try { return window.self !== window.top; } catch (e) { return true; }
176        })();
177        if (embedded) return;
178
179        oaiq('init', { pixelId: PIXEL_ID });
180
181        var loaded = false;
182        function loadPixel() {
183            if (loaded) return; loaded = true;
184            var s = document.createElement('script');
185            s.async = true;
186            s.src = 'https://bzrcdn.openai.com/sdk/oaiq.min.js';
187            document.head.appendChild(s);
188        }
189        var evts = ['scroll', 'mousemove', 'touchstart', 'keydown', 'click'];
190        function onFirst() {
191            evts.forEach(function (e) { window.removeEventListener(e, onFirst); });
192            loadPixel();
193        }
194        evts.forEach(function (e) { window.addEventListener(e, onFirst, { passive: true }); });
195        window.addEventListener('load', function () {
196            if ('requestIdleCallback' in window) { requestIdleCallback(loadPixel, { timeout: 4000 }); }
197            else { setTimeout(loadPixel, 3000); }
198        });
199
200        // Our event names -> OpenAI's taxonomy. Both are `customer_action`;
201        // neither carries an amount, because a booked demo has no transaction
202        // value we can state honestly. Adding an invented one would flow
203        // straight into their bidding.
204        var EVENTS = {
205            demo_booking_success: 'appointment_scheduled',
206            playbook_request_success: 'lead_created'
207        };
208
209        // Fired at most once per event name per page: the two channels below
210        // can both be live on one page, and a booking must not be reported
211        // twice because it arrived by two routes.
212        var sent = {};
213
214        function measureConversion(name, detail) {
215            try {
216                var mapped = EVENTS[name];
217                if (!mapped || sent[mapped]) return;
218                sent[mapped] = true;
219                var options = {};
220                // event_id is what lets a future Conversions API call dedupe
221                // against this one. The playbook flows have a real lead id to
222                // key on; the iframe route has none on the parent, so it goes
223                // without and cannot be deduped server-side until the slot is
224                // threaded back. Sharing one id across the two event NAMES is
225                // fine — the dedup key is (pixel, event name, event_id).
226                if (detail && detail.eventId) options.event_id = String(detail.eventId);
227                oaiq('measure', mapped, { type: 'customer_action' }, options);
228            } catch (e) { /* measurement must never break the page */ }
229        }
230        window.cadientMeasureConversion = measureConversion;
231
232        // Route 1: the scheduler iframe announcing a confirmed booking. Same
233        // message the ad consultants' own listener consumes. Origin-checked —
234        // the frame posts to '*' because it is embedded cross-origin by the
235        // WordPress estate, so the check belongs here, on the receiving end.
236        window.addEventListener('message', function (e) {
237            if (e.origin !== window.location.origin &&
238                e.origin !== 'https://cadient.ai' &&
239                e.origin !== 'https://dev.cadient.ai') return;
240            if (!e.data || e.data.event !== 'demo_booking_success') return;
241            measureConversion('demo_booking_success', e.data);
242        });
243    })();
244</script>
244
245
246
247<!-- Warmly — visitor de-anonymisation, i.e. analytics, so it rides the same
248     gate as everything above it including suppress_analytics. -->
249
250
251<script id="warmly-script-loader" src="https://opps-widget.getwarmly.com/warmly.js?clientId=5a34a1084b3b8a03e3901742e03c280b" defer></script>
251
252
253
254    
255
256</head>
257
258<body class="bg-slate-50 min-h-screen flex flex-col overflow-x-hidden">
vendor: 75 bytes, lines 258-262
258
259    
260
261
262<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
262GTM-WWP5NR36
vendor: 151 bytes, lines 262-265
262"
263height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
264
265<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
265GTM-PDRS4M23
vendor: 90 bytes, lines 265-269
265"
266height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
267
268
269    
270<header class="w-full bg-white border-b border-slate-200">
271    <div class="max-w-4xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
272        <a href="/" class="inline-flex items-center" aria-label="Cadient home">
273            <img src="/static/img/logo.webp" class="h-8 w-auto" alt="Cadient">
274        </a>
275        <a href="/"
276            class="inline-flex items-center gap-1.5 text-sm font-medium text-slate-500 hover:text-slate-900 transition-colors">
277            <i data-lucide="arrow-left" class="w-4 h-4"></i>
278            Back to home
279        </a>
280    </div>
281</header>
282
283    <main class="flex-1 flex items-center justify-center p-4">
284    
285    <div class="bg-white rounded-2xl shadow-xl overflow-hidden flex max-w-md md:max-w-4xl w-full fade-in">
286
287        <!-- Left Side: Cadient mark + product lockup + value props -->
288        
289<div
290    class="hidden md:flex w-1/2 bg-gradient-to-br from-slate-900 to-slate-800 p-12 flex-col text-white relative overflow-hidden">
291
292    <!-- Zone 1 (top): product lockup — mirrors the landing-page hero tile -->
293    
294    
295
296    <!-- Zone 2 (centered in remaining space): message -->
297    <div class="z-10 flex-1 flex flex-col justify-center py-10">
298        <h2 class="text-3xl font-bold mb-4 leading-tight">Welcome</h2>
299        <p class="text-slate-300 mb-8">Access your Precision Hiring Solutions.</p>
300
301        
302        <ul class="space-y-3">
303            
304            <li class="flex items-start gap-3 text-sm text-slate-200">
305                <i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 shrink-0 opacity-80"></i>
306                <span>One account for the full hiring suite</span>
307            </li>
308            
309            <li class="flex items-start gap-3 text-sm text-slate-200">
310                <i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 shrink-0 opacity-80"></i>
311                <span>Free trial on every product</span>
312            </li>
313            
314            <li class="flex items-start gap-3 text-sm text-slate-200">
315                <i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 shrink-0 opacity-80"></i>
316                <span>No credit card required</span>
317            </li>
318            
319        </ul>
320        
321    </div>
322
323    <!-- Zone 3 (bottom): quiet anchor, balancing the form column's footer link -->
324    <p class="z-10 text-xs text-slate-500">Part of the Cadient hiring suite</p>
325
326    <!-- Abstract Shapes Pattern -->
327    <div class="absolute top-0 right-0 -mr-16 -mt-16 w-64 h-64 rounded-full bg-brand-600 opacity-20 blur-3xl"></div>
328    <div class="absolute bottom-0 left-0 -ml-16 -mb-16 w-64 h-64 rounded-full bg-orange-500 opacity-20 blur-3xl"></div>
329</div>
330
331        <!-- Right Side: Form -->
332        <div class="w-full md:w-1/2 p-8 md:p-12 flex flex-col justify-center relative">
333
334            
335
336            
337            
338
339            
340
341            
342            <h1 class="text-2xl font-bold text-slate-900 mb-1.5">Sign In</h1>
343            <p id="loginSubtext" class="text-sm text-slate-500 mb-7"
344                data-otp="Enter your work email &mdash; we&rsquo;ll send you a six-digit sign-in code."
345                data-password="For accounts that have set a password.">Enter your work email &mdash;
346                we&rsquo;ll send you a six-digit sign-in code.</p>
347
348            
349            <!-- OTP Login Form (default) -->
350            <div id="otp-login-form">
351                <form action="/auth/login-otp-init?next=/?" method="POST">
352                    
353                    <div class="mb-6">
354                        <label for="otpLoginEmailInput" class="block text-sm font-medium text-slate-700 mb-1">Work Email</label>
355                        <input type="email" name="email" id="otpLoginEmailInput" required autofocus autocomplete="username"
356                            value=""
357                            class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-brand-600 outline-none">
358                    </div>
359
360                    
361                    <button type="submit"
362                        class="group w-full bg-brand-600 hover:bg-brand-700 text-white font-semibold py-3 rounded-lg shadow-lg transition-all flex items-center justify-center gap-2">
363                        Continue
364                        <i data-lucide="arrow-right" class="w-4 h-4 transition-transform group-hover:translate-x-0.5"></i>
365                    </button>
366
367                </form>
368            </div>
369
370            <!-- Password Login Form (hidden by default) -->
371            <div id="password-login-form" class="hidden">
372                <form action="/auth/login?next=/?" method="POST">
373                    
374                    <div class="mb-4">
375                        <label for="passwordLoginEmailInput" class="block text-sm font-medium text-slate-700 mb-1">Work Email</label>
376                        <input type="email" name="email" id="passwordLoginEmailInput" required autocomplete="username"
377                            class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-brand-600 outline-none">
378                    </div>
379
380                    <div class="mb-2">
381                        <label for="loginPasswordInput" class="block text-sm font-medium text-slate-700 mb-1">Password</label>
382                        <div class="relative">
383                            <input type="password" name="password" id="loginPasswordInput" required autocomplete="current-password"
384                                class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-brand-600 outline-none pr-10">
385                            <button type="button" id="toggleLoginPassword" aria-label="Show password" class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 focus:outline-none">
386                                <i data-lucide="eye" class="w-5 h-5"></i>
387                            </button>
388                        </div>
389                    </div>
390
391                    <div class="mb-6 text-right">
392                        <a href="/auth/forgot-password"
393                            class="text-sm text-brand-600 hover:underline">
394                            Forgot password?
395                        </a>
396                    </div>
397
398                    <button type="submit"
399                        class="w-full bg-brand-600 hover:bg-brand-700 text-white font-semibold py-3 rounded-lg shadow-lg transition-all flex items-center justify-center gap-2">
400                        Sign In <i data-lucide="arrow-right" class="w-4 h-4"></i>
401                    </button>
402
403                </form>
404            </div>
405
406            
407            
408                
409
410
411<div id="googleSignInDivider" class="flex items-center gap-3 mt-6 mb-4">
412    <div class="flex-1 h-px bg-slate-200"></div>
413    <span class="text-[11px] font-medium text-slate-400 uppercase tracking-wider whitespace-nowrap">
414        or sign in with
415    </span>
416    <div class="flex-1 h-px bg-slate-200"></div>
417</div>
418
419
420<div id="googleSignInError"
421     class="hidden bg-red-50 text-red-600 border border-red-100 p-3 rounded-lg mb-3 text-sm flex items-center gap-2">
422    <i data-lucide="alert-circle" class="w-4 h-4 shrink-0"></i>
423    <span id="googleSignInErrorText" class="flex-1"></span>
424</div>
425
426<div id="googleSignInDiv" class="flex justify-center"></div>
427
428<script src="https://accounts.google.com/gsi/client" async defer></script>
vendor: 1 bytes, line 428
428
429<script>
430    // SmartShield's two acknowledgements (register.html) are a compliance
431    // requirement that must precede account creation — same gate the manual
432    // form's `required` checkboxes enforce, but those never fire for this
433    // out-of-band Google flow, so it's re-checked here before ever calling
434    // Google, and the checked state is forwarded so the server can enforce
435    // it too (auth_routes.google_signup). A no-op (returns true) on any page
436    // without these checkboxes, e.g. login.html or a non-SmartShield register.
437    function isChecked(id) {
438        const el = document.getElementById(id);
439        return !!(el && el.checked);
440    }
441
442    function ssAttestationChecked() {
443        const att = document.getElementById('ssAttestationCheckbox');
444        const pol = document.getElementById('ssPoliciesCheckbox');
445        if (!att && !pol) return true;
446        return isChecked('ssAttestationCheckbox') && isChecked('ssPoliciesCheckbox');
447    }
448
449    function handleGoogleCredential(response) {
450        const errorBox = document.getElementById('googleSignInError');
451        const errorText = document.getElementById('googleSignInErrorText');
452        errorBox.classList.add('hidden');
453
454        if (!ssAttestationChecked()) {
455            // "above" again. This flipped twice: the banner said "above" while
456            // the checkboxes were below it, was corrected to "below" when this
457            // partial sat at the top of the card, and is now back to "above"
458            // because the partial moved beneath the form the checkboxes live
459            // in. Verify the direction against the rendered page if this block
460            // is ever moved again — it is not guessable from this file alone.
461            errorText.textContent = 'Please acknowledge the statements above before continuing.';
462            errorBox.classList.remove('hidden');
463            return;
464        }
465
466        fetch("/auth/google-signup", {
467            method: 'POST',
468            headers: { 'Content-Type': 'application/json' },
469            body: JSON.stringify({
470                credential: response.credential,
471                tool: "",
472                smart_shield_attestation: isChecked('ssAttestationCheckbox'),
473                smart_shield_policies: isChecked('ssPoliciesCheckbox'),
474                // Whether THIS page can even collect the attestation. The
475                // checkboxes exist only on the SmartShield register page, so on
476                // the login page the two flags above can only ever be false —
477                // and the server refuses to create a SmartShield account
478                // without them. Without this flag the server cannot tell
479                // "user didn't tick the box" from "the box isn't on this page",
480                // and a new user signing in with Google from the login page got
481                // "Please confirm the research-assist statement" pointing at a
482                // statement that wasn't there. See auth_routes.google_signup.
483                attestation_available: !!document.getElementById('ssAttestationCheckbox')
484            })
485        })
486        .then(res => res.json().then(data => ({ ok: res.ok, data })))
487        .then(({ ok, data }) => {
488            if (ok && data.redirect) {
489                window.location.href = data.redirect;
490            } else {
491                errorText.textContent = data.error || 'Google sign-in failed. Please try again.';
492                errorBox.classList.remove('hidden');
493            }
494        })
495        .catch(() => {
496            errorText.textContent = 'Google sign-in failed. Please try again.';
497            errorBox.classList.remove('hidden');
498        });
499    }
500
501    // Render the button as soon as the GIS library is available.
502    //
503    // Two problems with waiting on window 'load' and bailing when
504    // window.google is missing, which is what this used to do:
505    //
506    //   1. RACE — the GIS script is `async defer`, so on a slow connection it
507    //      can resolve AFTER 'load' has already fired. The one-shot check then
508    //      bails and nothing ever renders, with no retry. Never reproduces on
509    //      fast local dev; does reproduce in the field.
510    //   2. SILENT FAILURE — when accounts.google.com is unreachable at all
511    //      (ad blocker, corporate proxy, extension, offline), the user just
512    //      sees an empty gap where the button should be and no reason to look
513    //      at the email field instead. Recruiters behind corporate IT are
514    //      exactly the population that hits this.
515    //
516    // So: poll briefly for the library, then give up with a VISIBLE message
517    // that points at the working path. The "or sign in with" divider is hidden
518    // too — a group label introducing an empty space reads as a broken page —
519    // so what remains is an ordinary email form with nothing missing.
520    (function () {
521        var POLL_MS = 100;
522        var TIMEOUT_MS = 8000;   // generous; GIS normally lands in <1s
523        var waited = 0;
524
525        // One Tap is offered only to anonymous visitors. Prompting someone who
526        // already has a session would hand us a credential for an account
527        // they're signed into, which at best does nothing and at worst swaps
528        // the session mid-visit. `one_tap` lets a page opt out entirely.
529        var ONE_TAP = true
530                      && true;
531
532        function giveUp() {
533            var box = document.getElementById('googleSignInError');
534            var text = document.getElementById('googleSignInErrorText');
535            var holder = document.getElementById('googleSignInDiv');
536            var divider = document.getElementById('googleSignInDivider');
537            if (holder) holder.classList.add('hidden');
538            if (divider) divider.classList.add('hidden');
539            if (box && text) {
540                // "above", not "below": the email form now precedes this block.
541                text.textContent = 'Google sign-in is unavailable right now — ' +
542                    'continue with your email above instead.';
543                box.classList.remove('hidden');
544            }
545            if (window.console) {
546                console.warn('[auth] GIS library never loaded; Google button hidden.');
547            }
548        }
549
550        // GIS needs an explicit pixel width — it has no "100%". A hardcoded
551        // 320 left the button visibly narrower than the email field and CTA
552        // (352px in the desktop card), inset ~16px on each side, and would
553        // OVERFLOW a card narrower than 320. Measure the holder instead so the
554        // button's edges line up with the form at any viewport. GIS clamps to
555        // 400; floor at 200 so a mid-layout measurement can't produce a stub.
556        function buttonWidth(holder) {
557            var w = Math.round(holder.getBoundingClientRect().width);
558            if (!w) return 320;                       // holder not laid out yet
559            return Math.max(200, Math.min(400, w));
560        }
561
562        function tryRender() {
563            if (window.google && window.google.accounts && window.google.accounts.id) {
564                try {
565                    var holder = document.getElementById('googleSignInDiv');
566                    google.accounts.id.initialize({
567                        client_id: "116942318144-m5d5dsqfdj2cqvqm3rmo2kr8s30935vb.apps.googleusercontent.com",
568                        callback: handleGoogleCredential,
569                        // 'signin' / 'signup' asks Google to word the One Tap
570                        // prompt to match the page. Under FedCM the BROWSER
571                        // renders that dialog, so treat this as a hint, not a
572                        // guarantee — Chrome shows the top-level domain rather
573                        // than the app name regardless.
574                        context: "signin"
575                    });
576                    // logo_alignment 'center' groups the G with the label and
577                    // centres the pair. The default ('left') pins the G to the
578                    // left edge while centring the text independently, which at
579                    // our 352px width left a ~90px gap between them and was the
580                    // main reason the button read as foreign to the card.
581                    //
582                    // Compared against the alternatives at the real card width:
583                    // 'pill' gives a 20px radius against our 8px inputs (worse),
584                    // and theme 'filled_black' forces a white tile behind the G
585                    // (guidelines require the mark on white) which looks broken
586                    // and competes with the orange CTA. 'rectangular' is 4px —
587                    // not our 8px, but the closest GIS offers.
588                    //
589                    // Height is stuck at 40px vs the CTA's 48px: GIS exposes no
590                    // height parameter and 'large' is its maximum. Matching that
591                    // needs a custom button, which means giving up renderButton
592                    // and the ID token it returns — a backend change, not a CSS
593                    // one. Google does permit custom buttons, within limits:
594                    // https://developers.google.com/identity/branding-guidelines
595                    google.accounts.id.renderButton(
596                        holder,
597                        { theme: 'outline', size: 'large', shape: 'rectangular',
598                          logo_alignment: 'center',
599                          width: buttonWidth(holder), text: 'continue_with' }
600                    );
601
602                    // One Tap: the unprompted account dialog. Independent of
603                    // renderButton — prompt() opens it whether or not a button
604                    // was drawn — and it delivers a credential to the SAME
605                    // callback, so no new server path is needed.
606                    //
607                    // Suppressed when an unsatisfied compliance gate is on the
608                    // page. ssAttestationChecked() returns true where there are
609                    // no checkboxes, so this only bites on SmartShield's
610                    // register page: One Tap appears before the user can tick
611                    // the attestation, so tapping it would be rejected by
612                    // handleGoogleCredential and the user would get an error
613                    // for accepting an offer we made. Better not to offer.
614                    //
615                    // Not enabling auto_select (zero-click sign-in): it needs
616                    // google.accounts.id.disableAutoSelect() wired into logout
617                    // first, or a signed-out user gets silently signed back in.
618                    // Called with NO moment-listener callback on purpose. Passing
619                    // one makes GIS log a deprecation warning, because most of
620                    // PromptMomentNotification (the display and skip reasons)
621                    // was removed for privacy under FedCM. We only ever used it
622                    // for a console line, so there is nothing to trade away.
623                    if (ONE_TAP && ssAttestationChecked()) {
624                        google.accounts.id.prompt();
625                    }
626                } catch (e) {
627                    // initialize() throws on a malformed/missing client_id.
628                    if (window.console) console.warn('[auth] GIS init failed:', e);
629                    giveUp();
630                }
631                return;
632            }
633            waited += POLL_MS;
634            if (waited >= TIMEOUT_MS) { giveUp(); return; }
635            setTimeout(tryRender, POLL_MS);
636        }
637
638        tryRender();   // no 'load' dependency — start immediately
639    })();
640</script>
640
641
642            
643
644            
645            <div class="mt-5 text-center">
646                <button type="button" id="loginMethodToggle" onclick="toggleLoginMethod()"
647                    data-otp="Sign in with a password instead"
648                    data-password="Email me a sign-in code instead"
649                    class="text-sm text-brand-600 hover:underline font-medium">
650                    Sign in with a password instead
651                </button>
652            </div>
653
654            <div class="mt-6 text-center text-sm text-slate-500">
655                Don't have an account?
656                
657                <a href="/auth/register?next=/?" rel="nofollow"
658                    class="text-brand-600 font-semibold hover:underline">Create Account</a>
659            </div>
660        </div>
661    </div>
662    </main>
663
664    
664<script>
665        // Initialize Lucide Icons
666        lucide.createIcons();
667
668        const loginPasswordInput = document.getElementById('loginPasswordInput');
669        const toggleLoginPassword = document.getElementById('toggleLoginPassword');
670        toggleLoginPassword.addEventListener('click', function () {
671            const isPassword = loginPasswordInput.getAttribute('type') === 'password';
672            loginPasswordInput.setAttribute('type', isPassword ? 'text' : 'password');
673            this.setAttribute('aria-label', isPassword ? 'Hide password' : 'Show password');
674            this.innerHTML = isPassword
675                ? '<i data-lucide="eye-off" class="w-5 h-5"></i>'
676                : '<i data-lucide="eye" class="w-5 h-5"></i>';
677            lucide.createIcons();
678        });
679
680        function toggleLoginMethod() {
681            const passwordForm = document.getElementById('password-login-form');
682            const otpForm = document.getElementById('otp-login-form');
683
684            const showPassword = passwordForm.classList.contains('hidden');
685            passwordForm.classList.toggle('hidden', !showPassword);
686            otpForm.classList.toggle('hidden', showPassword);
687
688            // The heading and subtext now live above the shared Google button,
689            // outside both panes, so the subtext has to follow the toggle.
690            const subtext = document.getElementById('loginSubtext');
691            if (subtext) {
692                subtext.textContent = showPassword
693                    ? subtext.dataset.password
694                    : subtext.dataset.otp;
695            }
696
697            // Same reason: the toggle now lives below the shared Google block
698            // rather than inside the pane, so its own label has to follow.
699            const toggle = document.getElementById('loginMethodToggle');
700            if (toggle) {
701                toggle.textContent = showPassword
702                    ? toggle.dataset.password
703                    : toggle.dataset.otp;
704            }
705        }
706
707        // The OTP form is the default pane. Open the password form instead
708        // when the context is explicitly about passwords:
709        //   · ?method=password deep link
710        //   · ?reset_success=1 — the banner says "sign in with your new
711        //     password", so the password form must be showing
712        //   · server-rendered method='password' — a failed password attempt
713        //     re-renders this page and must not dump the user back on the
714        //     code form mid-task
715        // ?method=otp (welcome emails, pwless/finish redirects) is now a
716        // harmless no-op since OTP is already the default.
717        const params = new URLSearchParams(window.location.search);
718        const initialMethod = "" || params.get('method') || '';
719        if (initialMethod === 'password' || params.get('reset_success')) {
720            toggleLoginMethod();
721        }
722    </script>
722
723
724</body>
725
726</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.