PageSourceSearch

https://smartreality.com/js/marketplace-listing.js?v=1791462458

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

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.