PageSourceSearch

https://vajo.com/auth/login.php?msg=signup_required

html vajo.com collected 2026-09-24 14:19:38 UTC 37,878 bytes, 647 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>Vajo.com - Login</title>
7    
7<script src="https://cdn.tailwindcss.com"></script>
vendor: 85 bytes, lines 7-9
7
8    <script src="https://www.google.com/recaptcha/api.js" async defer></script>
9    
9<style>
10        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap');
11        body { font-family: 'Inter', sans-serif; background-color: #f8fafc; }
12        .g-recaptcha > div { margin: 0 auto; } /* Centers the widget */
13    </style>
14<!-- Google Tag Manager -->
15<script>
vendor: 327 bytes, lines 15-19
15(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
16new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
17j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
18'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
19})(window,document,'script','dataLayer','
19GTM-5TCHGBTM
vendor: 3 bytes, line 19
19');
19</script>
19
20<!-- End Google Tag Manager -->
21<!-- Privacy-friendly analytics by Plausible -->
22<script async src="https://plausible.io/js/pa-r-Vzsm9Uo63uO6vR2faMY.js"></script>
vendor: 1 bytes, line 22
22
23<script>
24  window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
25  plausible.init()
26</script>
26
27<!-- Tolt affiliate/referral tracking — sets window.tolt_referral once a
28     visitor arrives via an affiliate's link. See includes/affiliates.php's
29     docblock for how this fits alongside (not instead of) our own ?ref=
30     cookie system. -->
31<script async src="https://files.tlt-cdn.com/tlt.js" data-tolt="pk_fVJDNvT4Dn4QBooB93acc4Po"></script>
31
32<!-- User-journey events: custom funnel events (Plausible + GTM dataLayer) + a data-evt tagging hook -->
33<script>
34(function () {
35  function track(name, props) {
36    if (!name) return;
37    // Plausible custom event
38    if (typeof window.plausible === 'function') {
39      window.plausible(name, props ? { props: props } : undefined);
40    }
41    // GTM dataLayer (only present when GTM is enabled) → usable by GA4
42    if (window.dataLayer && typeof window.dataLayer.push === 'function') {
43      var payload = { event: name };
44      if (props) { for (var k in props) { if (Object.prototype.hasOwnProperty.call(props, k)) payload[k] = props[k]; } }
45      window.dataLayer.push(payload);
46    }
47  }
48  // Exposed globally so other inline scripts on the page (e.g.
49  // includes/signup-gate.php's popup) can fire the same funnel events
50  // without duplicating the Plausible/GTM dispatch logic.
51  window.lmTrack = track;
52
53  // Appends ": variant" to an event name on the 3 shuffled landing pages
54  // (index / learn-german / speak-german) — same "Landing View: index" /
55  // "Sign Up: index" convention already used above and in
56  // includes/signup-gate.php — so a single call site works both there and on
57  // pages that don't set LM_LANDING_VARIANT (membership/index.php,
58  // sales2.php, …), where the name is used as-is.
59  window.lmTrackVariant = function (base) {
60    return window.LM_LANDING_VARIANT ? (base + ': ' + window.LM_LANDING_VARIANT) : base;
61  };
62
63  // Checkout events carry TWO dimensions (which tier + which page), so the
64  // tier goes after the first colon (matching the convention above) and the
65  // page goes in parens after that, e.g. "Checkout Click: PRO (index)" — vs.
66  // just "Checkout Click: PRO" on pages with no landing variant (sales2.php,
67  // membership/index.php). Keeps every checkout drop-off point self-
68  // explanatory about exactly which plan AND which page it happened on.
69  window.lmTrackCheckout = function (stage, tierLabel) {
70    var base = 'Checkout ' + stage + ': ' + tierLabel;
71    return window.LM_LANDING_VARIANT ? (base + ' (' + window.LM_LANDING_VARIANT + ')') : base;
72  };
73
74  // Which area of the site is this page in? (sent as a property for breakdowns)
75  function section() {
76    var p = location.pathname.toLowerCase();
77    if (p.indexOf('/german-stories/') === 0) return 'german-stories';
78    if (p.indexOf('/beginner/') === 0)       return 'beginner';
79    if (p.indexOf('/kids/') === 0)           return 'kids';
80    if (p.indexOf('/german-grammar/') === 0) return 'grammar-qa';
81    if (p.indexOf('/membership/') === 0)     return 'membership';
82    if (p.indexOf('/flashcards/') === 0)     return 'flashcards';
83    if (p.indexOf('/printables/') === 0)     return 'printables';
84    if (p.indexOf('/auth/') === 0)           return 'auth';
85    if (/\/chapter\d+\.php$/.test(p))        return 'book-chapter';
86    if (p === '/' || p === '/index.php')     return 'home';
87    return 'other';
88  }
89  var SECTION = section();
90
91  // Which of home.php's 3 shuffled landing pages actually rendered (see
92  // includes/header.php - all 3 can be served at the same "/" URL via
93  // home.php's server-side require(), so Plausible's automatic URL-based
94  // pageview tracking can't tell them apart on its own). Fired as its OWN
95  // event name per page (not one shared event name + a property) so each
96  // page's count shows up as its own Goal in Plausible on every plan tier -
97  // property-based breakdowns need the paid Business plan, a separate
98  // event name doesn't.
99  if (window.LM_LANDING_VARIANT) {
100    track('Landing View: ' + window.LM_LANDING_VARIANT);
101  }
102
103  // Shared helper: where a paid checkout should send the buyer so the per-plan
104  // thank-you page fires the purchase conversion, then forwards to the receipt.
105  //   pro            → /pro-thankyou.php
106  //   platinum (new) → /platinum-thankyou.php
107  //   platinum (from pro/premium) → /upgrade-thankyou.php
108  // The receipt URL carries &ty=1 so the generic Purchase below won't double-fire.
109  window.lmThankYouUrl = function (tier, currentTier) {
110    tier = String(tier || '').toLowerCase();
111    currentTier = String(currentTier || '').toLowerCase();
112    var page = (tier === 'platinum')
113      ? ((currentTier === 'pro' || currentTier === 'premium') ? '/upgrade-thankyou.php' : '/platinum-thankyou.php')
114      : '/pro-thankyou.php';
115    return page + '?next=' + encodeURIComponent('/membership/success.php?tier=' + tier + '&ty=1');
116  };
117
118  // Conversion endpoint: a success / thank-you page load (root or membership).
119  // Skip when ?ty=1 — a thank-you page already fired the specific purchase event
120  // and forwarded here, so firing again would double-count.
121  // Two different conversions land here, so the event name says which:
122  //   /success.php            → standalone book purchase (buy.php's checkout)
123  //   /membership/success.php → membership purchase, but ONLY as a defensive
124  //                              fallback (see includes/analytics.php's
125  //                              lmThankYouUrl / the pro-thankyou.php family) —
126  //                              the real membership conversion event is fired
127  //                              by includes/thankyou.php itself.
128  if (/success\.php$/.test(location.pathname.toLowerCase()) && location.search.indexOf('ty=1') === -1) {
129    var purchaseLabel = (location.pathname.toLowerCase().indexOf('/membership/') === 0) ? 'Purchase: Membership (fallback)' : 'Purchase: Book';
130    track(purchaseLabel, { section: SECTION });
131  }
132
133  // Delegated click tracking (capture phase, so it also catches buttons whose
134  // own handlers stop propagation, and elements added to the page later).
135  document.addEventListener('click', function (e) {
136    var el = e.target;
137    if (!el || !el.closest) return;
138
139    // Generic tagging hook: <a data-evt="Event Name" data-evt-foo="bar"> …
140    var tagged = el.closest('[data-evt]');
141    if (tagged) {
142      var props = {};
143      Array.prototype.forEach.call(tagged.attributes, function (a) {
144        if (a.name.indexOf('data-evt-') === 0) props[a.name.slice(9)] = a.value;
145      });
146      track(tagged.getAttribute('data-evt'), Object.keys(props).length ? props : { section: SECTION });
147    }
148
149    // Audio Play — story readers (#lm-play-btn) and kids reader (#btn-play / #bottom-play-pause)
150    if (el.closest('#lm-play-btn, #btn-play, #bottom-play-pause')) {
151      track('Audio Play', { section: SECTION });
152    }
153
154    // Save Progress — the reader "Save Progress" button
155    if (el.closest('[onclick*="saveStoryProgress"]')) {
156      track('Save Progress', { section: SECTION });
157    }
158
159    // Upgrade Click — links toward the pricing / upgrade / membership-plans page
160    // (deliberately excludes /membership/dashboard.php, manage.php, success.php …)
161    // On a shuffle landing page, fired as its own per-page event name (same
162    // reasoning as "Landing View: X" above) so click-through is countable
163    // per page without needing Plausible's paid plan; elsewhere on the site
164    // it's the original shared "Upgrade Click" event.
165    var link = el.closest('a[href]');
166    if (link) {
167      var href = (link.getAttribute('href') || '').toLowerCase();
168      if (/pricing|upgrade/.test(href) || /\/membership\/(index\.php)?(\?|#|$)/.test(href)) {
169        var evtName = window.LM_LANDING_VARIANT ? ('Upgrade Click: ' + window.LM_LANDING_VARIANT) : 'Upgrade Click';
170        track(evtName, { from: SECTION });
171      }
172    }
173  }, true);
174})();
175</script>
175
176</head>
177<body class="min-h-screen flex flex-col items-center justify-center p-2 sm:p-4 relative overflow-hidden">
178<!-- Google Tag Manager (noscript) -->
vendor: 68 bytes, lines 178-179
178
179<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
179GTM-5TCHGBTM
vendor: 83 bytes, lines 179-180
179"
180height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
181<!-- End Google Tag Manager (noscript) -->
182
183    <!-- Decorative scattered book covers — purely visual, behind the card. -->
184    <div class="fixed inset-0 overflow-hidden pointer-events-none select-none" aria-hidden="true">
185        <div class="absolute inset-0 bg-slate-100"></div>
186                <img src="/german-stories/images/covers/eine-baerenstarke-liebe-ostlich-vom-spass-und-westlich-vom-mond.webp" alt="" loading="eager"
187             class="absolute rounded-xl shadow-lg opacity-40"
188             style="left:1%; top:0%; width:101px; transform: rotate(-7deg);">
189                <img src="/german-stories/images/covers/in-yankee-in-camelot-eine-zeitreise-der-besonderen-art.webp" alt="" loading="eager"
190             class="absolute rounded-xl shadow-lg opacity-40"
191             style="left:17.666666666667%; top:-4%; width:134px; transform: rotate(0deg);">
192                <img src="/german-stories/images/covers/eine-bruecke-bauen-annas-und-marks-weg-zur-versoehnung.webp" alt="" loading="eager"
193             class="absolute rounded-xl shadow-lg opacity-40"
194             style="left:34.333333333333%; top:2%; width:102px; transform: rotate(-7deg);">
195                <img src="/german-stories/images/covers/michael-kohlhaas.webp" alt="" loading="eager"
196             class="absolute rounded-xl shadow-lg opacity-40"
197             style="left:48%; top:-4%; width:113px; transform: rotate(-4deg);">
198                <img src="/german-stories/images/covers/die-verwandlung.webp" alt="" loading="eager"
199             class="absolute rounded-xl shadow-lg opacity-40"
200             style="left:68.666666666667%; top:4%; width:117px; transform: rotate(4deg);">
201                <img src="/german-stories/images/covers/der-schimmelreiter.webp" alt="" loading="eager"
202             class="absolute rounded-xl shadow-lg opacity-40"
203             style="left:83.333333333333%; top:4%; width:97px; transform: rotate(12deg);">
204                <img src="/german-stories/images/covers/mitteilungen-aus-den-memoiren-des-satan.webp" alt="" loading="eager"
205             class="absolute rounded-xl shadow-lg opacity-40"
206             style="left:3%; top:28%; width:117px; transform: rotate(-6deg);">
207                <img src="/german-stories/images/covers/das-spiel-des-lebens-und-wie-man-es-spielt.webp" alt="" loading="eager"
208             class="absolute rounded-xl shadow-lg opacity-40"
209             style="left:19.666666666667%; top:28%; width:136px; transform: rotate(-6deg);">
210                <img src="/german-stories/images/covers/uriel-acosta.webp" alt="" loading="eager"
211             class="absolute rounded-xl shadow-lg opacity-40"
212             style="left:30.333333333333%; top:27%; width:130px; transform: rotate(-6deg);">
213                <img src="/german-stories/images/covers/aus-ruinen-zur-republik.webp" alt="" loading="eager"
214             class="absolute rounded-xl shadow-lg opacity-40"
215             style="left:53%; top:23%; width:92px; transform: rotate(-1deg);">
216                <img src="/german-stories/images/covers/tom-sawyer-und-der-gestrichene-zaun.webp" alt="" loading="eager"
217             class="absolute rounded-xl shadow-lg opacity-40"
218             style="left:63.666666666667%; top:26%; width:131px; transform: rotate(-4deg);">
219                <img src="/german-stories/images/covers/rapunzel-eine-lustige-geschichte.webp" alt="" loading="eager"
220             class="absolute rounded-xl shadow-lg opacity-40"
221             style="left:85.333333333333%; top:23%; width:102px; transform: rotate(-11deg);">
222                <img src="/german-stories/images/covers/die-odyssee-der-konnektivitaet-ein-abenteuer-in-deutschland.webp" alt="" loading="eager"
223             class="absolute rounded-xl shadow-lg opacity-40"
224             style="left:-3%; top:49%; width:130px; transform: rotate(-12deg);">
225                <img src="/german-stories/images/covers/der-unsichtbare-mann.webp" alt="" loading="eager"
226             class="absolute rounded-xl shadow-lg opacity-40"
227             style="left:14.666666666667%; top:51%; width:111px; transform: rotate(-12deg);">
228                <img src="/german-stories/images/covers/die-dame-des-sees.webp" alt="" loading="eager"
229             class="absolute rounded-xl shadow-lg opacity-40"
230             style="left:34.333333333333%; top:52%; width:106px; transform: rotate(0deg);">
231                <img src="/german-stories/images/covers/das-schloss-von-otranto.webp" alt="" loading="eager"
232             class="absolute rounded-xl shadow-lg opacity-40"
233             style="left:51%; top:54%; width:128px; transform: rotate(5deg);">
234                <img src="/german-stories/images/covers/die-prinzessin-und-die-sehr-sehr-kleine-erbse.webp" alt="" loading="eager"
235             class="absolute rounded-xl shadow-lg opacity-40"
236             style="left:65.666666666667%; top:50%; width:92px; transform: rotate(5deg);">
237                <img src="/german-stories/images/covers/isabella-von-aegypten.webp" alt="" loading="eager"
238             class="absolute rounded-xl shadow-lg opacity-40"
239             style="left:81.333333333333%; top:53%; width:115px; transform: rotate(5deg);">
240                <img src="/german-stories/images/covers/der-untertan.webp" alt="" loading="eager"
241             class="absolute rounded-xl shadow-lg opacity-40"
242             style="left:3%; top:73%; width:129px; transform: rotate(8deg);">
243                <img src="/german-stories/images/covers/das-leben-napoleons-bonapartes-aufstieg-und-fall-eines-kaisers.webp" alt="" loading="eager"
244             class="absolute rounded-xl shadow-lg opacity-40"
245             style="left:15.666666666667%; top:72%; width:103px; transform: rotate(2deg);">
246                <img src="/german-stories/images/covers/der-krieg-der-welten-eine-deutsche-adaption.webp" alt="" loading="eager"
247             class="absolute rounded-xl shadow-lg opacity-40"
248             style="left:34.333333333333%; top:76%; width:125px; transform: rotate(8deg);">
249                <img src="/german-stories/images/covers/der-froschkoenig.webp" alt="" loading="eager"
250             class="absolute rounded-xl shadow-lg opacity-40"
251             style="left:51%; top:78%; width:120px; transform: rotate(-2deg);">
252                <img src="/german-stories/images/covers/die-reise-der-seewolf.webp" alt="" loading="eager"
253             class="absolute rounded-xl shadow-lg opacity-40"
254             style="left:69.666666666667%; top:76%; width:114px; transform: rotate(11deg);">
255                <img src="/german-stories/images/covers/schneewittchen-und-der-sprechende-spiegel.webp" alt="" loading="eager"
256             class="absolute rounded-xl shadow-lg opacity-40"
257             style="left:81.333333333333%; top:77%; width:101px; transform: rotate(6deg);">
258                <div class="absolute inset-0 bg-gradient-to-b from-white/70 via-white/50 to-white/75"></div>
259    </div>
260
261    <div class="max-w-md w-full bg-white rounded-3xl shadow-xl border border-slate-200 overflow-hidden relative z-10">
262        
263        <!-- Header & Logo -->
264        <div class="bg-slate-900 px-5 py-5 sm:px-8 sm:py-10 text-center text-white">
265
266            <!-- LOGO SECTION -->
267            <div class="flex justify-center items-center gap-2 mb-0 sm:mb-6">
268                <img src="/auth/logo.png" alt="Logo" class="h-8">
269
270                <span class="text-2xl font-black tracking-tight text-white">Vajo.com</span>
271            </div>
272
273            <!-- Hidden on tiny phones to keep the Log In button inside the viewport — -->
274            <!-- the logo above already identifies the page. -->
275            <h1 id="formTitle" class="hidden sm:block text-3xl font-extrabold tracking-tight mb-2">Welcome Back</h1>
276            <p id="formSubtitle" class="hidden sm:block text-slate-400">Log in to continue your German journey.</p>
277        </div>
278
279        <div class="p-5 sm:p-8">
280            <!-- URL Alerts -->
281                            <div class="mb-6 bg-blue-50 text-blue-700 p-4 rounded-xl text-sm font-bold text-center">
282                    Please create a free account or log in to access this material.                </div>
283            
284            <!-- JS Error Alerts -->
285            <div id="alertBox" class="hidden mb-6 p-4 rounded-xl text-sm font-bold text-center border"></div>
286
287            <form id="authForm" onsubmit="handleAuth(event)" class="space-y-3 sm:space-y-5">
288
289                <div>
290                    <label class="block text-sm font-bold text-slate-700 mb-1">Email Address</label>
291                    <input type="email" id="email" required placeholder="[email protected]" oninput="checkCompEmail()" value=""
292                           class="w-full px-4 py-2.5 sm:py-3 rounded-xl border border-slate-200 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all bg-slate-50 focus:bg-white">
293                    <p id="compHint" class="hidden mt-2 text-xs font-bold text-emerald-600">✓ Complimentary teacher pass detected — no password needed. Just click “Log In”.</p>
294                </div>
295
296                <div id="passwordFieldContainer">
297                    <div class="flex justify-between items-center mb-1">
298                        <label class="block text-sm font-bold text-slate-700">Password</label>
299                        <button type="button" onclick="setMode('forgot')" id="forgotLink" class="text-xs font-bold text-blue-600 hover:text-blue-800 transition-colors">Forgot password?</button>
300                    </div>
301                    <div class="relative">
302                        <input type="password" id="password" required placeholder="••••••••" minlength="6"
303                               class="w-full px-4 py-2.5 sm:py-3 rounded-xl border border-slate-200 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all bg-slate-50 focus:bg-white pr-12">
304                        
305                        <!-- Show/Hide Password Toggle Button -->
306                        <button type="button" onclick="togglePassword()" class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 focus:outline-none transition-colors" title="Toggle password visibility">
307                            <svg id="eyeOpen" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
307<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
308                            <svg id="eyeClosed" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path></svg>
309                        </button>
310                    </div>
311                </div>
312
313                <!-- Terms & Privacy (register mode only) — matches includes/signup-gate.php's
314                     popup, which has always required this; this standalone form never did. -->
315                <div id="termsContainer" class="hidden space-y-2">
316                    <label class="flex items-start gap-2 text-xs text-slate-600 cursor-pointer">
317                        <input type="checkbox" id="termsAccepted" class="mt-0.5 w-4 h-4 text-blue-600 bg-slate-50 border-slate-300 rounded focus:ring-blue-500 focus:ring-2 cursor-pointer shrink-0">
318                        <span>I accept the <a href="/terms.php" onclick="openLegal(event,'/terms.php','Terms');" class="text-blue-600 underline">Terms and Conditions</a></span>
319                    </label>
320                    <label class="flex items-start gap-2 text-xs text-slate-600 cursor-pointer">
321                        <input type="checkbox" id="privacyAccepted" class="mt-0.5 w-4 h-4 text-blue-600 bg-slate-50 border-slate-300 rounded focus:ring-blue-500 focus:ring-2 cursor-pointer shrink-0">
322                        <span>I accept the <a href="/privacy.php" onclick="openLegal(event,'/privacy.php','Privacy');" class="text-blue-600 underline">Privacy Policy</a></span>
323                    </label>
324                </div>
325
326                <!-- Stay Logged In Checkbox -->
327                <div id="rememberMeContainer" class="flex items-center -mt-2">
328                    <input id="rememberMe" type="checkbox" class="w-4 h-4 text-blue-600 bg-slate-50 border-slate-300 rounded focus:ring-blue-500 focus:ring-2 cursor-pointer transition-colors">
329                    <label for="rememberMe" class="ml-2 text-sm font-medium text-slate-600 select-none cursor-pointer">Stay logged in</label>
330                </div>
331
332                <!-- Google reCAPTCHA Widget -->
333                <div class="pt-1 pb-1 sm:pt-2 sm:pb-2 flex justify-center">
334                    <div class="g-recaptcha" data-sitekey="6LettZ8tAAAAAHjnfNu9S421PZVs_iQfiiDrVKFh"></div>
335                </div>
336
337                <button type="submit" id="submitBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-extrabold py-3 sm:py-3.5 rounded-xl shadow-md hover:shadow-lg transition-all">
338                    Log In
339                </button>
340
341            </form>
342
343            <!-- Mode Toggle -->
344            <div class="mt-4 sm:mt-8 text-center border-t border-slate-100 pt-4 sm:pt-6">
345                <p class="text-sm text-slate-500">
346                    <span id="toggleText">Don't have an account?</span>
347                    <button onclick="setMode(currentMode === 'login' ? 'register' : 'login')" class="font-bold text-blue-600 hover:text-blue-800 transition-colors ml-1" type="button" id="toggleBtn">
348                        Sign up for free
349                    </button>
350                </p>
351            </div>
352
353            <div class="mt-2 sm:mt-4 text-center">
354                <a href="/german-stories/index.php" class="text-xs font-bold text-slate-400 hover:text-slate-600 transition-colors">&larr; Back to Home</a>
355                <span class="text-slate-300 mx-1">&middot;</span>
356                <a href="/affiliate/become-affiliate.php" class="text-xs font-bold text-slate-400 hover:text-slate-600 transition-colors">Become an Affiliate (41%)</a>
357            </div>
358
359        </div>
360    </div>
361
362    
362<script>
363        let currentMode = 'login'; // 'login', 'register', or 'forgot'
364        let forceLoginActive = false; // State tracker for 5-device limit
365        const LOGIN_REDIRECT = ""; // '' unless a valid ?redirect= was given
366
367        // Complimentary teacher pass: a passwordless shared account. When its email
368        // is entered (in login mode) we hide the password field and let the user
369        // log in with just the email.
370        const COMP_EMAILS = ['[email protected]', '[email protected]'];
371        let compMode = false;
372
373        function checkCompEmail() {
374            const email = (document.getElementById('email').value || '').trim().toLowerCase();
375            const isComp = COMP_EMAILS.includes(email);
376            const pwWrap = document.getElementById('passwordFieldContainer');
377            const pwInput = document.getElementById('password');
378            const hint = document.getElementById('compHint');
379
380            // Reachable via ?mode=register deep links or the Sign Up toggle —
381            // auto-switch to Login so the password field actually hides.
382            // setMode() re-runs checkCompEmail() with currentMode already
383            // updated, so this doesn't recurse.
384            if (isComp && currentMode !== 'login') {
385                setMode('login');
386                return;
387            }
388
389            if (isComp && currentMode === 'login') {
390                compMode = true;
391                pwWrap.classList.add('hidden');
392                pwInput.required = false;
393                hint.classList.remove('hidden');
394            } else {
395                compMode = false;
396                hint.classList.add('hidden');
397                if (currentMode === 'login') {
398                    pwWrap.classList.remove('hidden');
399                    pwInput.required = true;
400                }
401            }
402        }
403
404        // Real secondary browser window with scrollbars — matches
405        // includes/signup-gate.php's lmSgOpenLegal() exactly.
406        function openLegal(e, url, name) {
407            e.preventDefault();
408            window.open(url, name, 'width=600,height=700,scrollbars=yes,resizable=yes');
409        }
410
411        function togglePassword() {
412            const pwdInput = document.getElementById('password');
413            const eyeOpen = document.getElementById('eyeOpen');
414            const eyeClosed = document.getElementById('eyeClosed');
415            
416            if (pwdInput.type === 'password') {
417                pwdInput.type = 'text';
418                eyeOpen.classList.add('hidden');
419                eyeClosed.classList.remove('hidden');
420            } else {
421                pwdInput.type = 'password';
422                eyeOpen.classList.remove('hidden');
423                eyeClosed.classList.add('hidden');
424            }
425        }
426
427        function setMode(mode) {
428            currentMode = mode;
429            forceLoginActive = false; // Reset the force login state if they click around
430            
431            const formTitle = document.getElementById('formTitle');
432            const formSubtitle = document.getElementById('formSubtitle');
433            const submitBtn = document.getElementById('submitBtn');
434            const toggleText = document.getElementById('toggleText');
435            const toggleBtn = document.getElementById('toggleBtn');
436            const alertBox = document.getElementById('alertBox');
437            
438            const passwordFieldContainer = document.getElementById('passwordFieldContainer');
439            const rememberMeContainer = document.getElementById('rememberMeContainer');
440            const termsContainer = document.getElementById('termsContainer');
441            const forgotLink = document.getElementById('forgotLink');
442            const passwordInput = document.getElementById('password');
443            
444            // Clear any existing errors when switching
445            alertBox.classList.add('hidden');
446
447            // Reset classes for the submit button to baseline
448            const baseBtnClasses = "w-full text-white font-extrabold py-3.5 rounded-xl shadow-md hover:shadow-lg transition-all ";
449
450            if (mode === 'register') {
451                formTitle.textContent = "Create Account";
452                formSubtitle.textContent = "Sign up free and get 24 hours of Platinum access.";
453                submitBtn.textContent = "Sign Up for 24H Free Platinum Level Access";
454                submitBtn.className = baseBtnClasses + "bg-slate-900 hover:bg-slate-800";
455                
456                toggleText.textContent = "Already have an account?";
457                toggleBtn.textContent = "Log in";
458                
459                passwordFieldContainer.classList.remove('hidden');
460                rememberMeContainer.classList.add('hidden'); // Hide on register
461                termsContainer.classList.remove('hidden');
462                forgotLink.classList.add('hidden');
463                passwordInput.required = true;
464
465            } else if (mode === 'forgot') {
466                formTitle.textContent = "Reset Password";
467                formSubtitle.textContent = "Enter your email to receive a reset link.";
468                submitBtn.textContent = "Send Reset Link";
469                submitBtn.className = baseBtnClasses + "bg-blue-600 hover:bg-blue-700";
470                
471                toggleText.textContent = "Remembered your password?";
472                toggleBtn.textContent = "Log in";
473                
474                // Hide password requirement for reset flow
475                passwordFieldContainer.classList.add('hidden');
476                rememberMeContainer.classList.add('hidden'); // Hide on forgot password
477                termsContainer.classList.add('hidden');
478                passwordInput.required = false;
479                
480            } else {
481                // Default Login Mode
482                formTitle.textContent = "Welcome Back";
483                formSubtitle.textContent = "Log in to continue your German journey.";
484                submitBtn.textContent = "Log In";
485                submitBtn.className = baseBtnClasses + "bg-blue-600 hover:bg-blue-700";
486                
487                toggleText.textContent = "Don't have an account?";
488                toggleBtn.textContent = "Sign up for free";
489                
490                passwordFieldContainer.classList.remove('hidden');
491                rememberMeContainer.classList.remove('hidden'); // Show on login
492                termsContainer.classList.add('hidden');
493                forgotLink.classList.remove('hidden');
494                passwordInput.required = true;
495            }
496            
497            // Re-sync the passwordless comp state for the new mode (e.g. leaving
498            // login mode must restore the password field).
499            checkCompEmail();
500
501            // Reset reCAPTCHA in case they clicked it before switching modes
502            if (typeof grecaptcha !== 'undefined') {
503                grecaptcha.reset();
504            }
505        }
506
507        async function handleAuth(e) {
508            e.preventDefault();
509            const btn = document.getElementById('submitBtn');
510            const alertBox = document.getElementById('alertBox');
511            
512            // Grab the reCAPTCHA token generated by Google
513            const recaptchaResponse = grecaptcha.getResponse();
514
515            if (!recaptchaResponse) {
516                showAlert('Please complete the "I am not a robot" check.', 'error');
517                return;
518            }
519
520            // Matches includes/signup-gate.php's popup, which has always
521            // required this; this standalone form never checked it.
522            if (currentMode === 'register') {
523                const termsOk = document.getElementById('termsAccepted').checked;
524                const privacyOk = document.getElementById('privacyAccepted').checked;
525                if (!termsOk || !privacyOk) {
526                    showAlert('Please accept the Terms and Privacy Policy to continue.', 'error');
527                    return;
528                }
529            }
530
531            const data = {
532                email: document.getElementById('email').value,
533                recaptcha_token: recaptchaResponse
534            };
535
536            // Only grab the password if we aren't submitting a forgot password request
537            if (currentMode !== 'forgot') {
538                data.password = document.getElementById('password').value;
539            }
540
541            // Handle specific Login variables (like Force Login overrides)
542            if (currentMode === 'login') {
543                data.stay_logged_in = document.getElementById('rememberMe').checked;
544                data.force_login = forceLoginActive;
545            }
546
547            if (currentMode === 'register') {
548                data.terms_accepted = document.getElementById('termsAccepted').checked;
549                data.privacy_accepted = document.getElementById('privacyAccepted').checked;
550            }
551
552            // Loading state
553            const originalText = btn.textContent;
554            btn.disabled = true;
555            btn.textContent = 'Please wait...';
556            btn.classList.add('opacity-70', 'cursor-not-allowed');
557            alertBox.classList.add('hidden');
558
559            try {
560                const res = await fetch(`auth.php?action=${currentMode}`, {
561                    method: 'POST',
562                    headers: { 'Content-Type': 'application/json' },
563                    body: JSON.stringify(data)
564                });
565                
566                const result = await res.json();
567
568                if (result.success) {
569                    if (currentMode === 'forgot') {
570                        showAlert(result.message || "If an account exists, a reset link has been sent.", 'success');
571                        grecaptcha.reset();
572                    } else if (currentMode === 'register') {
573                        // New free account → free thank-you page (fires the sign_up
574                        // conversion), which then forwards to the membership page.
575                        window.location.href = '/free-thankyou.php';
576                    } else if (result.complimentary) {
577                        // Complimentary teacher pass → 3-second welcome screen → dashboard.
578                        window.location.href = '/membership/complimentary-welcome.php';
579                    } else if (LOGIN_REDIRECT) {
580                        window.location.href = LOGIN_REDIRECT;
581                    } else {
582                        const tier = (result.tier || 'free').toLowerCase();
583                        window.location.href = tier === 'platinum' ? '/membership/dashboard.php' : '/membership/index.php';
584                    }
585                } else {
586                    // Check if they hit the 5 device limit
587                    if (result.error_code === 'DEVICE_LIMIT_REACHED') {
588                        forceLoginActive = true;
589                        
590                        // Transform the submit button
591                        btn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
592                        btn.classList.add('bg-red-600', 'hover:bg-red-700');
593                        
594                        // Show warning prompt
595                        alertBox.innerHTML = `<strong>5 Device Limit Reached!</strong><br>Check the CAPTCHA box again and click the red button below to securely sign out of all other devices.`;
596                        alertBox.className = `mb-6 p-4 rounded-xl text-sm text-center border block bg-amber-50 text-amber-800 border-amber-200`;
597                        
598                        grecaptcha.reset();
599                    } else if (result.error_code === 'COMP_EXPIRED') {
600                        // Hard stop passed — tell them and send them to the signup page.
601                        showAlert('Sorry, your 7 day complimentary pass has expired. You will now be directed to the signup page.', 'error');
602                        grecaptcha.reset();
603                        setTimeout(function () { window.location.href = '/auth/login.php?mode=register&msg=comp_expired'; }, 3000);
604                    } else {
605                        showAlert(result.message, 'error');
606                        grecaptcha.reset();
607                    }
608                }
609            } catch (err) {
610                showAlert('A network error occurred. Please try again.', 'error');
611                grecaptcha.reset();
612            }
613
614            // Restore button
615            btn.disabled = false;
616            btn.classList.remove('opacity-70', 'cursor-not-allowed');
617            
618            // If they are in Force Login mode, keep the button text accurately updated
619            if (forceLoginActive && currentMode === 'login') {
620                btn.textContent = 'Sign Out Others & Log In';
621            } else {
622                btn.textContent = originalText;
623            }
624        }
625
626        function showAlert(message, type) {
627            const alertBox = document.getElementById('alertBox');
628            alertBox.textContent = message; // Overwrites any innerHTML (like the device limit prompt)
629            alertBox.className = `mb-6 p-4 rounded-xl text-sm font-bold text-center border block`;
630            
631            if (type === 'error') {
632                alertBox.classList.add('bg-red-50', 'text-red-700', 'border-red-200');
633            } else {
634                alertBox.classList.add('bg-green-50', 'text-green-700', 'border-green-200');
635            }
636        }
637
638        // Open in the requested starting mode (e.g. arriving as the "signup page"
639        // after a complimentary pass expires), then sync the passwordless state.
640        (function () {
641            var startMode = "login";
642            if (startMode === 'register' || startMode === 'forgot') { setMode(startMode); }
643            else { checkCompEmail(); }
644        })();
645    </script>
645
646</body>
647</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.