PageSourceSearch

https://bermudaair.com/js/marketplace-listing.js?v=1790508061

js bermudaair.com collected 2026-09-27 11:21:05 UTC 29,394 bytes, 650 lines download raw bytes

1(function() {
2    'use strict';
3
4    // Theme toggle
5    var themeToggle = document.getElementById('themeToggle');
6    var iconLight = document.getElementById('themeIconLight');
7    var iconDark = document.getElementById('themeIconDark');
8    var html = document.documentElement;
9
10    function getEffectiveTheme() {
11        var theme = html.getAttribute('data-theme');
12        if (theme === 'dark') return 'dark';
13        if (theme === 'light') return 'light';
14        // auto — check system preference
15        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
16    }
17
18    function updateThemeIcons() {
19        var effective = getEffectiveTheme();
20        // Show sun icon when dark (click to go light), moon when light (click to go dark)
21        iconLight.style.display = effective === 'dark' ? 'inline' : 'none';
22        iconDark.style.display = effective === 'light' ? 'inline' : 'none';
23    }
24
25    // Restore saved preference
26    var saved = localStorage.getItem('mkt_theme');
27    if (saved) {
28        html.setAttribute('data-theme', saved);
29    }
30    updateThemeIcons();
31
32    if (themeToggle) {
33        themeToggle.addEventListener('click', function() {
34            var effective = getEffectiveTheme();
35            var next = effective === 'dark' ? 'light' : 'dark';
36            html.setAttribute('data-theme', next);
37            localStorage.setItem('mkt_theme', next);
38            updateThemeIcons();
39        });
40    }
41
42    // Listen for system theme changes when in auto mode
43    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
44        if (html.getAttribute('data-theme') === 'auto') {
45            updateThemeIcons();
46        }
47    });
48
49    // Favorite / Save toggle — persists to the server (no login required; anon
50    // saves are attributed to a first-party cookie the endpoint sets).
51    var favBtn = document.getElementById('favoriteBtn');
52    if (favBtn) {
53        var favListingId = parseInt(favBtn.getAttribute('data-listing-id'), 10) || 0;
54
55        var paintFav = function(saved) {
56            favBtn.innerHTML = '<i class="fa' + (saved ? 's' : 'r') + ' fa-heart"></i> ' + (saved ? 'Saved' : 'Save');
57        };
58
59        favBtn.addEventListener('click', function() {
60            if (!favListingId || favBtn.dataset.busy === '1') return;
61            favBtn.dataset.busy = '1';
62            var wasSaved = favBtn.querySelector('i').classList.contains('fas');
63            paintFav(!wasSaved); // optimistic
64
65            fetch('/api/marketplace/favorite', {
66                method: 'POST',
67                headers: { 'Content-Type': 'application/json' },
68                credentials: 'same-origin',
69                body: JSON.stringify({ listing_id: favListingId })
70            })
71            .then(function(r) { return r.json(); })
72            .then(function(d) {
73                if (!d || !d.success) { paintFav(wasSaved); return; } // revert on failure
74                paintFav(d.saved);
75            })
76            .catch(function() { paintFav(wasSaved); })
77            .finally(function() { favBtn.dataset.busy = '0'; });
78        });
79    }
80
81    // Alert subscription
82    var alertBtn = document.getElementById('alertBtn');
83    if (alertBtn) {
84        alertBtn.addEventListener('click', function() {
85            // TODO: POST to /api/marketplace/price_alert.php
86            alert('Price alert subscription coming soon.');
87        });
88    }
89
90    // Buy Now — starts Stripe Checkout. Guest-friendly: no login required.
91    // If the buyer is on a parked seller domain (non-canonical host), that
92    // host doesn't serve the /api endpoints, so we redirect to the canonical
93    // listing URL with autobuy=1 and auto-fire the flow once it loads.
94    function startBuyNowCheckout(triggerBtn, channel) {
95        if (!priceCard) return;
96
97        if (priceCard.dataset.buyNowEnabled !== '1') {
98            alert('Buy Now is not available — the seller has not completed payout setup.');
99            return;
100        }
101
102        var canonicalUrl = priceCard.dataset.canonicalUrl;
103        try {
104            if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) {
105                window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autobuy=1';
106                return;
107            }
108        } catch (e) { /* ignore parse errors */ }
109
110        var listingId = parseInt(priceCard.dataset.listingId, 10);
111        if (!listingId) return;
112
113        var origLabel = triggerBtn ? triggerBtn.innerHTML : '';
114        if (triggerBtn) {
115            triggerBtn.disabled = true;
116            triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting...';
117        }
118
119        fetch('/api/marketplace/checkout', {
120            method: 'POST',
121            headers: { 'Content-Type': 'application/json' },
122            body: JSON.stringify({ listing_id: listingId, channel: channel || 'card' })
123        }).then(function(r) {
124            return r.json().then(function(data) { return { ok: r.ok, data: data }; });
125        }).then(function(res) {
126            if (res.ok && res.data.success && res.data.checkout_url) {
127                window.location = res.data.checkout_url;
128                return;
129            }
130            alert(res.data && res.data.error ? res.data.error : 'Could not start checkout. Please try again.');
131            if (triggerBtn) {
132                triggerBtn.disabled = false;
133                triggerBtn.innerHTML = origLabel;
134            }
135        }).catch(function() {
136            alert('Network error. Please try again.');
137            if (triggerBtn) {
138                triggerBtn.disabled = false;
139                triggerBtn.innerHTML = origLabel;
140            }
141        });
142    }
143
144    var buyBtn = document.getElementById('buyNowBtn');
145    if (buyBtn && !buyBtn.disabled) {
146        buyBtn.addEventListener('click', function() { startBuyNowCheckout(buyBtn, 'card'); });
147    }
148
149    // Alipay (zh pages): familiar rail for Chinese buyers
150    var alipayBtn = document.getElementById('payAlipayBtn');
151    if (alipayBtn && !alipayBtn.disabled) {
152        alipayBtn.addEventListener('click', function() { startBuyNowCheckout(alipayBtn, 'alipay'); });
153    }
154
155    // Crypto/stablecoins: instant, no surcharge
156    var cryptoBtn = document.getElementById('payCryptoBtn');
157    if (cryptoBtn && !cryptoBtn.disabled) {
158        cryptoBtn.addEventListener('click', function() { startBuyNowCheckout(cryptoBtn, 'crypto'); });
159    }
160
161    // Bitcoin (BTCPay on our side): our own invoice page. Login required - a verified email is the only
162    // identity we ask of a Bitcoin buyer - so a signed-out click goes to login and back to this listing.
163    function startBtcCheckout(triggerBtn) {
164        if (!priceCard) return;
165        var canonicalUrl = priceCard.dataset.canonicalUrl;
166        try {
167            if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) {
168                window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autobtc=1';
169                return;
170            }
171        } catch (e) { /* ignore */ }
172        var listingId = parseInt(priceCard.dataset.listingId, 10);
173        if (!listingId) return;
174        var origLabel = triggerBtn ? triggerBtn.innerHTML : '';
175        if (triggerBtn) { triggerBtn.disabled = true; triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating invoice...'; }
176        fetch('/api/marketplace/btc-checkout', {
177            method: 'POST',
178            headers: { 'Content-Type': 'application/json' },
179            body: JSON.stringify({ listing_id: listingId })
180        }).then(function(r) {
181            return r.json().then(function(data) { return { ok: r.ok, data: data }; });
182        }).then(function(res) {
183            if (res.ok && res.data.success && res.data.pay_url) { window.location = res.data.pay_url; return; }
184            if (res.data && res.data.login_required) {
185                window.location = 'https://appraise.net/login?redirect=' + encodeURIComponent(window.location.pathname + '?autobtc=1');
186                return;
187            }
188            alert(res.data && res.data.error ? res.data.error : 'Could not start Bitcoin checkout. Please try again.');
189            if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; }
190        }).catch(function() {
191            alert('Network error. Please try again.');
192            if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; }
193        });
194    }
195    // PayPal (Orders v2): our endpoint creates the order and we send the buyer to PayPal's approval page.
196    function startPaypalCheckout(triggerBtn) {
197        if (!priceCard) return;
198        var canonicalUrl = priceCard.dataset.canonicalUrl;
199        try {
200            if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) {
201                window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autopaypal=1';
202                return;
203            }
204        } catch (e) { /* ignore */ }
205        var listingId = parseInt(priceCard.dataset.listingId, 10);
206        if (!listingId) return;
207        var origLabel = triggerBtn ? triggerBtn.innerHTML : '';
208        if (triggerBtn) { triggerBtn.disabled = true; triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting to PayPal...'; }
209        fetch('/api/marketplace/paypal-checkout', {
210            method: 'POST',
211            headers: { 'Content-Type': 'application/json' },
212            body: JSON.stringify({ listing_id: listingId })
213        }).then(function(r) {
214            return r.json().then(function(data) { return { ok: r.ok, data: data }; });
215        }).then(function(res) {
216            if (res.ok && res.data.success && res.data.approve_url) { window.location = res.data.approve_url; return; }
217            if (res.data && res.data.login_required) {
218                window.location = 'https://appraise.net/login?redirect=' + encodeURIComponent(window.location.pathname + '?autopaypal=1');
219                return;
220            }
221            alert(res.data && res.data.error ? res.data.error : 'Could not start PayPal checkout. Please try again.');
222            if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; }
223        }).catch(function() {
224            alert('Network error. Please try again.');
225            if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; }
226        });
227    }
228    var paypalBtn = document.getElementById('payPaypalBtn');
229    if (paypalBtn && !paypalBtn.disabled) {
230        paypalBtn.addEventListener('click', function() { startPaypalCheckout(paypalBtn); });
231        if (/[?&]autopaypal=1/.test(window.location.search)) { startPaypalCheckout(paypalBtn); }
232    }
233
234    var btcBtn = document.getElementById('payBtcBtn');
235    if (btcBtn && !btcBtn.disabled) {
236        btcBtn.addEventListener('click', function() { startBtcCheckout(btcBtn); });
237        if (/[?&]autobtc=1/.test(window.location.search)) { startBtcCheckout(btcBtn); }
238    }
239
240    // US bank debit (ACH): no card surcharge; settles in a few business days
241    var bankBtn = document.getElementById('payBankBtn');
242    if (bankBtn && !bankBtn.disabled) {
243        bankBtn.addEventListener('click', function() { startBuyNowCheckout(bankBtn, 'bank'); });
244    }
245
246    // ---- Buy via Escrow.com ----
247    // Delegates to the shared modal in /js/marketplace-escrow-modal.js, which
248    // collects the buyer's Escrow.com email (with pre-fill / localStorage),
249    // then POSTs to escrow-checkout.php and redirects to the agreement URL.
250    function startEscrowCheckout(triggerBtn) {
251        if (!priceCard) return;
252
253        // Non-canonical host redirect — same pattern as Stripe Buy Now.
254        var canonicalUrl = priceCard.dataset.canonicalUrl;
255        try {
256            if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) {
257                window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autoescrow=1';
258                return;
259            }
260        } catch (e) { /* ignore */ }
261
262        var listingId = parseInt(priceCard.dataset.listingId, 10);
263        if (!listingId) return;
264
265        if (typeof window.openEscrowModal === 'function') {
266            window.openEscrowModal({
267                mode: 'listing',
268                listingId: listingId,
269                triggerBtn: triggerBtn
270            });
271        } else {
272            // Defensive fallback if the modal module wasn't loaded for some
273            // reason — should never hit in production.
274            alert('Escrow.com checkout is temporarily unavailable. Please try again or use card payment.');
275        }
276    }
277
278    var escrowBtn = document.getElementById('buyEscrowBtn');
279    if (escrowBtn && !escrowBtn.disabled) {
280        escrowBtn.addEventListener('click', function() { startEscrowCheckout(escrowBtn); });
281    }
282
283    // ---- LTO Start Lease (mirrors Buy Now but hits lto-checkout.php) ----
284    // The lease is a contract: the buyer confirms they have read the lease-to-own terms in a modal first, and
285    // lto-checkout.php refuses to create the transaction without terms_accepted.
286    var ltoTermsModal = document.getElementById('ltoTermsModal');
287    var ltoTermsAck = document.getElementById('ltoTermsAck');
288    var ltoTermsContinue = document.getElementById('ltoTermsContinue');
289    var ltoPendingBtn = null;
290    function openLtoTerms(triggerBtn) {
291        if (!ltoTermsModal) { startLtoCheckout(triggerBtn, true); return; }
292        ltoPendingBtn = triggerBtn;
293        if (ltoTermsAck) { ltoTermsAck.checked = false; }
294        if (ltoTermsContinue) { ltoTermsContinue.disabled = true; }
295        ltoTermsModal.hidden = false;
296    }
297    if (ltoTermsModal) {
298        ltoTermsModal.addEventListener('click', function(e) { if (e.target.closest('[data-lto-close]')) ltoTermsModal.hidden = true; });
299        if (ltoTermsAck && ltoTermsContinue) { ltoTermsAck.addEventListener('change', function() { ltoTermsContinue.disabled = !ltoTermsAck.checked; }); }
300        if (ltoTermsContinue) { ltoTermsContinue.addEventListener('click', function() {
301            if (!ltoTermsAck || !ltoTermsAck.checked) return;
302            ltoTermsModal.hidden = true;
303            startLtoCheckout(ltoPendingBtn, true);
304        }); }
305    }
306    function startLtoCheckout(triggerBtn, termsAccepted) {
307        if (!priceCard) return;
308
309        var canonicalUrl = priceCard.dataset.canonicalUrl;
310        try {
311            if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) {
312                window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autolto=1';
313                return;
314            }
315        } catch (e) { /* ignore */ }
316
317        var listingId = parseInt(priceCard.dataset.listingId, 10);
318        if (!listingId) return;
319
320        var origLabel = triggerBtn ? triggerBtn.innerHTML : '';
321        if (triggerBtn) {
322            triggerBtn.disabled = true;
323            triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting...';
324        }
325
326        fetch('/api/marketplace/lto-checkout', {
327            method: 'POST',
328            headers: { 'Content-Type': 'application/json' },
329            body: JSON.stringify({ listing_id: listingId, terms_accepted: termsAccepted ? 1 : 0 })
330        }).then(function(r) {
331            return r.json().then(function(data) { return { ok: r.ok, data: data }; });
332        }).then(function(res) {
333            if (res.ok && res.data.success && res.data.checkout_url) {
334                window.location = res.data.checkout_url;
335                return;
336            }
337            alert(res.data && res.data.error ? res.data.error : 'Could not start lease. Please try again.');
338            if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; }
339        }).catch(function() {
340            alert('Network error. Please try again.');
341            if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; }
342        });
343    }
344
345    var ltoBtn = document.getElementById('ltoStartBtn');
346    if (ltoBtn && !ltoBtn.disabled) {
347        ltoBtn.addEventListener('click', function() { openLtoTerms(ltoBtn); });
348    }
349
350    // Auto-fire flows when landed with ?autobuy=1 / ?autolto=1 (from a parked-domain handoff).
351    function stripParam(name) {
352        try {
353            var pattern = new RegExp('([?&])' + name + '=1&?');
354            var cleanUrl = window.location.pathname + window.location.search.replace(pattern, '$1').replace(/[?&]$/, '') + window.location.hash;
355            window.history.replaceState({}, '', cleanUrl);
356        } catch (e) { /* ignore */ }
357    }
358    if (buyBtn && !buyBtn.disabled && /[?&]autobuy=1\b/.test(window.location.search)) {
359        stripParam('autobuy');
360        startBuyNowCheckout(buyBtn);
361    }
362    if (ltoBtn && !ltoBtn.disabled && /[?&]autolto=1\b/.test(window.location.search)) {
363        stripParam('autolto');
364        startLtoCheckout(ltoBtn);
365    }
366
367    // ---- Make Offer / Inquiry modal ----
368    var priceCard = document.querySelector('[data-card="price"]');
369    var offerModal = document.getElementById('offerModal');
370
371    function openOfferModal(mode) {
372        if (!priceCard || !offerModal) return;
373
374        // If served on a non-canonical host (seller's parked domain via catchall),
375        // show a handoff interstitial before bouncing to appraise.net so the
376        // origin change isn't a surprise to the buyer.
377        var canonicalUrl = priceCard.dataset.canonicalUrl;
378        if (canonicalUrl) {
379            try {
380                var canonicalHost = new URL(canonicalUrl).host;
381                if (canonicalHost && canonicalHost !== window.location.host) {
382                    openHandoffModal(canonicalUrl, mode);
383                    return;
384                }
385            } catch (e) { /* ignore parse errors */ }
386        }
387
388        if (priceCard.dataset.isLoggedIn !== '1') {
389            var returnTo = (canonicalUrl || window.location.href);
390            window.location = 'https://appraise.net/login?redirect=' + encodeURIComponent(returnTo);
391            return;
392        }
393
394        var isInquiry = mode === 'inquiry';
395        var domain = priceCard.dataset.domain || 'this domain';
396        var amountField = document.getElementById('offerAmountField');
397        var msgInput = document.getElementById('offerMessageInput');
398        var amountInput = document.getElementById('offerAmountInput');
399
400        document.getElementById('offerModalTitle').textContent = isInquiry ? 'Inquire About Price' : 'Make an Offer';
401        document.getElementById('offerModalSub').textContent = isInquiry
402            ? 'Send a question to the seller of ' + domain + '.'
403            : 'Submit an offer for ' + domain + '. You choose how long it stays open.';
404        document.getElementById('offerSubmitLabel').textContent = isInquiry ? 'Send Inquiry' : 'Send Offer';
405
406        amountField.hidden = isInquiry;
407        var expiryField = document.getElementById('offerExpiryField');
408        if (expiryField) expiryField.hidden = isInquiry;
409        amountInput.value = '';
410        msgInput.value = '';
411        if (isInquiry) {
412            msgInput.placeholder = 'What would you like to ask the seller?';
413        } else {
414            msgInput.placeholder = 'Add a note for the seller...';
415        }
416
417        var err = document.getElementById('offerModalError');
418        err.hidden = true; err.textContent = '';
419
420        offerModal.hidden = false;
421        offerModal.dataset.mode = mode;
422        setTimeout(function() {
423            (isInquiry ? msgInput : amountInput).focus();
424        }, 50);
425    }
426
427    function closeOfferModal() {
428        if (offerModal) offerModal.hidden = true;
429    }
430
431    function submitOffer() {
432        if (!offerModal || !priceCard) return;
433        var mode = offerModal.dataset.mode;
434        var isInquiry = mode === 'inquiry';
435        var listingId = parseInt(priceCard.dataset.listingId, 10);
436        var amountInput = document.getElementById('offerAmountInput');
437        var msgInput = document.getElementById('offerMessageInput');
438        var submitBtn = document.getElementById('offerSubmitBtn');
439        var err = document.getElementById('offerModalError');
440        err.hidden = true; err.textContent = '';
441
442        var amount = isInquiry ? 0 : parseFloat(amountInput.value);
443        var message = msgInput.value.trim();
444
445        if (!isInquiry && (!amount || amount <= 0)) {
446            err.textContent = 'Enter a valid offer amount.';
447            err.hidden = false;
448            return;
449        }
450        var minOffer = parseFloat(priceCard.dataset.minOfferPrice) || 0;
451        if (!isInquiry && minOffer > 0 && amount < minOffer) {
452            err.textContent = 'Minimum offer is $' + minOffer.toLocaleString();
453            err.hidden = false;
454            return;
455        }
456        if (isInquiry && !message) {
457            err.textContent = 'Please type your question for the seller.';
458            err.hidden = false;
459            return;
460        }
461
462        submitBtn.disabled = true;
463        var origLabel = document.getElementById('offerSubmitLabel').textContent;
464        document.getElementById('offerSubmitLabel').textContent = 'Sending...';
465
466        fetch('/api/marketplace/deal', {
467            method: 'POST',
468            headers: { 'Content-Type': 'application/json' },
469            body: JSON.stringify({
470                action: 'create_offer',
471                listing_id: listingId,
472                amount: amount,
473                message: message,
474                inquiry: isInquiry,
475                expiry_days: parseInt((document.getElementById('offerExpiryInput') || {}).value, 10) || 14
476            })
477        }).then(function(r) { return r.json(); }).then(function(data) {
478            if (data.success && data.thread_token) {
479                window.location = '/marketplace/deal/' + data.thread_token;
480            } else {
481                err.textContent = data.error || 'Could not submit. Please try again.';
482                err.hidden = false;
483                submitBtn.disabled = false;
484                document.getElementById('offerSubmitLabel').textContent = origLabel;
485            }
486        }).catch(function() {
487            err.textContent = 'Network error. Please try again.';
488            err.hidden = false;
489            submitBtn.disabled = false;
490            document.getElementById('offerSubmitLabel').textContent = origLabel;
491        });
492    }
493
494    // Delegated handlers — survive artist-tool rebuilds of priceCardContent
495    document.addEventListener('click', function(e) {
496        if (!e.target.closest) return;
497        if (e.target.closest('#makeOfferBtn')) { openOfferModal('offer'); return; }
498        if (e.target.closest('#inquireBtn')) { openOfferModal('inquiry'); return; }
499        if (e.target.closest('.mobile-cta .btn-cta') && priceCard) {
500            var lt = priceCard.dataset.listingType;
501            // buy_now_or_offer's mobile CTA shows "Buy Now" as the primary action,
502            // so route it through the BIN flow just like a buy_now listing.
503            if (lt === 'buy_now' || lt === 'buy_now_or_offer') {
504                var bn = document.getElementById('buyNowBtn');
505                if (bn && !bn.disabled) { startBuyNowCheckout(bn); }
506                else if (lt === 'buy_now_or_offer') { openOfferModal('offer'); }
507                return;
508            }
509            if (lt === 'make_offer') { openOfferModal('offer'); return; }
510            if (lt === 'inquiry') { openOfferModal('inquiry'); return; }
511            if (lt === 'lease_to_own') { var lto = document.getElementById('ltoStartBtn');
511 if (lto) lto.click(); }
512        }
513    });
514
515    if (offerModal) {
516        offerModal.querySelectorAll('[data-offer-close]').forEach(function(el) {
517            el.addEventListener('click', closeOfferModal);
518        });
519        document.getElementById('offerSubmitBtn').addEventListener('click', submitOffer);
520        document.addEventListener('keydown', function(e) {
521            if (e.key === 'Escape' && !offerModal.hidden) closeOfferModal();
522        });
523    }
524
525    // Handoff modal (non-canonical hosts)
526    var handoffModal = document.getElementById('handoffModal');
527
528    function openHandoffModal(canonicalUrl, mode) {
529        if (!handoffModal) { window.location = canonicalUrl; return; }
530        var isInquiry = mode === 'inquiry';
531        document.getElementById('handoffTitle').textContent = isInquiry ? 'Continue on Appraise.net' : 'Continue on Appraise.net';
532        document.getElementById('handoffSub').innerHTML = isInquiry
533            ? "Inquiries are handled on <strong>Appraise.net Marketplace</strong>, where you can message the seller securely. You'll be taken there now and can return any time."
534            : "Offers are negotiated and paid securely on <strong>Appraise.net Marketplace</strong>. You'll be taken there to complete your offer — and you can return here any time.";
535        document.getElementById('handoffContinueBtn').setAttribute('href', canonicalUrl);
536        handoffModal.hidden = false;
537    }
538
539    function closeHandoffModal() {
540        if (handoffModal) handoffModal.hidden = true;
541    }
542
543    if (handoffModal) {
544        handoffModal.querySelectorAll('[data-handoff-close]').forEach(function(el) {
545            el.addEventListener('click', closeHandoffModal);
546        });
547        document.addEventListener('keydown', function(e) {
548            if (e.key === 'Escape' && !handoffModal.hidden) closeHandoffModal();
549        });
550    }
551
552    // Seller: Toggle publish
553    var publishBtn = document.getElementById('togglePublishBtn');
554    if (publishBtn) {
555        publishBtn.addEventListener('click', function() {
556            // TODO: POST to toggle status
557            alert('Publish toggle coming soon.');
558        });
559    }
560
561    // Seller: Delete
562    var deleteBtn = document.getElementById('deleteListingBtn');
563    if (deleteBtn) {
564        deleteBtn.addEventListener('click', function() {
565            if (confirm('Are you sure you want to delete this listing? This cannot be undone.')) {
566                // TODO: POST to delete
567                alert('Delete coming soon.');
568            }
569        });
570    }
571
572    // Left sidebar visibility on 1280px+
573    var leftSidebar = document.querySelector('.sticky-sidebar');
574    if (leftSidebar && window.innerWidth >= 1280) {
575        leftSidebar.style.display = 'block';
576    }
577    window.addEventListener('resize', function() {
578        if (leftSidebar && window.innerWidth >= 1280) {
579            leftSidebar.style.display = 'block';
580        }
581    });
582    // Certificate modal
583    var certModal = document.getElementById('certModal');
584    if (certModal) {
585        var certIframe = document.getElementById('certIframe');
586        var certBackdrop = certModal.querySelector('.cert-modal-backdrop');
587        var certClose = certModal.querySelector('.cert-modal-close');
588
589        var certLoading = certModal.querySelector('.cert-modal-loading');
590
591        document.querySelectorAll('.cert-link').forEach(function(link) {
592            link.addEventListener('click', function(e) {
593                e.preventDefault();
594                var certId = this.dataset.certId;
595                if (certId) {
596                    if (certLoading) certLoading.style.display = 'flex';
597                    certIframe.style.opacity = '0';
598                    certIframe.src = 'https://appraise.net/certificate?id=' + encodeURIComponent(certId);
599                    certModal.classList.add('open');
600                }
601            });
602        });
603
604        certIframe.addEventListener('load', function() {
605            if (certLoading) certLoading.style.display = 'none';
606            certIframe.style.opacity = '1';
607        });
608
609        function closeCertModal() {
610            certModal.classList.remove('open');
611            certIframe.src = '';
612            certIframe.style.opacity = '0';
613        }
614
615        certBackdrop.addEventListener('click', closeCertModal);
616        certClose.addEventListener('click', closeCertModal);
617        document.addEventListener('keydown', function(e) {
618            if (e.key === 'Escape' && certModal.classList.contains('open')) closeCertModal();
619        });
620    }
621})();
622
623/* ---- Nav brand-logo lightbox: the badge shows a cropped strip of the logo; click it to
624        see the full logo in an overlay (X, backdrop, the logo itself, or Esc all close). ---- */
625(function() {
626    var badge = document.querySelector('.nav-brand-logo.logo-zoomable');
627    var box = document.getElementById('logo-lightbox');
628    if (!badge || !box) return;
629    var img = box.querySelector('.logo-lightbox-img');
630
631    function openLogo() {
632        img.src = badge.getAttribute('data-logo-src') || '';
633        img.alt = badge.getAttribute('data-logo-alt') || '';
634        box.hidden = false;
635        document.addEventListener('keydown', onKey);
636    }
637    function closeLogo() {
638        box.hidden = true;
639        img.removeAttribute('src');
640        document.removeEventListener('keydown', onKey);
641    }
642    function onKey(e) { if (e.key === 'Escape') closeLogo(); }
643
644    badge.addEventListener('click', function(e) { e.preventDefault(); openLogo(); });
645    badge.addEventListener('keydown', function(e) {
646        if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openLogo(); }
647    });
648    // Any click on the overlay (backdrop, close button, or the enlarged logo) dismisses it.
649    box.addEventListener('click', closeLogo);
650})();

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.