PageSourceSearch

https://www.ua-coins.info/js/script.js?v=2.3.85

js ua-coins.info collected 2026-09-25 09:05:13 UTC 181,807 bytes, 4,323 lines download raw bytes

1$(document).ready(function() {
2    window.AppLocale = $("body").data("current-locale") || "ru";
3    $("img[data-fallback-src]").each(function() {
4        var img = this;
5        var fallbackSrc = img.getAttribute("data-fallback-src");
6        if (!fallbackSrc) {
7            return;
8        }
9        img.addEventListener("error", function() {
10            if (img.src.indexOf(fallbackSrc) !== -1) {
11                return;
12            }
13            img.src = fallbackSrc;
14        });
15        if (img.complete && img.naturalWidth === 0) {
16            img.src = fallbackSrc;
17        }
18    });
19    try {
20        if (window.Intl && Intl.DateTimeFormat) {
21            var tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
22            if (tz && typeof tz === "string") {
23                document.cookie = "time_zone=" + encodeURIComponent(tz) + "; path=/; max-age=31536000; samesite=lax";
24            }
25        }
26    } catch (e) {}
27    $(document).on("click", ".js-locale-switch-link", function() {
28        var locale = ($(this).data("locale") || "").toString().toLowerCase();
29        if (!locale || [ "ru", "ua", "en" ].indexOf(locale) === -1) {
30            return;
31        }
32        var endpoint = "/locale/preference/" + encodeURIComponent(locale);
33        try {
34            if (navigator.sendBeacon) {
35                var blob = new Blob([ "1" ], {
36                    type: "text/plain;charset=UTF-8"
37                });
38                navigator.sendBeacon(endpoint, blob);
39                return;
40            }
41        } catch (e) {}
42        try {
43            fetch(endpoint, {
44                method: "POST",
45                credentials: "same-origin",
46                keepalive: true
47            });
48        } catch (e) {}
49    });
50    if ($(window).width() > 992) {
51        (function() {
52            var a = document.querySelector("#fixblock"), b = null, K = null, Z = 0, P = 0, N = 20;
53            if (a != null) {
54                window.addEventListener("scroll", Ascroll, false);
55                document.body.addEventListener("scroll", Ascroll, false);
56            }
57            function Ascroll() {
58                var Ra = a.getBoundingClientRect(), R1bottom = document.querySelector("#main-content-block").getBoundingClientRect().bottom - 40;
59                if (Ra.bottom < R1bottom) {
60                    if (b == null) {
61                        var Sa = getComputedStyle(a, ""), s = "";
62                        for (var i = 0; i < Sa.length; i++) {
63                            if (Sa[i].indexOf("overflow") == 0 || Sa[i].indexOf("padding") == 0 || Sa[i].indexOf("border") == 0 || Sa[i].indexOf("outline") == 0 || Sa[i].indexOf("box-shadow") == 0 || Sa[i].indexOf("background") == 0) {
64                                s += Sa[i] + ": " + Sa.getPropertyValue(Sa[i]) + "; ";
65                            }
66                        }
67                        b = document.createElement("div");
68                        b.className = "stop";
69                        b.style.cssText = s + " box-sizing: border-box; width: " + a.offsetWidth + "px;";
70                        a.insertBefore(b, a.firstChild);
71                        var l = a.childNodes.length;
72                        for (var i = 1; i < l; i++) {
73                            b.appendChild(a.childNodes[1]);
74                        }
75                        a.style.height = b.getBoundingClientRect().height + "px";
76                        a.style.padding = "0";
77                        a.style.border = "0";
78                    }
79                    var Rb = b.getBoundingClientRect(), Rh = Ra.top + Rb.height, W = window.innerHeight, R1 = Math.round(Rh - R1bottom), R2 = Math.round(Rh - W);
80                    if (Rb.height > W) {
81                        if (Ra.top < K) {
82                            if (R2 + N > R1) {
83                                if (Rb.bottom - W + N - 20 <= 0) {
84                                    b.className = "stickyy";
85                                    b.style.top = W - Rb.height - N + "px";
86                                    Z = N + Ra.top + Rb.height - W;
87                                } else {
88                                    b.className = "stop";
89                                    b.style.top = -Z + "px";
90                                }
91                            } else {
92                                b.className = "stop";
93                                b.style.top = -R1 + "px";
94                                Z = R1;
95                            }
96                        } else {
97                            if (Ra.top - P < 0) {
98                                if (Rb.top - P >= 0) {
99                                    b.className = "stickyy";
100                                    b.style.top = P + "px";
101                                    Z = Ra.top - P;
102                                } else {
103                                    b.className = "stop";
104                                    b.style.top = -Z + "px";
105                                }
106                            } else {
107                                b.className = "";
108                                b.style.top = "";
109                                Z = 0;
110                            }
111                        }
112                        K = Ra.top;
113                    } else {
114                        if (Ra.top - P <= 0) {
115                            if (Ra.top - P <= R1) {
116                                b.className = "stop";
117                                b.style.top = -R1 + "px";
118                            } else {
119                                b.className = "stickyy";
120                                b.style.top = P + "px";
121                            }
122                        } else {
123                            b.className = "";
124                            b.style.top = "";
125                        }
126                    }
127                    window.addEventListener("resize", function() {
128                        a.children[0].style.width = getComputedStyle(a, "").width;
129                    }, false);
130                }
131            }
132        })();
133    }
134    window.AppUi = window.AppUi || {};
135    window.AppUi.initTooltips = function(root) {
136        if (!window.bootstrap || !window.bootstrap.Tooltip) {
137            return;
138        }
139        var scope = root && root.querySelectorAll ? root : document;
140        scope.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) {
141            window.bootstrap.Tooltip.getOrCreateInstance(el);
142        });
143    };
144    window.AppUi.showModal = function(modal) {
145        var el = typeof modal === "string" ? document.querySelector(modal) : modal;
146        if (!el || !window.bootstrap || !window.bootstrap.Modal) {
147            return;
148        }
149        window.bootstrap.Modal.getOrCreateInstance(el).show();
150    };
151    window.AppUi.hideModal = function(modal) {
152        var el = typeof modal === "string" ? document.querySelector(modal) : modal;
153        if (!el || !window.bootstrap || !window.bootstrap.Modal) {
154            return;
155        }
156        window.bootstrap.Modal.getOrCreateInstance(el).hide();
157    };
158    $(function() {
159        window.AppUi.initTooltips(document);
160    });
161    $(document).on("click", ".js-password-toggle", function() {
162        var button = this;
163        var targetId = button.getAttribute("aria-controls");
164        var input = targetId ? document.getElementById(targetId) : null;
165        if (!input) {
166            var field = button.closest ? button.closest(".auth-password-field") : null;
167            input = field ? field.querySelector("input") : null;
168        }
169        if (!input) {
170            return;
171        }
172        var shouldShow = input.type === "password";
173        input.type = shouldShow ? "text" : "password";
174        var label = shouldShow ? button.getAttribute("data-hide-label") : button.getAttribute("data-show-label");
175        if (label) {
176            button.setAttribute("aria-label", label);
177        }
178        var icon = button.querySelector(".fa-regular, .fa-solid");
179        if (icon) {
180            icon.classList.toggle("fa-eye", !shouldShow);
181            icon.classList.toggle("fa-eye-slash", shouldShow);
182        }
183    });
184    function authMessages(form) {
185        var d = form.dataset || {};
186        return {
187            required: d.msgRequired || "Заполните это поле",
188            email: d.msgEmail || "Введите корректный email",
189            minlength: d.msgMinlength || "Слишком короткое значение"
190        };
191    }
192    function authFieldError(input, msgs) {
193        var v = input.validity;
194        if (!v || v.valid) {
195            return "";
196        }
197        if (v.valueMissing) {
198            return msgs.required;
199        }
200        if (v.typeMismatch) {
201            return msgs.email;
202        }
203        if (v.tooShort) {
204            return msgs.minlength;
205        }
206        if (v.patternMismatch) {
207            return msgs.email;
208        }
209        return msgs.required;
210    }
211    function authGroupOf(input) {
212        return input.closest ? input.closest(".auth-form__group") : null;
213    }
214    function setAuthFieldError(input, message) {
215        var group = authGroupOf(input);
216        if (!group) {
217            return;
218        }
219        var box = group.querySelector(".auth-field-error");
220        if (message) {
221            if (!box) {
222                box = document.createElement("div");
223                box.className = "auth-field-error";
224                box.setAttribute("role", "alert");
225                box.setAttribute("aria-live", "polite");
226                group.appendChild(box);
227            }
228            box.textContent = message;
229            input.classList.add("is-invalid");
230            input.setAttribute("aria-invalid", "true");
231        } else {
232            if (box && box.parentNode) {
233                box.parentNode.removeChild(box);
234            }
235            input.classList.remove("is-invalid");
236            input.removeAttribute("aria-invalid");
237        }
238    }
239    function authValidatableInputs(form) {
240        return Array.prototype.slice.call(form.querySelectorAll('input[required], input[minlength], input[type="email"]')).filter(function(i) {
241            return i.type !== "hidden" && !i.disabled;
242        });
243    }
244    function validateAuthForm(form) {
245        var msgs = authMessages(form);
246        var firstInvalid = null;
247        authValidatableInputs(form).forEach(function(input) {
248            var err = authFieldError(input, msgs);
249            setAuthFieldError(input, err);
250            if (err && !firstInvalid) {
251                firstInvalid = input;
252            }
253        });
254        if (firstInvalid) {
255            firstInvalid.focus();
256        }
257        return !firstInvalid;
258    }
259    $(document).on("focusout", "form[data-auth-form] input", function() {
260        if (!this.form) {
261            return;
262        }
263        setAuthFieldError(this, authFieldError(this, authMessages(this.form)));
264    });
265    $(document).on("input", "form[data-auth-form] input.is-invalid", function() {
266        if (!this.form) {
267            return;
268        }
269        setAuthFieldError(this, authFieldError(this, authMessages(this.form)));
270    });
271    $(document).on("submit", "form[data-auth-form]", function(e) {
272        var form = this;
273        if (!validateAuthForm(form)) {
274            e.preventDefault();
275            return false;
276        }
277        if (form.dataset.submitting === "1") {
278            e.preventDefault();
279            return false;
280        }
281        form.dataset.submitting = "1";
282        var button = form.querySelector('button[type="submit"]');
283        if (button) {
284            button.classList.add("is-loading");
285            button.disabled = true;
286        }
287    });
288    $(document).on("click", ".js-copy-link", function(e) {
289        e.preventDefault();
290        var $btn = $(this);
291        var url = $btn.data("url") || window.location.href;
292        function ok() {
293            $btn.addClass("is-copied");
294            var copiedLabel = $btn.data("copied-label") || "Скопировано";
295            var $hint = $('<span class="copy-hint" role="status" aria-live="polite"></span>').text(copiedLabel);
296            $btn.after($hint);
297            setTimeout(function() {
298                $btn.removeClass("is-copied");
299                $hint.fadeOut(150, function() {
300                    $(this).remove();
301                });
302            }, 1500);
303        }
304        if (navigator.clipboard && navigator.clipboard.writeText) {
305            navigator.clipboard.writeText(url).then(ok).catch(function() {
306                try {
307                    var ta = document.createElement("textarea");
308                    ta.value = url;
309                    document.body.appendChild(ta);
310                    ta.select();
311                    document.execCommand("copy");
312                    document.body.removeChild(ta);
313                    ok();
314                } catch (err) {}
315            });
316        } else {
317            try {
318                var ta = document.createElement("textarea");
319                ta.value = url;
320                document.body.appendChild(ta);
321                ta.select();
322                document.execCommand("copy");
323                document.body.removeChild(ta);
324                ok();
325            } catch (err) {}
326        }
327        return false;
328    });
329    (function() {
330        var track = document.getElementById("mobileGalleryTrack");
331        if (!track) return;
332        var slides = Array.prototype.slice.call(track.querySelectorAll(".gallery-slide"));
333        var thumbs = Array.prototype.slice.call(document.querySelectorAll(".gallery-thumb"));
334        var dots = Array.prototype.slice.call(document.querySelectorAll(".gallery-dot"));
335        var prevBtn = document.getElementById("galleryPrevBtn");
336        var nextBtn = document.getElementById("galleryNextBtn");
337        function clamp(i) {
338            if (i < 0) return 0;
339            if (i >= slides.length) return slides.length - 1;
340            return i;
341        }
342        function setActive(index) {
343            thumbs.forEach(function(t) {
344                t.classList.toggle("is-active", parseInt(t.getAttribute("data-index")) === index);
345            });
346            dots.forEach(function(d) {
347                var on = parseInt(d.getAttribute("data-index")) === index;
348                d.classList.toggle("is-active", on);
349                d.setAttribute("aria-pressed", on ? "true" : "false");
350            });
351            if (prevBtn) prevBtn.disabled = index <= 0;
352            if (nextBtn) nextBtn.disabled = index >= slides.length - 1;
353        }
354        function scrollToIndex(index) {
355            index = clamp(index);
356            var slide = slides[index];
357            if (!slide) return;
358            if (typeof slide.scrollIntoView === "function") {
359                slide.scrollIntoView({
360                    behavior: "smooth",
361                    inline: "center",
362                    block: "nearest"
363                });
364            } else {
365                var left = slide.offsetLeft - 8;
366                $(track).animate({
367                    scrollLeft: left
368                }, 200);
369            }
370            setActive(index);
371        }
372        $(document).on("click", ".gallery-thumb", function() {
373            var idx = parseInt(this.getAttribute("data-index")) || 0;
374            scrollToIndex(idx);
375        });
376        $(document).on("click", ".gallery-dot", function() {
377            var idx = parseInt(this.getAttribute("data-index")) || 0;
378            scrollToIndex(idx);
379        });
380        if (prevBtn) {
381            prevBtn.addEventListener("click", function() {
382                var current = thumbs.findIndex(function(t) {
383                    return t.classList.contains("is-active");
384                });
385                if (current < 0) current = 0;
386                scrollToIndex(current - 1);
387            });
388        }
389        if (nextBtn) {
390            nextBtn.addEventListener("click", function() {
391                var current = thumbs.findIndex(function(t) {
392                    return t.classList.contains("is-active");
393                });
394                if (current < 0) current = 0;
395                scrollToIndex(current + 1);
396            });
397        }
398        var raf = null;
399        function onScroll() {
400            if (raf) return;
401            raf = requestAnimationFrame(function() {
402                raf = null;
403                var trackRect = track.getBoundingClientRect();
404                var center = trackRect.left + trackRect.width / 2;
405                var best = 0, bestDist = Infinity;
406                for (var i = 0; i < slides.length; i++) {
407                    var r = slides[i].getBoundingClientRect();
408                    var slideCenter = r.left + r.width / 2;
409                    var dist = Math.abs(slideCenter - center);
410                    if (dist < bestDist) {
411                        bestDist = dist;
412                        best = i;
413                    }
414                }
415                setActive(best);
416            });
417        }
418        track.addEventListener("scroll", onScroll, {
419            passive: true
420        });
421        setTimeout(function() {
422            onScroll();
423        }, 300);
424    })();
425    $(document).on("show.bs.modal", "#reportModal", function(e) {
426        var $modal = $(this);
427        var $url = $modal.find("#report-url");
428        if ($url.length) {
429            $url.val(window.location.href);
430        }
431        var $type = $modal.find("#report-type");
432        var trigger = e.relatedTarget;
433        if ($type.length && trigger) {
434            var $trigger = $(trigger);
435            var desired = $trigger.data("report");
436            if (desired) {
437                $type.val(desired).trigger("change");
438            }
439        }
440    });
441    $(document).on("submit", ".js-add-comment-form", function(e) {
442        e.preventDefault();
443        var $form = $(this);
444        var url = $form.data("url") || "/comments/add";
445        var $btn = $("#send_new_comment");
446        var $textarea = $("#new_comment");
447        var $error = $form.find(".js-comment-error");
448        var requiredMsg = $form.data("required-msg") || "Поле обязательно для заполнения";
449        var text = ($textarea.val() || "").trim();
450        var fileInput = $form.find(".js-attach-input").get(0);
451        var hasFile = !!(fileInput && fileInput.files && fileInput.files.length > 0);
452        // Пустой текст допустим, если прикреплена картинка (сообщение = только фото).
453        if (!text && !hasFile) {
454            if ($error.length) {
455                $error.text(requiredMsg);
456            }
457            $textarea.focus();
458            return;
459        }
460        if ($btn.prop("disabled") || $btn.hasClass("is-loading")) {
461            return;
462        }
463        $btn.prop("disabled", true).addClass("is-loading");
464        if ($error.length) {
465            $error.text("");
466        }
467        var ajaxOpts = { url: url, type: "POST", dataType: "json" };
468        if (hasFile) {
469            // multipart — иначе файл не уедет
470            ajaxOpts.data = new FormData($form.get(0));
471            ajaxOpts.processData = false;
472            ajaxOpts.contentType = false;
473        } else {
474            ajaxOpts.data = $form.serialize();
475        }
476        $.ajax(ajaxOpts).done(function(resp, status, xhr) {
477            if (resp && resp.result) {
478                if (window.CommentEditor && window.CommentEditor.isEnhanced($textarea.get(0))) {
479                    window.CommentEditor.clear($textarea.get(0));
480                } else {
481                    $textarea.val("");
482                }
483                $("#comment_length").text("0/2000");
484                $("#reply_parent_id").val("");
485                // сбросить прикреплённую картинку
486                if (fileInput) { fileInput.value = ""; }
487                var $preview = $form.find(".js-attach-preview");
488                if ($preview.length) {
489                    $preview.attr("hidden", "hidden").find(".ua-attach-preview__img").attr("src", "");
490                }
491                if ($error.length) {
492                    $error.text("");
493                }
494                if (window.GuestCommentDraft) {
495                    window.GuestCommentDraft.clearFor($form);
496                }
497                var $count = $(".js-comments-count");
498                $count.text(parseInt($count.text(), 10) + 1);
499                var html = resp.html || "";
500                var id = typeof resp.id !== "undefined" ? resp.id : resp.data && resp.data.id ? resp.data.id : "";
501                if (html) {
502                    var $tmp = $("<div/>").html(html.trim());
503                    var $item = $tmp.children().first();
504                    $item.addClass("comment-just-added");
505                    $(".comments_block").append($item);
506                } else {
507                    $(".comments_block").append('<div class="comments comment-just-added"><div class="comment_text" id="' + id + '">' + (resp.data && resp.data.text ? resp.data.text : "") + "</div></div>");
508                }
509                var $lastComment = $(".comments_block .comments").last();
510                $lastComment.on("animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", function() {
511                    $(this).removeClass("comment-just-added");
512                });
513                if (window.history && window.history.replaceState) {
514                    var hash = "#" + id;
515                    window.history.replaceState({}, document.title, window.location.pathname + window.location.search + hash);
516                }
517            } else {
518                var msg = resp && resp.msg ? resp.msg : "Ошибка отправки";
519                if ($error.length) {
520                    $error.text(msg);
521                } else {
522                    alert(msg);
523                }
524            }
525        }).fail(function(xhr) {
526            var msg = "Сеть недоступна. Попробуйте позже";
527            if (xhr && xhr.responseJSON && xhr.responseJSON.msg) {
528                msg = xhr.responseJSON.msg;
529            }
530            if ($error.length) {
531                $error.text(msg);
532            } else {
533                alert(msg);
534            }
535        }).always(function() {
536            $btn.prop("disabled", false).removeClass("is-loading");
537        });
538    });
539    function showPurchaseAccessModal(err) {
540        var t = window.AppTranslations || {};
541        var data = err || {};
542        var isLoginRequired = data.type === "login_required";
543        var title = data.title || t.purchaseAccessTitle || "Покупка лота";
544        var message = data.message || t.purchaseAccessUnavailable || "Действие сейчас недоступно.";
545        var loginUrl = data.loginUrl || "/login";
546        var registerUrl = data.registerUrl || "/registration";
547        $("#purchaseErrorTitle").text(title);
548        $("#purchaseErrorMessage").html(message);
549        if (isLoginRequired) {
550            $("#purchaseErrorHint").removeClass("d-none").text(t.purchaseAccessHint || "После входа вы сможете сразу продолжить.").show();
551            $("#purchaseErrorLoginBtn").removeClass("d-none").data("redirectUrl", loginUrl).show();
552            $("#purchaseErrorRegisterBtn").removeClass("d-none").data("redirectUrl", registerUrl).show();
553            $("#purchaseErrorCloseBtn").removeClass("btn-primary").addClass("btn-link").text(t.modalLater || "Позже");
554        } else {
555            $("#purchaseErrorHint").addClass("d-none").hide().text("");
556            $("#purchaseErrorLoginBtn").addClass("d-none").hide().removeData("redirectUrl");
557            $("#purchaseErrorRegisterBtn").addClass("d-none").hide().removeData("redirectUrl");
558            $("#purchaseErrorCloseBtn").removeClass("btn-link").addClass("btn-primary").text(t.modalOk || "Понятно");
559        }
560        window.AppUi.showModal("#purchaseErrorModal");
561    }
562    $(document).on("click", "#purchaseErrorLoginBtn, #purchaseErrorRegisterBtn", function() {
563        var url = $(this).data("redirectUrl");
564        if (url) {
565            window.location.href = url;
566        }
567    });
568    function escapeHtml(value) {
569        return String(value == null ? "" : value).replace(/[&<>"']/g, function(ch) {
570            return {
571                "&": "&amp;",
572                "<": "&lt;",
573                ">": "&gt;",
574                "\"": "&quot;",
575                "'": "&#039;"
576            }[ch];
577        });
578    }
579    function buildPurchaseConfirmMessage(title, price) {
580        var t = window.AppTranslations || {};
581        var intro = t.purchaseConfirmIntro || "Вы покупаете";
582        var note = t.purchaseConfirmNote || "После подтверждения будет создана сделка. Далее заполните данные доставки и оплаты.";
583        var priceLabel = t.purchaseConfirmPrice || "Цена";
584
585        return [
586            '<div class="purchase-confirm">',
587                '<div class="purchase-confirm__label">' + escapeHtml(intro) + '</div>',
588                '<div class="purchase-confirm__title">«' + escapeHtml(title) + '»</div>',
589                '<div class="purchase-confirm__price-row">',
590                    '<span class="purchase-confirm__price-label">' + escapeHtml(priceLabel) + '</span>',
591                    '<strong class="purchase-confirm__price">' + escapeHtml(price) + '</strong>',
592                '</div>',
593                '<div class="purchase-confirm__note">' + escapeHtml(note) + '</div>',
594            '</div>'
595        ].join("");
596    }
597    $(document).on("submit", ".buy-form:not(.cart-add-form):not(.favorite-toggle-form)", function(e) {
598        e.preventDefault();
599        var $form = $(this);
600        var $btn = $form.find(".buy-btn");
601        var actionUrl = $form.attr("action");
602        var advertId = actionUrl.match(/\/(\d+)$/)[1];
603        if ($btn.prop("disabled") || $btn.hasClass("is-loading")) {
604            return;
605        }
606        if (window.PurchasePermissions && !window.PurchasePermissions.canBuy) {
607            showPurchaseAccessModal(window.PurchasePermissions.error || {});
608            return;
609        }
610        $btn.prop("disabled", true).addClass("is-loading");
611        var price = $form.data("buy-price") || $(".sale-price").text().trim();
612        var advertTitle = $form.data("buy-title") || $("h1").text().trim();
613        var purchaseConfirm = window.AppTranslations && window.AppTranslations.purchaseConfirm ? window.AppTranslations.purchaseConfirm : "Подтверждение покупки";
614        window.showConfirm({
615            title: purchaseConfirm,
616            messageHtml: buildPurchaseConfirmMessage(advertTitle, price)
617        }).then(function(ok) {
618            if (ok) {
619                var $tempForm = $("<form>", {
620                    method: "POST",
621                    action: $form.attr("action")
622                });
623                $tempForm.append($form.find("input").clone());
624                $tempForm.appendTo("body").submit();
625            } else {
626                $btn.prop("disabled", false).removeClass("is-loading");
627            }
628        }).catch(function() {
629            $btn.prop("disabled", false).removeClass("is-loading");
630        });
631    });
632    $(document).on("submit", ".cart-add-form", function(e) {
633        e.preventDefault();
634        var $form = $(this);
635        var $btn = $form.find(".buy-btn");
636        var cartUrl = $btn.data("cart-url") || "/cart";
637        var inCartLabel = $btn.data("label-in-cart") || "В корзине";
638        if ($form.hasClass("is-in-cart")) {
639            window.location.href = cartUrl;
640            return;
641        }
642        if ($btn.prop("disabled") || $btn.hasClass("is-loading")) {
643            return;
644        }
645        if (window.PurchasePermissions && !window.PurchasePermissions.canBuy) {
646            showPurchaseAccessModal(window.PurchasePermissions.error || {});
647            return;
648        }
649        $btn.prop("disabled", true).addClass("is-loading");
650        $.ajax({
651            url: $form.attr("action"),
652            method: "POST",
653            data: $form.serialize()
654        }).done(function() {
655            $form.addClass("is-in-cart");
656            $btn.removeClass("is-loading").addClass("buy-btn--in-cart");
657            $btn.find(".buy-btn__label").text(inCartLabel);
658            $btn.prop("disabled", false);
659        }).fail(function() {
660            $btn.prop("disabled", false).removeClass("is-loading");
661            alert("Не удалось добавить в корзину. Попробуйте еще раз.");
662        });
663    });
664    $(document).on("submit", ".favorite-toggle-form", function(e) {
665        e.preventDefault();
666        var $form = $(this);
667        var $btn = $form.find(".favorite-btn");
668        var $icon = $btn.find(".fa-heart");
669        var $count = $form.find(".favorite-count");
670        if ($btn.prop("disabled") || $btn.hasClass("is-loading")) {
671            return;
672        }
673        if (window.PurchasePermissions && !window.PurchasePermissions.canBuy) {
674            showPurchaseAccessModal(window.PurchasePermissions.error || {});
675            return;
676        }
677        $btn.prop("disabled", true).addClass("is-loading");
678        $.ajax({
679            url: $form.attr("action"),
680            method: "POST",
681            data: $form.serialize()
682        }).done(function(resp) {
683            if (!resp || !resp.ok) {
684                throw new Error("bad_response");
685            }
686            var isFavorite = !!resp.isFavorite;
687            var count = parseInt(resp.count, 10);
688            if (isNaN(count) || count < 0) {
689                count = 0;
690            }
691            $form.toggleClass("is-favorite", isFavorite);
692            $btn.toggleClass("is-active", isFavorite);
693            $icon.toggleClass("fa-solid", isFavorite).toggleClass("fa-regular", !isFavorite);
694            var t = window.AppTranslations || {};
695            var addTitle = t.addToFavorites || "Добавить в избранное";
696            var removeTitle = t.removeFromFavorites || "Убрать из избранного";
697            var nextTitle = isFavorite ? removeTitle : addTitle;
698            $btn.attr("title", nextTitle).attr("data-original-title", nextTitle);
699            var $label = $btn.find(".favorite-btn__label");
700            if ($label.length) {
701                var addLabel = t.favoriteLabelAdd || "В избранное";
702                var activeLabel = t.favoriteLabelActive || "В избранном";
703                $label.text(isFavorite ? activeLabel : addLabel);
704            }
705            $count.text(count);
706            if (count > 0 || isFavorite) {
707                $count.removeClass("is-hidden");
708            } else {
709                $count.addClass("is-hidden");
710            }
711        }).fail(function() {
712            alert("Не удалось обновить избранное. Попробуйте еще раз.");
713        }).always(function() {
714            $btn.prop("disabled", false).removeClass("is-loading");
715        });
716    });
717    $(".comments_block").on("click", ".remove-comment", function() {
718        var $btn = $(this);
719        var isRestore = !!$btn.closest(".comments").find(".restore-comment").length;
720        if (isRestore) {
721            removeComment($btn);
722            return false;
723        }
724        var confirmMessage = getCommentI18n("delete-confirm", "Действительно удалить этот комментарий?");
725        if (window.showConfirm) {
726            window.showConfirm({
727                title: getCommentI18n("delete-title", "Подтверждение удаления"),
728                messageHtml: confirmMessage
729            }).then(function(ok) {
730                if (ok) {
731                    removeComment($btn);
732                }
733            });
734        } else if (confirm(confirmMessage)) {
735            removeComment($btn);
736        }
737        return false;
738    });
739    $(".comments_block").on("click", ".restore-comment", function() {
740        removeComment($(this));
741        return false;
742    });
743    (function() {
744        var $btn = $("#emoji_btn");
745        var $panel = $("#emoji_panel");
746        var $ta = $("#new_comment");
747        if (!$btn.length || !$panel.length || !$ta.length) {
748            return;
749        }
750        var isMobile = window.matchMedia && window.matchMedia("(max-width: 768px)").matches;
751        if (isMobile) {
752            if (window.bootstrap && window.bootstrap.Tooltip && $btn[0]) {
753                var emojiTooltip = window.bootstrap.Tooltip.getInstance($btn[0]);
754                if (emojiTooltip) {
755                    emojiTooltip.dispose();
756                }
757            }
758        }
759        function closePanel() {
760            $panel.hide().attr("aria-hidden", "true");
761        }
762        function openPanel() {
763            $panel.show().attr("aria-hidden", "false");
764        }
765        $btn.on("click", function(e) {
766            e.preventDefault();
767            if ($panel.is(":visible")) {
768                closePanel();
769            } else {
770                openPanel();
771            }
772        });
773        function insertAtCursor(emoji) {
774            var el = $ta.get(0);
775            if (!el) {
776                return;
777            }
778            // Если поле превращено в WYSIWYG-редактор — вставляем в него.
779            if (window.CommentEditor && window.CommentEditor.insert(el, emoji)) {
780                return;
781            }
782            el.focus();
783            var start = el.selectionStart || 0;
784            var end = el.selectionEnd || 0;
785            var value = el.value || "";
786            el.value = value.substring(0, start) + emoji + value.substring(end);
787            var pos = start + emoji.length;
788            if (el.setSelectionRange) {
789                el.setSelectionRange(pos, pos);
790            }
791            $ta.trigger("input");
792        }
793        $panel.on("click", ".emoji-item", function(e) {
794            e.preventDefault();
795            var emoji = $(this).text();
796            insertAtCursor(emoji);
797            closePanel();
798        });
799        $(document).on("click", function(e) {
800            if (!$panel.is(":visible")) {
801                return;
802            }
803            var $t = $(e.target);
804            if ($t.closest("#emoji_panel").length === 0 && $t.closest("#emoji_btn").length === 0) {
805                closePanel();
806            }
807        });
808        $(document).on("keydown", function(e) {
809            if (e.key === "Escape") {
810                closePanel();
811            }
812        });
813        $(window).on("resize", function() {
814            var mobile = window.matchMedia && window.matchMedia("(max-width: 768px)").matches;
815            if (mobile) {
816                closePanel();
817            }
818        });
819    })();
820    $(".carousel").carousel();
821    $("[id^=carousel-selector-]").click(function() {
822        var id_selector = $(this).attr("id");
823        var id = id_selector.substr(id_selector.length - 1);
824        id = parseInt(id);
825        $(".carousel").carousel(id);
826        $("[id^=carousel-selector-]").removeClass("selected");
827        $(this).addClass("selected");
828    });
829    $(".carousel").on("slid", function(e) {
830        var id = $(".item.active").data("slide-number");
831        id = parseInt(id);
832        $("[id^=carousel-selector-]").removeClass("selected");
833        $("[id=carousel-selector-" + id + "]").addClass("selected");
834    });
835    $(".comments_block").on("click", ".reply", function(e) {
836        e.preventDefault();
837        var $commentsContainer = $(this).closest(".comments-container");
838        var $addCommentBlock = $commentsContainer.find(".add-comment-block");
839        var $textarea = $addCommentBlock.find("#new_comment");
840        if (!$textarea.length) {
841            var $guestCompose = $addCommentBlock.find(".guest-comment-compose");
842            var $target = $guestCompose.length ? $guestCompose : $addCommentBlock;
843            if ($target.length) {
844                $([ document.documentElement, document.body ]).animate({
845                    scrollTop: Math.max($target.offset().top - 16, 0)
846                }, 500).promise().done(function() {
847                    if ($guestCompose.length) {
848                        $guestCompose.removeClass("guest-comment-compose--prompted");
849                        $guestCompose[0].offsetHeight;
850                        $guestCompose.addClass("guest-comment-compose--prompted");
851                        window.setTimeout(function() {
852                            $guestCompose.removeClass("guest-comment-compose--prompted");
853                        }, 1400);
854                    }
855                    var $loginLink = $target.find("a[href]").first();
856                    if ($loginLink.length) {
857                        try {
858                            $loginLink[0].focus({
859                                preventScroll: true
860                            });
861                        } catch (error) {
862                            $loginLink[0].focus();
863                        }
864                    }
865                });
866            }
867            return;
868        }
869        var parentId = $(this).data("comment-id");
870        $addCommentBlock.find("#reply_parent_id").val(parentId || "");
871        var comment = $textarea.val() || "";
872        var user_name = $(this).data("user-name");
873        var user_name_txt = user_name + ", ";
874        var has_string = comment.indexOf(user_name_txt) >= 0;
875        if (has_string == false) {
876            $textarea.val(user_name_txt + comment);
877            if (window.CommentEditor) {
878                window.CommentEditor.pull($textarea.get(0));
879            }
880        }
881        $([ document.documentElement, document.body ]).animate({
882            scrollTop: Math.max($addCommentBlock.offset().top - 16, 0)
883        }, 500).promise().done(function() {
884            var rich = $textarea.get(0) && $textarea.get(0)._rich;
885            (rich || $textarea.get(0)).focus();
886        });
887    });
888    $(".comments_block").on("click", ".comment-edit-trigger", function(e) {
889        e.preventDefault();
890        openInlineCommentEditor($(this).closest(".comments"));
891    });
892    $(".comments_block").on("click", ".comment-edit-cancel", function(e) {
893        e.preventDefault();
894        var $wrap = $(this).closest(".comment-inline-editor-wrap");
895        var $text = $wrap.siblings(".comment-text-content");
896        var $votes = $wrap.siblings(".comment-vote-box");
897        $wrap.remove();
898        $text.show();
899        $votes.show();
900    });
901    $(".comments_block").on("click", ".comment-edit-save", function(e) {
902        e.preventDefault();
903        saveInlineCommentEdit($(this).closest(".comments"));
904    });
905    $(".comments_block").on("keydown", ".comment-inline-editor, .ua-rich-editor--inline", function(e) {
906        if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
907            e.preventDefault();
908            saveInlineCommentEdit($(this).closest(".comments"));
909        }
910    });
911    $("#report-error").on("click", function() {
912        var form = $(this).closest("form");
913        var textField = form.find("#report-text");
914        var urlField = form.find("#report-url");
915        clearInvalid(textField);
916        clearInvalid(urlField);
917        var text = textField.val();
918        var url = urlField.val();
919        var hasError = false;
920        if (!text || text.length >= 1e3) {
921            setInvalid(textField);
922            hasError = true;
923        }
924        if (!url || url.length >= 250) {
925            setInvalid(urlField);
926            hasError = true;
927        }
928        if (hasError) {
929            return false;
930        }
931        reportError(form);
932        return false;
933    });
934    function setInvalid(field) {
935        field.closest(".form-group").addClass("is-invalid");
936    }
937    function clearInvalid(field) {
938        field.closest(".form-group").removeClass("is-invalid");
939    }
940    $("#report-text, #report-url").on("input", function() {
941        $(this).closest(".form-group").removeClass("is-invalid");
942    });
943    $("body").on("click", ".like-button", function() {
944        $(this).removeClass("like-button");
945        likeComment($(this));
946        return false;
947    });
948    new_comment = document.getElementById("new_comment");
949    if (new_comment) {
950        new_comment.addEventListener("input", function() {
951            comment_length.innerText = new_comment.value.length + "/2000";
952            var err = document.querySelector(".js-comment-error");
953            if (err && new_comment.value.trim().length > 0) {
954                err.textContent = "";
955            }
956        });
957    }
958    (function initGuestCommentDraft() {
959        var PREFIX = "uacoins_comment_draft:";
960        var PENDING = "uacoins_comment_draft_pending";
961        var TTL = 24 * 60 * 60 * 1e3;
962        var MAX_LEN = 2e3;
963        function hasLS() {
964            try {
965                var k = "__uacoins_t";
966                localStorage.setItem(k, "1");
967                localStorage.removeItem(k);
968                return true;
969            } catch (e) {
970                return false;
971            }
972        }
973        var LS = hasLS();
974        function keyFor(model, id) {
975            return PREFIX + model + ":" + id;
976        }
977        function save(model, id, text) {
978            if (!LS) return;
979            try {
980                if (text && text.trim()) {
981                    localStorage.setItem(keyFor(model, id), JSON.stringify({
982                        text: String(text).slice(0, MAX_LEN),
983                        ts: Date.now()
984                    }));
985                } else {
986                    localStorage.removeItem(keyFor(model, id));
987                }
988            } catch (e) {}
989        }
990        function read(model, id) {
991            if (!LS) return "";
992            try {
993                var raw = localStorage.getItem(keyFor(model, id));
994                if (!raw) return "";
995                var obj = JSON.parse(raw);
996                if (!obj || !obj.text) return "";
997                if (obj.ts && Date.now() - obj.ts > TTL) {
998                    localStorage.removeItem(keyFor(model, id));
999                    return "";
1000                }
1001                return String(obj.text).slice(0, MAX_LEN);
1002            } catch (e) {
1003                return "";
1004            }
1005        }
1006        window.GuestCommentDraft = {
1007            clearFor: function($form) {
1008                if (!LS) return;
1009                try {
1010                    var m = $form.find('input[name="model"]').val();
1011                    var mid = $form.find('input[name="model_id"]').val();
1012                    if (m && mid) {
1013                        localStorage.removeItem(keyFor(m, mid));
1014                    }
1015                    localStorage.removeItem(PENDING);
1016                } catch (e) {}
1017            }
1018        };
1019        var $guest = $(".js-guest-comment");
1020        if ($guest.length) {
1021            var gModel = String($guest.data("model"));
1022            var gId = String($guest.data("model-id"));
1023            var $text = $guest.find(".js-guest-comment-text");
1024            var $len = $("#guest_comment_length");
1025            function updLen() {
1026                if ($len.length) {
1027                    $len.text(($text.val() || "").length + "/2000");
1028                }
1029            }
1030            var existing = read(gModel, gId);
1031            if (existing && $text.length && !$text.val()) {
1032                $text.val(existing);
1033            }
1034            updLen();
1035            $text.on("input", function() {
1036                save(gModel, gId, $text.val());
1037                updLen();
1038            });
1039            $guest.on("click", ".js-guest-comment-auth", function(e) {
1040                var base = $(this).data("href");
1041                if (!base) {
1042                    return;
1043                }
1044                e.preventDefault();
1045                var text = ($text.val() || "").trim();
1046                if (text) {
1047                    save(gModel, gId, text);
1048                    try {
1049                        localStorage.setItem(PENDING, keyFor(gModel, gId));
1050                    } catch (err) {}
1051                }
1052                var target = window.location.pathname + window.location.search + "#comment_form";
1053                var url = base + (base.indexOf("?") > -1 ? "&" : "?") + "_target_path=" + encodeURIComponent(target);
1054                window.location.href = url;
1055            });
1056        }
1057        var $form = $(".js-add-comment-form");
1058        var $new = $("#new_comment");
1059        if ($form.length && $new.length && LS) {
1060            var lModel = String($form.data("model") || $form.find('input[name="model"]').val() || "");
1061            var lId = String($form.data("model-id") || $form.find('input[name="model_id"]').val() || "");
1062            if (lModel && lId) {
1063                var draft = read(lModel, lId);
1064                var pending = "";
1065                try {
1066                    pending = localStorage.getItem(PENDING) || "";
1067                } catch (e) {}
1068                var justLoggedIn = pending && pending === keyFor(lModel, lId);
1069                if (draft && !$new.val()) {
1070                    var newEl = $new[0];
1071                    newEl.value = draft;
1072                    newEl.dispatchEvent(new Event("input", {
1073                        bubbles: true
1074                    }));
1075                    if (justLoggedIn) {
1076                        var hint = document.getElementById("comment_restored_hint");
1077                        if (hint) {
1078                            hint.hidden = false;
1079                        }
1080                        var anchor = document.getElementById("comment_form");
1081                        if (anchor && anchor.scrollIntoView) {
1082                            anchor.scrollIntoView({
1083                                behavior: "smooth",
1084                                block: "center"
1085                            });
1086                        }
1087                        try {
1088                            $new.focus();
1089                            var v = $new.val();
1090                            $new.val("").val(v);
1091                        } catch (e) {}
1092                    }
1093                }
1094                if (justLoggedIn) {
1095                    try {
1096                        localStorage.removeItem(PENDING);
1097                    } catch (e) {}
1098                }
1099            }
1100        }
1101    })();
1102    new_feedback = document.getElementById("new_feedback");
1103    if (new_feedback) {
1104        new_feedback.addEventListener("input", function() {
1105            feedback_length.innerText = new_feedback.value.length + "/500";
1106        });
1107    }
1108    var shareLocale = document.body ? document.body.getAttribute("data-current-locale") : "ru";
1109    var shareLabel = shareLocale === "ua" ? "Поділитися" : shareLocale === "en" ? "Share" : "Поделиться";
1110    var shareCopyLabel = shareLocale === "ua" ? "Копіювати посилання" : shareLocale === "en" ? "Copy link" : "Копировать ссылку";
1111    var shareCopiedLabel = shareLocale === "ua" ? "Скопійовано" : shareLocale === "en" ? "Copied" : "Скопировано";
1112    function fallbackCopy(text) {
1113        var input = document.createElement("input");
1114        input.value = text;
1115        input.setAttribute("readonly", "readonly");
1116        input.className = "ua-offscreen-copy-input";
1117        document.body.appendChild(input);
1118        input.select();
1119        document.execCommand("copy");
1120        document.body.removeChild(input);
1121    }
1122    function copyTextToClipboard(text, done) {
1123        if (navigator.clipboard && navigator.clipboard.writeText) {
1124            navigator.clipboard.writeText(text).then(done).catch(function() {
1125                fallbackCopy(text);
1126                done();
1127            });
1128            return;
1129        }
1130        fallbackCopy(text);
1131        done();
1132    }
1133    function getFancyboxShareData(carousel) {
1134        var page = carousel && carousel.getPage ? carousel.getPage() : null;
1135        var slide = page && page.slides && page.slides.length ? page.slides[0] : null;
1136        var url = window.location.href;
1137        var title = document.title;
1138        var media = "";
1139        if (slide) {
1140            title = normalizeShareText(slide.caption || slide.alt || title);
1141            media = makeAbsoluteUrl(slide.src || "");
1142        }
1143        return {
1144            url: url,
1145            title: normalizeShareText(title),
1146            media: media
1147        };
1148    }
1149    function normalizeShareText(value) {
1150        if (value instanceof HTMLElement) {
1151            return value.textContent || "";
1152        }
1153        return $("<div>").html(String(value || "")).text();
1154    }
1155    function makeAbsoluteUrl(url) {
1156        if (!url) {
1157            return "";
1158        }
1159        try {
1160            return new URL(url, window.location.href).href;
1161        } catch (e) {
1162            return url;
1163        }
1164    }
1165    function openShareWindow(url) {
1166        window.open(url, "", "left=0,top=0,width=640,height=420,menubar=no,toolbar=no,resizable=yes,scrollbars=yes");
1167    }
1168    function closeFancyboxShareMenu(container) {
1169        if (!container) {
1170            return;
1171        }
1172        var existingMenu = container.querySelector(".ua-fancybox-share-menu");
1173        if (existingMenu) {
1174            existingMenu.remove();
1175        }
1176        Array.prototype.forEach.call(container.querySelectorAll('[data-ua-fancybox-share][aria-expanded="true"]'), function(button) {
1177            button.setAttribute("aria-expanded", "false");
1178        });
1179    }
1180    function openFancyboxShareMenu(carousel, event) {
1181        var container = carousel && carousel.getContainer ? carousel.getContainer() : null;
1182        var root = container ? container.closest(".fancybox__container") : null;
1183        if (!root) {
1184            return;
1185        }
1186        var currentMenu = root.querySelector(".ua-fancybox-share-menu");
1187        if (currentMenu) {
1188            currentMenu.remove();
1189            if (event && event.currentTarget) {
1190                event.currentTarget.setAttribute("aria-expanded", "false");
1191            }
1192            return;
1193        }
1194        var data = getFancyboxShareData(carousel);
1195        var encodedUrl = encodeURIComponent(data.url);
1196        var encodedTitle = encodeURIComponent(data.title);
1197        var encodedShareText = encodeURIComponent((data.title ? data.title + " " : "") + data.url);
1198        var menu = document.createElement("div");
1199        menu.className = "ua-fancybox-share-menu";
1200        menu.setAttribute("role", "menu");
1201        menu.innerHTML = "" + '<button class="ua-fancybox-share-menu__item js-fancybox-share-copy" type="button" role="menuitem">' + shareCopyLabel + "</button>" + '<a class="ua-fancybox-share-menu__item" role="menuitem" href="https://t.me/share/url?url=' + encodedUrl + "&text=" + encodedTitle + '">Telegram</a>' + '<a class="ua-fancybox-share-menu__item" role="menuitem" href="viber://forward?text=' + encodedShareText + '">Viber</a>' + '<a class="ua-fancybox-share-menu__item" role="menuitem" href="https://www.facebook.com/sharer/sharer.php?u=' + encodedUrl + '">Facebook</a>' + '<a class="ua-fancybox-share-menu__item" role="menuitem" href="https://api.whatsapp.com/se
1201nd?text=' + encodedShareText + '">WhatsApp</a>';
1202        menu.querySelector(".js-fancybox-share-copy").addEventListener("click", function() {
1203            var button = this;
1204            copyTextToClipboard(data.url, function() {
1205                button.textContent = shareCopiedLabel;
1206                setTimeout(function() {
1207                    button.textContent = shareCopyLabel;
1208                }, 1600);
1209            });
1210        });
1211        Array.prototype.forEach.call(menu.querySelectorAll("a"), function(link) {
1212            link.addEventListener("click", function(clickEvent) {
1213                clickEvent.preventDefault();
1214                openShareWindow(link.href);
1215            });
1216        });
1217        root.appendChild(menu);
1218        if (event && event.currentTarget) {
1219            event.currentTarget.setAttribute("aria-expanded", "true");
1220        }
1221    }
1222    $(document).on("click", function(event) {
1223        var target = event.target;
1224        if (!target || !target.closest || target.closest(".ua-fancybox-share-menu") || target.closest("[data-ua-fancybox-share]")) {
1225            return;
1226        }
1227        closeFancyboxShareMenu(document);
1228    });
1229    if (window.Fancybox && window.Fancybox.bind) {
1230        window.Fancybox.bind("[data-fancybox]", {
1231            l10n: {
1232                SHARE: shareLabel
1233            },
1234            wheel: "slide",
1235            Carousel: {
1236                infinite: true,
1237                Toolbar: {
1238                    absolute: true,
1239                    items: {
1240                        share: {
1241                            tpl: '<button data-ua-fancybox-share class="f-button" title="{{SHARE}}" aria-label="{{SHARE}}" aria-expanded="false"><svg><path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/></svg></button>',
1242                            click: openFancyboxShareMenu
1243                        }
1244                    },
1245                    display: {
1246                        left: [ "counter" ],
1247                        middle: [ "zoomIn", "zoomOut", "toggle1to1" ],
1248                        right: [ "share", "autoplay", "fullscreen", "thumbs", "close" ]
1249                    }
1250                },
1251                on: {
1252                    change: function(carousel) {
1253                        var container = carousel && carousel.getContainer ? carousel.getContainer() : null;
1254                        var root = container ? container.closest(".fancybox__container") : null;
1255                        closeFancyboxShareMenu(root);
1256                    }
1257                }
1258            },
1259            on: {
1260                close: function(fancybox) {
1261                    closeFancyboxShareMenu(fancybox && fancybox.getContainer ? fancybox.getContainer() : document);
1262                }
1263            }
1264        });
1265    }
1266    (function() {
1267        var form = document.getElementById("deal-chat-form");
1268        if (!form) {
1269            return;
1270        }
1271        var list = document.getElementById("deal-chat-list");
1272        var scrollToBottom = function(force) {
1273            if (!list) {
1274                return;
1275            }
1276            var nearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 140;
1277            if (force || nearBottom) {
1278                list.scrollTop = list.scrollHeight;
1279            }
1280        };
1281        scrollToBottom(true);
1282        setTimeout(function() {
1283            scrollToBottom(true);
1284        }, 0);
1285        setTimeout(function() {
1286            scrollToBottom(true);
1287        }, 120);
1288        window.addEventListener("load", function() {
1289            scrollToBottom(true);
1290        });
1291        if (list) {
1292            Array.prototype.forEach.call(list.querySelectorAll("img"), function(img) {
1293                if (img.complete) {
1294                    return;
1295                }
1296                img.addEventListener("load", function() {
1297                    scrollToBottom(true);
1298                }, {
1299                    once: true
1300                });
1301            });
1302        }
1303        var ta = form.querySelector(".deal-chat__input");
1304        var sendBtn = form.querySelector(".deal-chat__send");
1305        var fileInput = document.getElementById("deal-chat-photos");
1306        var captionInput = document.getElementById("deal-chat-photo-caption");
1307        var sharedCaptionInput = document.getElementById("deal-chat-upload-shared-caption");
1308        var photoModal = document.getElementById("deal-chat-photos-modal");
1309        var photoGrid = document.getElementById("deal-chat-upload-grid");
1310        var photoCancelBtn = document.getElementById("deal-chat-photo-cancel");
1311        var photoSendBtn = document.getElementById("deal-chat-photo-send");
1312        var sendingStatus = document.getElementById("deal-chat-sending");
1313        var draftKey = form.getAttribute("data-draft-key") || "";
1314        var draftStorage = null;
1315        var draftSaveTimer = null;
1316        var draftClearedForSubmit = false;
1317        var previewUrls = [];
1318        var isSubmitting = false;
1319        if (draftKey) {
1320            try {
1321                draftStorage = window.localStorage || null;
1322            } catch (e) {
1323                draftStorage = null;
1324            }
1325        }
1326        var getTextValue = function() {
1327            return ta ? (ta.value || "").trim() : "";
1328        };
1329        var isTextValid = function() {
1330            return getTextValue().length > 0;
1331        };
1332        var submitForm = function() {
1333            if (typeof form.requestSubmit === "function") {
1334                form.requestSubmit();
1335            } else {
1336                var submitEvent = document.createEvent("Event");
1337                submitEvent.initEvent("submit", true, true);
1338                if (form.dispatchEvent(submitEvent)) {
1339                    form.submit();
1340                }
1341            }
1342        };
1343        var setSubmittingState = function(state) {
1344            isSubmitting = state;
1345            form.dataset.submitting = state ? "1" : "0";
1346            if (ta) {
1347                ta.readOnly = state;
1348            }
1349            if (photoSendBtn) {
1350                photoSendBtn.disabled = state;
1351            }
1352            if (photoCancelBtn) {
1353                photoCancelBtn.disabled = state;
1354            }
1355            if (sendBtn) {
1356                sendBtn.disabled = state || !isTextValid();
1357                sendBtn.classList.toggle("is-loading", state);
1358            }
1359            if (sendingStatus) {
1360                if (state) {
1361                    sendingStatus.textContent = form.getAttribute("data-sending-label") || "Отправка...";
1362                    sendingStatus.classList.add("active");
1363                } else {
1364                    sendingStatus.textContent = "";
1365                    sendingStatus.classList.remove("active");
1366                }
1367            }
1368        };
1369        var updateSendAvailability = function() {
1370            if (!sendBtn || isSubmitting) {
1371                return;
1372            }
1373            sendBtn.disabled = !isTextValid();
1374        };
1375        var saveDraft = function() {
1376            if (draftClearedForSubmit || !draftStorage || !draftKey || !ta) {
1377                return;
1378            }
1379            try {
1380                var value = ta.value || "";
1381                if (value.trim().length > 0) {
1382                    draftStorage.setItem(draftKey, value);
1383                } else {
1384                    draftStorage.removeItem(draftKey);
1385                }
1386            } catch (e) {}
1387        };
1388        var scheduleDraftSave = function() {
1389            window.clearTimeout(draftSaveTimer);
1390            draftSaveTimer = window.setTimeout(saveDraft, 200);
1391        };
1392        var clearDraft = function() {
1393            window.clearTimeout(draftSaveTimer);
1394            draftClearedForSubmit = true;
1395            if (!draftStorage || !draftKey) {
1396                return;
1397            }
1398            try {
1399                draftStorage.removeItem(draftKey);
1400            } catch (e) {}
1401        };
1402        if (ta) {
1403            var autoGrow = function() {
1404                this.style.height = "auto";
1405                this.style.height = Math.min(this.scrollHeight, 160) + "px";
1406            };
1407            if (draftStorage && draftKey && !ta.value) {
1408                try {
1409                    var savedDraft = draftStorage.getItem(draftKey);
1410                    if (savedDraft) {
1411                        ta.value = savedDraft;
1412                    }
1413                } catch (e) {}
1414            }
1415            ta.addEventListener("input", autoGrow);
1416            ta.addEventListener("input", updateSendAvailability);
1417            ta.addEventListener("input", scheduleDraftSave);
1418            ta.addEventListener("keydown", function(e) {
1419                if (e.key !== "Enter" || e.shiftKey || e.isComposing) {
1420                    return;
1421                }
1422                e.preventDefault();
1423                if (isSubmitting || !isTextValid()) {
1424                    if (!isTextValid()) {
1425                        alert(form.getAttribute("data-empty-required-msg") || "Сообщение или фото обязательно");
1426                    }
1427                    return;
1428                }
1429                submitForm();
1430            });
1431            autoGrow.call(ta);
1432            window.addEventListener("pagehide", saveDraft);
1433            document.addEventListener("visibilitychange", function() {
1434                if (document.hidden) {
1435                    saveDraft();
1436                }
1437            });
1438        }
1439        var closePhotoModal = function() {
1440            if (photoModal && window.AppUi) {
1441                window.AppUi.hideModal(photoModal);
1442            }
1443        };
1444        var openPhotoModal = function() {
1445            if (photoModal && window.AppUi) {
1446                window.AppUi.showModal(photoModal);
1447            }
1448        };
1449        var clearPreviewUrls = function() {
1450            previewUrls.forEach(function(url) {
1451                URL.revokeObjectURL(url);
1452            });
1453            previewUrls = [];
1454        };
1455        var resetPhotoSelection = function() {
1456            if (fileInput) {
1457                fileInput.value = "";
1458            }
1459            if (captionInput) {
1460                captionInput.value = "";
1461            }
1462            if (sharedCaptionInput) {
1463                sharedCaptionInput.value = "";
1464            }
1465            if (photoGrid) {
1466                photoGrid.innerHTML = "";
1467            }
1468            form.dataset.photosConfirmed = "0";
1469            clearPreviewUrls();
1470        };
1471        var cancelPhotoSelection = function() {
1472            closePhotoModal();
1473            resetPhotoSelection();
1474        };
1475        var buildPhotoPreview = function(files) {
1476            if (!photoGrid) {
1477                return;
1478            }
1479            photoGrid.innerHTML = "";
1480            clearPreviewUrls();
1481            Array.prototype.forEach.call(files, function(file, idx) {
1482                var url = URL.createObjectURL(file);
1483                previewUrls.push(url);
1484                var item = document.createElement("div");
1485                item.className = "deal-chat-upload__item";
1486                var img = document.createElement("img");
1487                img.className = "deal-chat-upload__thumb";
1488                img.src = url;
1489                img.alt = "photo-" + (idx + 1);
1490                item.appendChild(img);
1491                photoGrid.appendChild(item);
1492            });
1493        };
1494        var storeCaption = function() {
1495            if (!captionInput) {
1496                return;
1497            }
1498            captionInput.value = sharedCaptionInput ? (sharedCaptionInput.value || "").trim() : "";
1499        };
1500        if (fileInput) {
1501            fileInput.addEventListener("change", function() {
1502                if (isSubmitting) {
1503                    this.value = "";
1504                    return;
1505                }
1506                var remaining = parseInt(this.getAttribute("data-remaining") || "0", 10);
1507                if (this.files && this.files.length > remaining) {
1508                    var maxPhotosMsgTpl = form.getAttribute("data-max-photos-msg") || "Можно выбрать не более %count% фото для этого чата";
1509                    alert(maxPhotosMsgTpl.replace("__COUNT__", String(remaining)).replace("%count%", String(remaining)));
1510                    this.value = "";
1511                    return;
1512                }
1513                if (this.files && this.files.length > 0) {
1514                    buildPhotoPreview(this.files);
1515                    openPhotoModal();
1516                }
1517            });
1518        }
1519        form.addEventListener("submit", function(e) {
1520            if (isSubmitting || form.dataset.submitting === "1") {
1521                e.preventDefault();
1522                return;
1523            }
1524            var hasFiles = fileInput && fileInput.files && fileInput.files.length > 0;
1525            if (!isTextValid() && !hasFiles) {
1526                e.preventDefault();
1527                if (ta) {
1528                    ta.focus();
1529                }
1530                alert(form.getAttribute("data-empty-required-msg") || "Сообщение или фото обязательно");
1531                return;
1532            }
1533            if (hasFiles && form.dataset.photosConfirmed !== "1") {
1534                e.preventDefault();
1535                buildPhotoPreview(fileInput.files);
1536                openPhotoModal();
1537                return;
1538            }
1539            if (!hasFiles && captionInput) {
1540                captionInput.value = "";
1541            }
1542            if (hasFiles) {
1543                storeCaption();
1544            }
1545            clearDraft();
1546            setSubmittingState(true);
1547        });
1548        if (photoSendBtn) {
1549            photoSendBtn.addEventListener("click", function() {
1550                if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
1551                    return;
1552                }
1553                if (isSubmitting) {
1554                    return;
1555                }
1556                storeCaption();
1557                form.dataset.photosConfirmed = "1";
1558                closePhotoModal();
1559                submitForm();
1560            });
1561        }
1562        if (sharedCaptionInput) {
1563            sharedCaptionInput.addEventListener("keydown", function(e) {
1564                if (e.key !== "Enter" || e.shiftKey || e.isComposing) {
1565                    return;
1566                }
1567                e.preventDefault();
1568                if (photoSendBtn && !photoSendBtn.disabled) {
1569                    photoSendBtn.click();
1570                }
1571            });
1572        }
1573        if (photoCancelBtn) {
1574            photoCancelBtn.addEventListener("click", cancelPhotoSelection);
1575        }
1576        if (photoModal) {
1577            Array.prototype.forEach.call(photoModal.querySelectorAll("[data-chat-photo-cancel]"), function(el) {
1578                el.addEventListener("click", cancelPhotoSelection);
1579            });
1580            photoModal.addEventListener("hidden.bs.modal", function() {
1581                if (!isSubmitting && form.dataset.photosConfirmed !== "1") {
1582                    resetPhotoSelection();
1583                }
1584            });
1585        }
1586        document.addEventListener("keydown", function(e) {
1587            if (e.key === "Escape" && photoModal && photoModal.classList.contains("open")) {
1588                cancelPhotoSelection();
1589            }
1590        });
1591        updateSendAvailability();
1592    })();
1593    // Лёгкий поллинг чата сделки: новые сообщения появляются без перезагрузки.
1594    // Умный, не «тупой»: пауза в фоне (document.hidden), мгновенный дозапрос при
1595    // возврате к вкладке/окну, защита от наложения запросов, дешёвый эндпоинт.
1596    (function() {
1597        var list = document.getElementById("deal-chat-list");
1598        if (!list) { return; }
1599        var pollUrl = list.getAttribute("data-poll-url");
1600        if (!pollUrl) { return; }
1601        var lastId = parseInt(list.getAttribute("data-last-id"), 10) || 0;
1602        var readTitle = list.getAttribute("data-read-title") || "";
1603        var POLL_MS = 6000;         // вкладка активна — отзывчиво
1604        var POLL_HIDDEN_MS = 60000; // вкладка в фоне — раз в минуту: минимум нагрузки, при возврате догоняем сразу
1605        var pollTimer = null;
1606        var inFlight = false;
1607        // Уведомления о новых сообщениях собеседника: короткий звук + счётчик во вкладке.
1608        var originalTitle = document.title;
1609        var unreadTitle = 0;
1610        var audioCtx = null;
1611        var soundOn = list.getAttribute("data-sound") !== "0";
1612        // Разблокировка аудио по первому жесту пользователя (политика автоплея браузеров).
1613        function unlockAudio() {
1614            try {
1615                var Ctx = window.AudioContext || window.webkitAudioContext;
1616                if (Ctx && !audioCtx) { audioCtx = new Ctx(); }
1617                if (audioCtx && audioCtx.state === "suspended") { audioCtx.resume(); }
1618            } catch (e) {}
1619            document.removeEventListener("pointerdown", unlockAudio);
1620            document.removeEventListener("keydown", unlockAudio);
1621        }
1622        document.addEventListener("pointerdown", unlockAudio);
1623        document.addEventListener("keydown", unlockAudio);
1624        function playDing() {
1625            if (!soundOn) { return; }
1626            try {
1627                var Ctx = window.AudioContext || window.webkitAudioContext;
1628                if (!Ctx) { return; }
1629                if (!audioCtx) { audioCtx = new Ctx(); }
1630                if (audioCtx.state === "suspended") { audioCtx.resume(); }
1631                var now = audioCtx.currentTime;
1632                // Два коротких мягких тона — ненавязчивый «динь».
1633                [[880, 0], [1174.7, 0.09]].forEach(function(pair) {
1634                    var osc = audioCtx.createOscillator();
1635                    var gain = audioCtx.createGain();
1636                    osc.type = "sine";
1637                    osc.frequency.value = pair[0];
1638                    var t = now + pair[1];
1639                    gain.gain.setValueAtTime(0.0001, t);
1640                    gain.gain.exponentialRampToValueAtTime(0.14, t + 0.012);
1641                    gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.19);
1642                    osc.connect(gain);
1643                    gain.connect(audioCtx.destination);
1644                    osc.start(t);
1645                    osc.stop(t + 0.22);
1646                });
1647            } catch (e) {}
1648        }
1649        // Подсветка вкладки: помимо счётчика в заголовке — красный бейдж поверх favicon.
1650        var faviconLinks = Array.prototype.slice.call(
1651            document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]')
1652        );
1653        var faviconOriginal = faviconLinks.map(function(l) { return l.getAttribute("href"); });
1654        var faviconBadged = false;
1655        var dangerColor = (getComputedStyle(document.documentElement)
1656            .getPropertyValue("--ua-danger") || "").trim() || "#CC3F3F";
1657        function setFaviconBadge(count) {
1658            if (!faviconLinks.length) { return; }
1659            try {
1660                var size = 32;
1661                var canvas = document.createElement("canvas");
1662                canvas.width = size;
1663                canvas.height = size;
1664                var ctx = canvas.getContext("2d");
1665                ctx.beginPath();
1666                ctx.arc(size / 2, size / 2, size / 2 - 1, 0, Math.PI * 2);
1667                ctx.fillStyle = dangerColor;
1668                ctx.fill();
1669                var label = count > 9 ? "9+" : String(count);
1670                ctx.fillStyle = "#ffffff";
1671                ctx.font = "bold " + (count > 9 ? 15 : 20) + "px Arial, sans-serif";
1672                ctx.textAlign = "center";
1673                ctx.textBaseline = "middle";
1674                ctx.fillText(label, size / 2, size / 2 + 1);
1675                var url = canvas.toDataURL("image/png");
1676                faviconLinks.forEach(function(l) { l.setAttribute("href", url); });
1677                faviconBadged = true;
1678            } catch (e) {}
1679        }
1680        function restoreFavicon() {
1681            if (!faviconBadged) { return; }
1682            faviconLinks.forEach(function(l, i) {
1683                if (faviconOriginal[i] != null) { l.setAttribute("href", faviconOriginal[i]); }
1684            });
1685            faviconBadged = false;
1686        }
1687        function bumpTitle(n) {
1688            unreadTitle += n;
1689            document.title = "(" + unreadTitle + ") " + originalTitle;
1690            setFaviconBadge(unreadTitle);
1691        }
1692        function resetTitle() {
1693            if (unreadTitle === 0) { restoreFavicon(); return; }
1694            unreadTitle = 0;
1695            document.title = originalTitle;
1696            restoreFavicon();
1697        }
1698        // Считаем только реальные сообщения собеседника (не системные/арбитраж/отзывы).
1699        function countPeerRows(rows) {
1700            var c = 0;
1701            rows.forEach(function(row) {
1702                var cl = row.classList;
1703                if (cl && cl.contains("theirs")
1704                    && !cl.contains("system-notification")
1705                    && !cl.contains("admin-intervention")
1706                    && !cl.contains("review-notification")) {
1707                    c += 1;
1708                }
1709            });
1710            return c;
1711        }
1712        function nearBottom() {
1713            return list.scrollHeight - list.scrollTop - list.clientHeight < 140;
1714        }
1715        function scrollDown() {
1716            list.scrollTop = list.scrollHeight;
1717        }
1718        function appendHtml(html) {
1719            var wasNearBottom = nearBottom();
1720            var empty = list.querySelector(".deal-chat__empty");
1721            if (empty && empty.parentNode) {
1722                empty.parentNode.removeChild(empty);
1723            }
1724            var tmp = document.createElement("div");
1725            tmp.innerHTML = html;
1726            var added = [];
1727            while (tmp.firstChild) {
1728                var node = tmp.firstChild;
1729                list.appendChild(node);
1730                if (node.nodeType === 1) {
1731                    added.push(node);
1732                }
1733            }
1734            if (wasNearBottom) {
1735                scrollDown();
1736            }
1737            added.forEach(function(row) {
1738                Array.prototype.forEach.call(row.querySelectorAll("img"), function(img) {
1739                    if (img.complete) { return; }
1740                    img.addEventListener("load", function() {
1741                        if (wasNearBottom) { scrollDown(); }
1742                    }, { once: true });
1743                });
1744            });
1745            return added;
1746        }
1747        function applyReadReceipts(ids) {
1748            ids.forEach(function(id) {
1749                var row = list.querySelector('.deal-chat__row[data-message-id="' + id + '"]');
1750                if (!row) { return; }
1751                var mark = row.querySelector('.deal-chat__read[data-read="0"]');
1752                if (!mark) { return; }
1753                mark.setAttribute("data-read", "1");
1754                if (readTitle) { mark.setAttribute("title", readTitle); }
1755            });
1756        }
1757        function scheduleNext() {
1758            window.clearTimeout(pollTimer);
1759            // В фоне опрашиваем реже (нагрузка мала), но НЕ выключаем — иначе не будет звука/счётчика.
1760            pollTimer = window.setTimeout(poll, document.hidden ? POLL_HIDDEN_MS : POLL_MS);
1761        }
1762        function pollNow() {
1763            window.clearTimeout(pollTimer);
1764            poll();
1765        }
1766        function poll() {
1767            if (inFlight) { return; }
1768            inFlight = true;
1769            var url = pollUrl + (pollUrl.indexOf("?") === -1 ? "?" : "&") + "after=" + encodeURIComponent(lastId);
1770            fetch(url, {
1771                headers: { "X-Requested-With": "XMLHttpRequest" },
1772                credentials: "same-origin"
1773            }).then(function(resp) {
1774                return resp.ok ? resp.json() : null;
1775            }).then(function(data) {
1776                if (data && data.reload) {
1777                    // Доступ к чату закрылся (например, баÐ
1777»Ð°Ð½Ñ ушёл в минус) — показываем причину.
1778                    window.location.reload();
1779                    return;
1780                }
1781                if (data && data.ok) {
1782                    if (typeof data.lastId === "number" && data.lastId > lastId) {
1783                        lastId = data.lastId;
1784                        list.setAttribute("data-last-id", String(lastId));
1785                    }
1786                    if (data.html) {
1787                        var addedRows = appendHtml(data.html);
1788                        var peerCount = countPeerRows(addedRows);
1789                        if (peerCount > 0) {
1790                            playDing();
1791                            if (document.hidden) { bumpTitle(peerCount); }
1792                        }
1793                    }
1794                    if (data.readReceipts && data.readReceipts.length) {
1795                        applyReadReceipts(data.readReceipts);
1796                    }
1797                }
1798            }).catch(function() {}).then(function() {
1799                inFlight = false;
1800                scheduleNext();
1801            });
1802        }
1803        document.addEventListener("visibilitychange", function() {
1804            if (!document.hidden) { resetTitle(); pollNow(); }
1805            else { scheduleNext(); }
1806        });
1807        window.addEventListener("focus", function() { resetTitle(); pollNow(); });
1808        pollNow();
1809    })();
1810});
1811
1812(function() {
1813    function ready(fn) {
1814        if (document.readyState !== "loading") {
1815            fn();
1816        } else {
1817            document.addEventListener("DOMContentLoaded", fn);
1818        }
1819    }
1820    ready(function() {
1821        var modalEl = document.getElementById("appConfirmModal");
1822        var titleEl = document.getElementById("appConfirmTitle");
1823        var msgEl = document.getElementById("appConfirmMessage");
1824        var okBtn = document.getElementById("appConfirmOkBtn");
1825        var cancelBtn = document.getElementById("appConfirmCancelBtn");
1826        var defaultCancelLabel = cancelBtn ? cancelBtn.textContent.trim() : "Отмена";
1827        var defaultConfirmLabel = okBtn ? okBtn.textContent.replace(/^[^\S\r\n]*[\u2713✔✓\s]*\s*/, "").trim() : "Подтвердить";
1828        function hasBootstrapModal() {
1829            return !!(window.jQuery && typeof jQuery.fn.modal === "function" && modalEl);
1830        }
1831        window.showConfirm = function(opts) {
1832            opts = opts || {};
1833            var title = opts.title || (titleEl ? titleEl.textContent : "Подтверждение");
1834            var messageHtml = opts.messageHtml || "";
1835            var confirmLabel = opts.confirmLabel || defaultConfirmLabel;
1836            var cancelLabel = opts.cancelLabel || defaultCancelLabel;
1837            var variant = opts.variant || "default";
1838            if (!hasBootstrapModal()) {
1839                var plain = messageHtml.replace(/<[^>]+>/g, "");
1840                return Promise.resolve(window.confirm(plain));
1841            }
1842            return new Promise(function(resolve) {
1843                if (titleEl) titleEl.textContent = title;
1844                if (msgEl) msgEl.innerHTML = messageHtml;
1845                if (okBtn) okBtn.innerHTML = '<span class="fa-solid ' + (variant === "danger" ? "fa-trash-can" : "fa-check") + '" aria-hidden="true"></span> ' + confirmLabel;
1846                if (cancelBtn) cancelBtn.textContent = cancelLabel;
1847                modalEl.classList.remove("app-confirm-modal--danger");
1848                if (variant === "danger") {
1849                    modalEl.classList.add("app-confirm-modal--danger");
1850                }
1851                var $m = jQuery(modalEl);
1852                var resolved = false;
1853                function cleanup() {
1854                    $m.off("hidden.bs.modal", onHide);
1855                    if (okBtn) okBtn.removeEventListener("click", onOk);
1856                    modalEl.classList.remove("app-confirm-modal--danger");
1857                }
1858                function onHide() {
1859                    cleanup();
1860                    if (!resolved) resolve(false);
1861                }
1862                function onOk() {
1863                    resolved = true;
1864                    window.AppUi.hideModal($m[0]);
1865                    cleanup();
1866                    resolve(true);
1867                }
1868                $m.on("hidden.bs.modal", onHide);
1869                if (okBtn) okBtn.addEventListener("click", onOk, {
1870                    once: true
1871                });
1872                window.AppUi.showModal($m[0]);
1873            });
1874        };
1875        document.addEventListener("click", function(e) {
1876            var trigger = e.target.closest("[data-confirm]");
1877            if (!trigger) return;
1878            var isButton = trigger.matches('button, input[type="submit"], .btn');
1879            var isLink = trigger.matches("a[href]");
1880            var form = trigger.closest("form");
1881            e.preventDefault();
1882            var message = trigger.getAttribute("data-confirm") || "";
1883            var title = trigger.getAttribute("data-confirm-title") || undefined;
1884            var confirmLabel = trigger.getAttribute("data-confirm-label") || undefined;
1885            var cancelLabel = trigger.getAttribute("data-confirm-cancel-label") || undefined;
1886            window.showConfirm({
1887                title: title,
1888                messageHtml: message,
1889                confirmLabel: confirmLabel,
1890                cancelLabel: cancelLabel
1891            }).then(function(ok) {
1892                if (!ok) return;
1893                if (isLink) {
1894                    window.location.href = trigger.getAttribute("href");
1895                    return;
1896                }
1897                if (form) {
1898                    form.submit();
1899                    return;
1900                }
1901            });
1902        });
1903        document.addEventListener("submit", function(e) {
1904            var form = e.target;
1905            if (!(form instanceof HTMLFormElement)) return;
1906            if (!form.hasAttribute("data-confirm")) return;
1907            e.preventDefault();
1908            var message = form.getAttribute("data-confirm") || "";
1909            var title = form.getAttribute("data-confirm-title") || undefined;
1910            var confirmLabel = form.getAttribute("data-confirm-label") || undefined;
1911            var cancelLabel = form.getAttribute("data-confirm-cancel-label") || undefined;
1912            window.showConfirm({
1913                title: title,
1914                messageHtml: message,
1915                confirmLabel: confirmLabel,
1916                cancelLabel: cancelLabel
1917            }).then(function(ok) {
1918                if (ok) form.submit();
1919            });
1920        });
1921    });
1922})();
1923
1924(function() {
1925    function ready(fn) {
1926        if (document.readyState !== "loading") {
1927            fn();
1928        } else {
1929            document.addEventListener("DOMContentLoaded", fn);
1930        }
1931    }
1932    ready(function() {
1933        var form = document.querySelector(".js-deal-delivery-form");
1934        if (!form) {
1935            return;
1936        }
1937        var method = form.querySelector("#delivery_method");
1938        var paymentGroup = form.querySelector("[data-payment-method-group]");
1939        var paymentInput = form.querySelector("#payment_method");
1940        var npGroup = form.querySelector("#np_branch_group");
1941        var ukrGroup = form.querySelector("#ukrposhta_index_group");
1942        var npInput = form.querySelector("#delivery_np_branch");
1943        var ukrInput = form.querySelector("#delivery_ukrposhta_index");
1944        var submitBtn = form.querySelector(".deal-delivery__submit");
1945        var submitLabel = submitBtn ? submitBtn.querySelector(".deal-delivery__submit-label") : null;
1946        var initialSubmitLabel = submitLabel ? submitLabel.textContent : "";
1947        var savingLabel = form.getAttribute("data-saving-label") || initialSubmitLabel;
1948        if (!method) {
1949            return;
1950        }
1951        function toggleGroup(group, visible) {
1952            if (!group) {
1953                return;
1954            }
1955            group.hidden = !visible;
1956            group.classList.toggle("is-hidden", !visible);
1957            group.setAttribute("aria-hidden", visible ? "false" : "true");
1958        }
1959        function syncVisibility() {
1960            var value = method.value;
1961            var isNp = value === "nova_poshta";
1962            var isUkr = value === "ukrposhta";
1963            var needsPayment = value !== "" && value !== "meetup";
1964            toggleGroup(npGroup, isNp);
1965            toggleGroup(ukrGroup, isUkr);
1966            if (npInput) {
1967                npInput.required = isNp;
1968            }
1969            if (ukrInput) {
1970                ukrInput.required = isUkr;
1971            }
1972            toggleGroup(paymentGroup, needsPayment);
1973            if (paymentInput) {
1974                paymentInput.required = needsPayment;
1975                paymentInput.disabled = !needsPayment;
1976            }
1977        }
1978        method.addEventListener("change", syncVisibility);
1979        syncVisibility();
1980        form.addEventListener("submit", function() {
1981            if (!submitBtn || submitBtn.classList.contains("is-loading")) {
1982                return;
1983            }
1984            submitBtn.classList.add("is-loading");
1985            submitBtn.disabled = true;
1986            if (submitLabel && savingLabel) {
1987                submitLabel.textContent = savingLabel;
1988            }
1989        });
1990    });
1991})();
1992
1993/* Оценка сделки (deals/{id}/review): состояние по выбранной оценке, быстрые фразы, счётчик, проверка перед отправкой.
1994   Вид (панель негатива, набор фраз, подписи кнопки) переключается CSS по data-score формы. */
1995(function() {
1996    function ready(fn) {
1997        if (document.readyState !== "loading") {
1998            fn();
1999        } else {
2000            document.addEventListener("DOMContentLoaded", fn);
2001        }
2002    }
2003    function escapeRegExp(value) {
2004        return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2005    }
2006    // Длина в символах, как её считает сервер (mb_strlen), а не в UTF-16-единицах.
2007    function charLength(value) {
2008        return Array.from(value).length;
2009    }
2010    ready(function() {
2011        var form = document.querySelector(".js-deal-review");
2012        if (!form) {
2013            return;
2014        }
2015        var radios = form.querySelectorAll(".js-review-score");
2016        var text = form.querySelector(".js-review-text");
2017        var chips = form.querySelectorAll(".js-review-chip");
2018        var counter = form.querySelector(".js-review-counter");
2019        var requirement = form.querySelector(".js-review-requirement");
2020        var live = form.querySelector(".js-review-live");
2021        var scoreField = form.querySelector(".deal-review__field--score");
2022        var scoreError = form.querySelector(".js-review-score-error");
2023        var textError = form.querySelector(".js-review-text-error");
2024        var submitBtn = form.querySelector(".js-review-submit");
2025        if (!text || !submitBtn || !radios.length) {
2026            return;
2027        }
2028        var maxLength = parseInt(text.getAttribute("maxlength"), 10) || 255;
2029        var negativeMin = parseInt(form.getAttribute("data-negative-min-length"), 10) || 0;
2030        var defaultPlaceholder = text.getAttribute("data-placeholder-default") || "";
2031        var lastAnnouncedRemaining = null;
2032
2033        // Проверку ведём сами: нативные пузыри не умеют «обязательно только для негатива».
2034        form.noValidate = true;
2035        form.classList.add("is-enhanced");
2036
2037        function checkedRadio() {
2038            for (var i = 0; i < radios.length; i++) {
2039                if (radios[i].checked) {
2040                    return radios[i];
2041                }
2042            }
2043            return null;
2044        }
2045        function scoreKey() {
2046            var radio = checkedRadio();
2047            return radio ? radio.getAttribute("data-key") : "";
2048        }
2049        function showError(box, message, target) {
2050            if (!box) {
2051                return;
2052            }
2053            var textNode = box.querySelector(".js-review-error-text");
2054            if (textNode) {
2055                textNode.textContent = message;
2056            }
2057            box.hidden = false;
2058            if (target) {
2059                target.setAttribute("aria-describedby", box.id);
2060            }
2061            if (box.parentElement) {
2062                box.parentElement.classList.add("is-invalid");
2063            }
2064        }
2065        function hideError(box, target) {
2066            if (!box || box.hidden) {
2067                return;
2068            }
2069            box.hidden = true;
2070            if (target) {
2071                target.removeAttribute("aria-describedby");
2072            }
2073            if (box.parentElement) {
2074                box.parentElement.classList.remove("is-invalid");
2075            }
2076        }
2077        function negativeTextMissing() {
2078            return scoreKey() === "neg" && charLength(text.value.trim()) < negativeMin;
2079        }
2080        function syncChips() {
2081            Array.prototype.forEach.call(chips, function(chip) {
2082                var phrase = chip.getAttribute("data-phrase") || "";
2083                chip.setAttribute("aria-pressed", phrase && text.value.indexOf(phrase) !== -1 ? "true" : "false");
2084            });
2085        }
2086        function syncCounter() {
2087            var length = text.value.length;
2088            var remaining = maxLength - length;
2089            if (counter) {
2090                counter.textContent = length + " / " + maxLength;
2091                counter.classList.toggle("is-near", remaining <= 20 && remaining > 0);
2092                counter.classList.toggle("is-full", remaining <= 0);
2093            }
2094            if (requirement) {
2095                requirement.classList.toggle("is-met", charLength(text.value.trim()) >= negativeMin);
2096            }
2097            // Скринридеру сообщаем только о приближении к пределу, а не о каждом символе.
2098            var bucket = remaining <= 0 ? 0 : (remaining <= 20 ? 20 : null);
2099            if (live && bucket !== lastAnnouncedRemaining) {
2100                lastAnnouncedRemaining = bucket;
2101                if (bucket !== null) {
2102                    live.textContent = (form.getAttribute("data-live-remaining") || "").replace("%count%", String(Math.max(remaining, 0)));
2103                }
2104            }
2105        }
2106        function syncScore() {
2107            var radio = checkedRadio();
2108            var key = radio ? radio.getAttribute("data-key") : "";
2109            form.setAttribute("data-score", key);
2110            text.placeholder = radio ? (radio.getAttribute("data-placeholder") || defaultPlaceholder) : defaultPlaceholder;
2111            text.required = key === "neg";
2112            if (key) {
2113                hideError(scoreError, scoreField);
2114            }
2115            if (!negativeTextMissing()) {
2116                hideError(textError, text);
2117                text.removeAttribute("aria-invalid");
2118            }
2119        }
2120        function addPhrase(value, phrase) {
2121            var base = value.replace(/\s+$/, "");
2122            if (!base) {
2123                return phrase + ".";
2124            }
2125            return base + (/[.!?…]$/.test(base) ? " " : ". ") + phrase + ".";
2126        }
2127        function removePhrase(value, phrase) {
2128            return value
2129                .replace(new RegExp("\\s*" + escapeRegExp(phrase) + "[.!?…]?"), "")
2130                .replace(/^\s+/, "");
2131        }
2132
2133        Array.prototype.forEach.call(radios, function(radio) {
2134            radio.addEventListener("change", syncScore);
2135        });
2136
2137        Array.prototype.forEach.call(chips, function(chip) {
2138            chip.addEventListener("click", function() {
2139                var phrase = chip.getAttribute("data-phrase") || "";
2140                if (!phrase) {
2141                    return;
2142                }
2143                var next = text.value.indexOf(phrase) !== -1 ? removePhrase(text.value, phrase) : addPhrase(text.value, phrase);
2144                if (next.length > maxLength) {
2145                    if (live) {
2146                        live.textContent = form.getAttribute("data-error-no-room") || "";
2147                    }
2148                    if (counter) {
2149                        counter.classList.remove("is-shake");
2150                        void counter.offsetWidth;
2151                        counter.classList.add("is-shake");
2152                    }
2153                    return;
2154                }
2155                text.value = next;
2156                text.dispatchEvent(new Event("input", { bubbles: true }));
2157            });
2158        });
2159
2160        text.addEventListener("input", function() {
2161            syncChips();
2162            syncCounter();
2163            if (!negativeTextMissing()) {
2164                hideError(textError, text);
2165                text.removeAttribute("aria-invalid");
2166            }
2167        });
2168
2169        form.addEventListener("submit", function(event) {
2170            if (submitBtn.classList.contains("is-loading")) {
2171                event.preventDefault();
2172                return;
2173            }
2174            if (!scoreKey()) {
2175                event.preventDefault();
2176                showError(scoreError, form.getAttribute("data-error-score") || "", scoreField);
2177                radios[0].focus();
2178                return;
2179            }
2180            if (negativeTextMissing()) {
2181                event.preventDefault();
2182                showError(textError, form.getAttribute("data-error-negative") || "", text);
2183                text.setAttribute("aria-invalid", "true");
2184                text.focus();
2185                return;
2186            }
2187            submitBtn.classList.add("is-loading");
2188            submitBtn.setAttribute("aria-busy", "true");
2189            // Кнопку блокируем после начала отправки, чтобы форма успела уйти.
2190            window.setTimeout(function() {
2191                submitBtn.disabled = true;
2192            }, 0);
2193        });
2194
2195        // Возврат «назад» из истории поднимает страницу из bfcache с зависшей загрузкой — сбрасываем.
2196        window.addEventListener("pageshow", function(event) {
2197            if (event.persisted) {
2198                submitBtn.classList.remove("is-loading");
2199                submitBtn.removeAttribute("aria-busy");
2200                submitBtn.disabled = false;
2201            }
2202        });
2203
2204        syncScore();
2205        syncChips();
2206        syncCounter();
2207    });
2208})();
2209
2210function removeComment(el) {
2211    var comment_id = el.data("comment-id");
2212    var $container = el.closest(".comments-container");
2213    stopDeletedCommentCountdown(comment_id);
2214    $.ajax({
2215        type: "POST",
2216        url: $container.data("remove-url") || "/comments/remove-comment",
2217        data: {
2218            comment_id: comment_id,
2219            locale: getCommentLocale(),
2220            _token: $container.data("token") || ""
2221        },
2222        dataType: "json",
2223        success: function(response) {
2224            if (response.success) {
2225                replaceCommentBlockFromHtml(comment_id, response.html, "comment-updated");
2226            } else if (response && response.msg) {
2227                alert(response.msg);
2228            }
2229        }
2230    });
2231}
2232
2233function getCommentI18n(key, fallbackValue) {
2234    var i18n = $("#comment-i18n");
2235    if (!i18n.length) {
2236        return fallbackValue;
2237    }
2238    return i18n.data(key) || fallbackValue;
2239}
2240
2241function getCommentLocale() {
2242    return getCommentI18n("locale", window.AppLocale || "ru");
2243}
2244
2245function replaceCommentBlockFromHtml(commentId, html, className) {
2246    if (!html) {
2247        return;
2248    }
2249    stopDeletedCommentCountdown(commentId);
2250    var $new = $("<div/>").html($.trim(html)).children().first();
2251    if (!$new.length) {
2252        return;
2253    }
2254    if (className) {
2255        $new.addClass(className);
2256        $new.on("animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", function() {
2257            $(this).removeClass(className);
2258        });
2259    }
2260    var $current = $('.comments_block .comments[data-comment-id="' + commentId + '"]');
2261    if ($current.length) {
2262        $current.replaceWith($new);
2263        window.AppUi.initTooltips($new[0]);
2264        if ($new.hasClass("comments-deleted")) {
2265            startDeletedCommentCountdown($new, commentId);
2266        }
2267    }
2268}
2269
2270var commentDeleteCountdownTimers = {};
2271
2272function stopDeletedCommentCountdown(commentId) {
2273    var id = String(commentId || "");
2274    if (!id) {
2275        return;
2276    }
2277    if (commentDeleteCountdownTimers[id]) {
2278        clearInterval(commentDeleteCountdownTimers[id]);
2279        delete commentDeleteCountdownTimers[id];
2280    }
2281}
2282
2283function startDeletedCommentCountdown($comment, commentId) {
2284    if (!$comment || !$comment.length) {
2285        return;
2286    }
2287    var id = String(commentId || $comment.data("comment-id") || "");
2288    if (!id) {
2289        return;
2290    }
2291    stopDeletedCommentCountdown(id);
2292    var $line = $comment.find(".comment-deleted-line").first();
2293    if (!$line.length) {
2294        return;
2295    }
2296    var $counter = $line.find(".comment-delete-countdown");
2297    if (!$counter.length) {
2298        $counter = $('<span class="comment-delete-countdown" aria-live="polite"></span>');
2299        $line.append($counter);
2300    }
2301    var secondsLeft = 9;
2302    $counter.text(secondsLeft);
2303    commentDeleteCountdownTimers[id] = setInterval(function() {
2304        secondsLeft -= 1;
2305        if (secondsLeft < 0) {
2306            stopDeletedCommentCountdown(id);
2307            $comment.slideUp(180, function() {
2308                $(this).remove();
2309            });
2310            return;
2311        }
2312        $counter.text(secondsLeft);
2313    }, 1e3);
2314}
2315
2316function openInlineCommentEditor(commentBlock) {
2317    if (!commentBlock || !commentBlock.length) {
2318        return;
2319    }
2320    if (commentBlock.find(".comment-inline-editor-wrap").length) {
2321        return;
2322    }
2323    var textNode = commentBlock.find(".comment-text-content").first();
2324    if (!textNode.length) {
2325        return;
2326    }
2327    // data-raw хранит исходный текст с маркерами/переносами строк; иначе .text()
2328    // потеряет форматирование (теги) и переносы (<br>) при редактировании.
2329    var rawAttr = textNode.attr("data-raw");
2330    var textRaw = typeof rawAttr === "string" ? rawAttr : $.trim(textNode.text());
2331    var votes = commentBlock.find(".comment-vote-box");
2332    textNode.hide();
2333    votes.hide();
2334    var editorFormatAttr = getCommentI18n("can-format", "") ? ' data-format-enabled="1"' : "";
2335    var editorHtml = [ '<div class="comment-inline-editor-wrap">', '<textarea class="comment-inline-editor form-control" maxlength="2000"' + editorFormatAttr + "></textarea>", '<div class="comment-inline-editor-actions">', '<button type="button" class="btn btn-outline-secondary btn-sm comment-edit-cancel">' + getCommentI18n("edit-cancel", "Отмена") + "</button>", '<button type="button" class="btn btn-primary btn-sm comment-edit-save">' + getCommentI18n("edit-save", "Сохранить") + "</button>", "</div>", '<div class="comment-inline-editor-error"></div>', "</div>" ].join("");
2336    var editorWrap = $(editorHtml);
2337    textNode.after(editorWrap);
2338    var editorEl = editorWrap.find(".comment-inline-editor").val(textRaw).get(0);
2339    if (editorEl) {
2340        // Премиум: превращаем в WYSIWYG-редактор; иначе обычный textarea.
2341        var rich = window.CommentEditor ? window.CommentEditor.enhance(editorEl) : null;
2342        if (rich) {
2343            rich.focus();
2344            // каретка в конец
2345            var range = document.createRange();
2346            range.selectNodeContents(rich);
2347            range.collapse(false);
2348            var sel = window.getSelection();
2349            sel.removeAllRanges();
2350            sel.addRange(range);
2351        } else {
2352            editorEl.focus();
2353            var caret = editorEl.value.length;
2354            editorEl.setSelectionRange(caret, caret);
2355        }
2356    }
2357}
2358
2359function saveInlineCommentEdit(commentBlock) {
2360    if (!commentBlock || !commentBlock.length) {
2361        return;
2362    }
2363    var commentId = commentBlock.data("comment-id");
2364    var $container = commentBlock.closest(".comments-container");
2365    var editorWrap = commentBlock.find(".comment-inline-editor-wrap");
2366    var textarea = editorWrap.find(".comment-inline-editor");
2367    var saveBtn = editorWrap.find(".comment-edit-save");
2368    var errorBox = editorWrap.find(".comment-inline-editor-error");
2369    var text = $.trim(textarea.val() || "");
2370    if (!text) {
2371        errorBox.text(getCommentI18n("edit-required", "Введите текст сообщения"));
2372        textarea.focus();
2373        return;
2374    }
2375    if (saveBtn.prop("disabled")) {
2376        return;
2377    }
2378    saveBtn.prop("disabled", true).addClass("is-loading");
2379    errorBox.text("");
2380    $.ajax({
2381        type: "POST",
2382        url: $container.data("edit-url") || "/comments/edit-comment",
2383        data: {
2384            comment_id: commentId,
2385            text: text,
2386            locale: getCommentLocale(),
2387            _token: $container.data("token") || ""
2388        },
2389        dataType: "json",
2390        success: function(response) {
2391            if (response && response.success) {
2392                replaceCommentBlockFromHtml(commentId, response.html, "comment-updated");
2393            } else {
2394                errorBox.text(response && response.msg ? response.msg : getCommentI18n("edit-save-failed", "Не удалось сохранить комментарий"));
2395            }
2396        },
2397        error: function(xhr) {
2398            var msg = getCommentI18n("edit-save-failed", "Не удалось сохранить комментарий");
2399            if (xhr && xhr.responseJSON && xhr.responseJSON.msg) {
2400                msg = xhr.responseJSON.msg;
2401            }
2402            errorBox.text(msg);
2403        },
2404        complete: function() {
2405            saveBtn.prop("disabled", false).removeClass("is-loading");
2406        }
2407    });
2408}
2409
2410function reportError(el) {
2411    var url = el.find("#report-url").val();
2412    var type = el.find("#report-type").val();
2413    var text = el.find("#report-text").val();
2414    el.find("#report-error").prop("disabled", true).addClass("is-loading");
2415    $.ajax({
2416        type: "POST",
2417        url: "/comments/report-error",
2418        data: {
2419            url: url,
2420            type: type,
2421            text: text
2422        },
2423        dataType: "json",
2424        success: function(response) {
2425            if (response.result) {
2426                el.hide();
2427                var container = el.closest(".modal-content");
2428                if (container && container.length) {
2429                    container.find("#report-success").css("display", "flex");
2430                }
2431                var modal = el.closest(".modal");
2432                if (modal && modal.length) {
2433                    modal.off("hidden.bs.modal.reportReset");
2434                    modal.on("hidden.bs.modal.reportReset", function() {
2435                        el.show();
2436                        var cont = el.closest(".modal-content");
2437                        if (cont && cont.length) {
2438                            cont.find("#report-success").hide();
2439                        }
2440                        el.find("#report-text").val("");
2441                        el.find("#report-error").prop("disabled", false).removeClass("is-loading");
2442                    });
2443                }
2444            } else {
2445                el.find("#report-error").prop("disabled", false).removeClass("is-loading");
2446            }
2447        },
2448        error: function() {
2449            el.find("#report-error").prop("disabled", false).removeClass("is-loading");
2450        }
2451    });
2452}
2453
2454function likeComment(el) {
2455    var comment_block = el.closest(".comment_text");
2456    var comment_id = comment_block.attr("id");
2457    var type = "like";
2458    if (el.hasClass("dislike_comment")) {
2459        type = "dislike";
2460    }
2461    $.ajax({
2462        type: "POST",
2463        url: "/comments/like-comment",
2464        data: {
2465            comment_id: comment_id,
2466            type: type
2467        },
2468        dataType: "json",
2469        success: function(response) {
2470            if (response.result) {
2471                el.addClass("like-button");
2472                comment_block.find(".sum_comment_vote").text(response.sum_vote);
2473                comment_block.find(".like-button").removeClass("selected_vote");
2474                if (response.vote == 1) {
2475                    comment_block.find(".like_comment").addClass("selected_vote");
2476                } else if (response.vote == -1) {
2477                    comment_block.find(".dislike_comment").addClass("selected_vote");
2478                }
2479            }
2480        }
2481    });
2482}
2483
2484document.addEventListener("click", e => {
2485    const wrapper = e.target.closest(".premium-star-wrapper");
2486    document.querySelectorAll(".premium-star-wrapper.is-premium-hint-open").forEach(el => el.classList.remove("is-premium-hint-open"));
2487    if (wrapper) {
2488        wrapper.classList.add("is-premium-hint-open");
2489        setTimeout(() => wrapper.classList.remove("is-premium-hint-open"), 1200);
2490    }
2491});
2492
2493(function() {
2494    "use strict";
2495    function formatGrn(cents) {
2496        return (cents / 100).toLocaleString("ru-RU", {
2497            minimumFractionDigits: 2,
2498            maximumFractionDigits: 2
2499        });
2500    }
2501    function initAuctionForm() {
2502        var auctionFields = document.getElementById("auction-fields");
2503        if (!auctionFields) {
2504            return;
2505        }
2506        var typeRadios = document.querySelectorAll('input[name$="[listing_type]"]');
2507        var modeRadios = document.querySelectorAll('input[name$="[auction_duration_mode]"]');
2508        var fixedOnly = document.querySelectorAll("[data-fixed-only]");
2509        var presetBlock = auctionFields.querySelector("[data-duration-preset]");
2510        var customBlock = auctionFields.querySelector("[data-duration-custom]");
2511        var startInput = auctionFields.querySelector("[data-auction-start]");
2512        var reserveInput = auctionFields.querySelector("[data-auction-reserve]");
2513        var stepInput = auctionFields.querySelector('input[name$="[auction_price_step]"]');
2514        var endsInput = auctionFields.querySelector("[data-auction-ends]");
2515        var summary = document.getElementById("auction-charge-summary");
2516        var theForm = auctionFields.closest("form");
2517        var priceInput = document.querySelector('input[name$="[price]"]');
2518        var qtyInput = document.querySelector('input[name$="[quantity]"]');
2519        var balanceCents = parseInt(auctionFields.getAttribute("data-balance-cents"), 10) || 0;
2520        var customFeeCents = parseInt(auctionFields.getAttribute("data-custom-fee-cents"), 10) || 100;
2521        var locked = auctionFields.getAttribute("data-locked") === "1";
2522        function selectedType() {
2523            var checked = document.querySelector('input[name$="[listing_type]"]:checked');
2524            return checked ? checked.value : "fixed_price";
2525        }
2526        function selectedMode() {
2527            var checked = document.querySelector('input[name$="[auction_duration_mode]"]:checked');
2528            return checked ? checked.value : "preset";
2529        }
2530        function updateChargeSummary() {
2531            if (!summary) {
2532                return;
2533            }
2534            var customDate = selectedMode() === "custom";
2535            var reserveGrn = reserveInput ? parseInt(reserveInput.value, 10) || 0 : 0;
2536            var totalCents = (customDate ? customFeeCents : 0) + Math.round(reserveGrn * 100 * .01);
2537            if (selectedType() !== "auction" || totalCents <= 0) {
2538                summary.classList.add("is-hidden");
2539                summary.innerHTML = "";
2540                return;
2541            }
2542            summary.classList.remove("is-hidden");
2543            var insufficient = totalCents > balanceCents;
2544            var html = '<div class="auction-charge-summary__row"><span>' + summary.getAttribute("data-label-total") + ":</span> <strong>" + formatGrn(totalCents) + " грн.</strong></div>";
2545            html += '<div class="auction-charge-summary__row"><span>' + summary.getAttribute("data-label-balance") + ":</span> " + formatGrn(balanceCents) + " грн.</div>";
2546            if (insufficient) {
2547                html += '<div class="auction-charge-summary__warn">' + summary.getAttribute("data-label-insufficient") + "</div>";
2548            }
2549            summary.innerHTML = html;
2550            summary.classList.toggle("is-insufficient", insufficient);
2551        }
2552        function applyType() {
2553            var isAuction = selectedType() === "auction";
2554            auctionFields.classList.toggle("is-hidden", !isAuction);
2555            fixedOnly.forEach(function(el) {
2556                el.classList.toggle("is-hidden", isAuction);
2557            });
2558            if (isAuction) {
2559                if (qtyInput) {
2560                    qtyInput.value = 1;
2561                }
2562                syncPriceFromStart();
2563            }
2564            updateChargeSummary();
2565        }
2566        function applyMode() {
2567            var custom = selectedMode() === "custom";
2568            if (presetBlock) {
2569                presetBlock.classList.toggle("is-hidden", custom);
2570            }
2571            if (customBlock) {
2572                customBlock.classList.toggle("is-hidden", !custom);
2573            }
2574            updateChargeSummary();
2575        }
2576        function syncPriceFromStart() {
2577            if (selectedType() !== "auction" || !priceInput || !startInput) {
2578                return;
2579            }
2580            var v = parseInt(startInput.value, 10);
2581            priceInput.value = v && v > 0 ? v : 1;
2582        }
2583        typeRadios.forEach(function(r) {
2584            r.addEventListener("change", applyType);
2585        });
2586        if (!locked) {
2587            modeRadios.forEach(function(r) {
2588                r.addEventListener("change", applyMode);
2589            });
2590            if (startInput) {
2591                startInput.addEventListener("input", function() {
2592                    syncPriceFromStart();
2593                    updateChargeSummary();
2594                });
2595            }
2596            if (reserveInput) {
2597                reserveInput.addEventListener("input", updateChargeSummary);
2598            }
2599            if (endsInput) {
2600                endsInput.addEventListener("click", function() {
2601                    if (typeof endsInput.showPicker === "function") {
2602                        try {
2603                            endsInput.showPicker();
2604                        } catch (err) {}
2605                    }
2606                });
2607            }
2608        }
2609        applyType();
2610        applyMode();
2611    }
2612    function initAuctionTimer() {
2613        var card = document.querySelector("[data-auction]");
2614        if (!card) {
2615            return;
2616        }
2617        var endsAtRaw = card.getAttribute("data-ends-at");
2618        var timerEl = card.querySelector("[data-auction-timer]");
2619        if (!endsAtRaw || !timerEl) {
2620            return;
2621        }
2622        if (isNaN(new Date(endsAtRaw).getTime())) {
2623            return;
2624        }
2625        var endedLabel = card.getAttribute("data-auction-ended-label") || "";
2626        var timeUpSent = false;
2627        function tick() {
2628            // Панель уже сменилась итогом торгов — таймеру больше нечего показывать.
2629            if (!timerEl.isConnected) {
2630                return;
2631            }
2632            // Атрибут читаем каждый тик: антиснайпинг мог продлить торги,
2633            // и живое обновление уже положило сюда новое время.
2634            var endsAt = new Date(card.getAttribute("data-ends-at")).getTime();
2635            if (isNaN(endsAt)) {
2636                return;
2637            }
2638            var diff = endsAt - Date.now();
2639            if (diff <= 0) {
2640                timerEl.textContent = endedLabel || "00:00:00";
2641                // Время вышло: пусть живое обновление сразу спросит итог, не дожидаясь своего опроса.
2642                if (!timeUpSent) {
2643                    timeUpSent = true;
2644                    card.dispatchEvent(new CustomEvent("auction:timeup"));
2645                }
2646                window.setTimeout(tick, 1e3);
2647                return;
2648            }
2649            timeUpSent = false;
2650            var d = Math.floor(diff / 864e5);
2651            var h = Math.floor(diff % 864e5 / 36e5);
2652            var m = Math.floor(diff % 36e5 / 6e4);
2653            var s = Math.floor(diff % 6e4 / 1e3);
2654            function pad(n) {
2655                return n < 10 ? "0" + n : "" + n;
2656            }
2657            timerEl.textContent = (d > 0 ? d + "д " : "") + pad(h) + ":" + pad(m) + ":" + pad(s);
2658            window.setTimeout(tick, 1e3);
2659        }
2660        tick();
2661    }
2662    // Живое состояние аукциона: цена, число ставок, таймер и зона действия
2663    // (панель лидера / форма ставки) обновляются сами, как чат сделки.
2664    // Опрос дешёвый: сервер отдаёт ключ состояния и, пока он не изменился,
2665    // не рендерит разметку вовсе.
2666    function initAuctionLiveState() {
2667        var card = document.querySelector("[data-auction]");
2668        var url = card ? card.getAttribute("data-auction-state-url") : "";
2669        if (!url) {
2670            return;
2671        }
2672        // У завершённых торгов цены и формы ставки уже нет — только панель итога.
2673        var priceBlock = card.querySelector(".auction-lot-panel__price");
2674        var btn = priceBlock ? priceBlock.querySelector("[data-auction-refresh]") : null;
2675        var priceEl = priceBlock ? priceBlock.querySelector("[data-auction-price]") : null;
2676        var bidsEl = priceBlock ? priceBlock.querySelector("[data-auction-bids]") : null;
2677        var captionEl = priceBlock ? priceBlock.querySelector("[data-auction-caption]") : null;
2678        var reserveEl = priceBlock ? priceBlock.querySelector("[data-auction-reserve]") : null;
2679        var actionEl = document.querySelector("[data-auction-action]");
2680        var noteEl = document.querySelector("[data-auction-live-note]");
2681        var noteTextEl = noteEl ? noteEl.querySelector("[data-auction-live-note-text]") : null;
2682        var noteApplyBtn = noteEl ? noteEl.querySelector("[data-auction-live-apply]") : null;
2683        var announcer = document.querySelector("[data-auction-announcer]");
2684
2685        var POLL_MS = 5000;          // вкладка активна — обновляем раз в 5 секунд
2686        var POLL_HIDDEN_MS = 60000;  // в фоне реже: лот сам себя не торгует
2687        var POLL_CLOSING_MS = 30000; // итог уже показан, ждём, пока крон закроет торги и создаст сделку
2688        var SPIN_MS = 600;           // минимальное время вращения, чтобы обновление было заметно
2689
2690        var pollTimer = null;
2691        var inFlight = false;
2692        var stopped = false;
2693        // Ключ состояния, с которым отрисована страница: первый опрос не перерисует то же самое.
2694        var stateKey = card.getAttribute("data-auction-state-key") || "";
2695        var pendingHtml = null;
2696        var awaitingClose = !!card.querySelector("[data-auction-result]");
2697
2698        function setSpinning(on) {
2699            if (btn) {
2700                btn.classList.toggle("is-refreshing", on);
2701            }
2702        }
2703
2704        // Пока человек вводит ставку, подменять форму под руками Ð
2704½ÐµÐ»ÑŒÐ·Ñ.
2705        function actionBusy() {
2706            if (!actionEl) {
2707                return false;
2708            }
2709            var active = document.activeElement;
2710            if (active && actionEl.contains(active)) {
2711                return true;
2712            }
2713            if (actionEl.querySelector(".auction-bid-confirm-panel")) {
2714                return true;
2715            }
2716            var inputs = actionEl.querySelectorAll('input[name="amount"], input[name="max_amount"]');
2717            for (var i = 0; i < inputs.length; i++) {
2718                if (inputs[i].getAttribute("data-touched") === "1") {
2719                    return true;
2720                }
2721            }
2722            return false;
2723        }
2724
2725        function markTouchedOnInput() {
2726            if (!actionEl) {
2727                return;
2728            }
2729            actionEl.addEventListener("input", function(e) {
2730                var target = e.target;
2731                if (target && target.name && (target.name === "amount" || target.name === "max_amount")) {
2732                    target.setAttribute("data-touched", "1");
2733                }
2734            });
2735        }
2736
2737        function currentFormMin() {
2738            if (!actionEl) {
2739                return 0;
2740            }
2741            var input = actionEl.querySelector('input[name="amount"]');
2742            if (!input) {
2743                return 0;
2744            }
2745            var raw = input.getAttribute("data-bid-min") || input.getAttribute("min") || input.value;
2746            var num = parseInt(raw, 10);
2747            return isNaN(num) ? 0 : num;
2748        }
2749
2750        function hideNote() {
2751            if (!noteEl) {
2752                return;
2753            }
2754            noteEl.hidden = true;
2755            noteEl.classList.add("is-hidden");
2756        }
2757
2758        function showNote(minNextBid) {
2759            if (!noteEl || !noteTextEl) {
2760                return;
2761            }
2762            var tpl = noteEl.getAttribute("data-note-template") || "Ставки изменились: минимальная ставка %min% грн";
2763            noteTextEl.textContent = tpl.replace("%min%", Number(minNextBid || 0).toLocaleString("ru-RU"));
2764            noteEl.hidden = false;
2765            noteEl.classList.remove("is-hidden");
2766        }
2767
2768        function applyActionHtml(html) {
2769            if (!actionEl || !html) {
2770                return;
2771            }
2772            actionEl.innerHTML = html;
2773            pendingHtml = null;
2774            hideNote();
2775            initAuctionBidStepper();
2776        }
2777
2778        // Торги завершились: итог целиком заменяет панель — таймер, цену, форму ставки, избранное.
2779        // Форму не бережём даже под руками: ставку на завершённый лот всё равно не принять.
2780        function showResult(html) {
2781            var tpl = document.createElement("template");
2782            tpl.innerHTML = html;
2783            var next = tpl.content.querySelector("[data-auction-result]");
2784            var current = card.querySelector("[data-auction-result]");
2785            // Крон закрыл торги, но для этого зрителя в панели ничего не поменялось — не мигаем и не объявляем повторно.
2786            if (next && current && next.getAttribute("data-result-key") === current.getAttribute("data-result-key")) {
2787                return;
2788            }
2789            card.innerHTML = html;
2790            card.classList.add("auction-lot-panel--finished");
2791            pendingHtml = null;
2792            var result = card.querySelector("[data-auction-result]");
2793            if (!result) {
2794                return;
2795            }
2796            result.classList.add("is-revealed");
2797            if (announcer) {
2798                announcer.textContent = result.getAttribute("data-announce") || "";
2799            }
2800        }
2801
2802        function applyState(data) {
2803            if (typeof data.resultHtml === "string" && data.resultHtml) {
2804                showResult(data.resultHtml);
2805                awaitingClose = !!data.awaitingClose;
2806                if (!awaitingClose) {
2807                    stop();
2808                }
2809                return;
2810            }
2811            if (priceEl && data.priceFormatted) {
2812                var nextPrice = data.priceFormatted;
2813                if (nextPrice !== priceEl.textContent) {
2814                    priceEl.textContent = nextPrice;
2815                    priceEl.classList.remove("is-bumped");
2816                    void priceEl.offsetWidth;
2817                    priceEl.classList.add("is-bumped");
2818                }
2819            }
2820            if (bidsEl && typeof data.bidsText === "string") {
2821                bidsEl.textContent = "(" + data.bidsText + ")";
2822            }
2823            if (captionEl && data.caption) {
2824                captionEl.textContent = data.caption;
2825            }
2826            // Про резерв говорим только после завершения торгов и только если он не достигнут:
2827            // текст даёт сервер, здесь достаточно показать или спрятать заметку.
2828            if (reserveEl) {
2829                var reserveVisible = !!data.reserveText;
2830                reserveEl.textContent = reserveVisible ? data.reserveText : "";
2831                reserveEl.hidden = !reserveVisible;
2832                reserveEl.classList.toggle("is-hidden", !reserveVisible);
2833            }
2834            // Антиснайпинг мог сдвинуть окончание — таймер читает атрибут на каждом тике.
2835            if (card && data.endsAt) {
2836                card.setAttribute("data-ends-at", data.endsAt);
2837            }
2838
2839            if (typeof data.actionHtml === "string") {
2840                if (actionBusy()) {
2841                    // Форму держит человек: подменять её нельзя. Но дёргать его заметкой стоит
2842                    // только тогда, когда выросла минимальная ставка — иначе его сумма всё ещё
2843                    // валидна, и свежая форма просто ждёт свободного момента.
2844                    pendingHtml = data.actionHtml;
2845                    var nextMin = parseInt(data.minNextBid, 10);
2846                    if (!isNaN(nextMin) && nextMin > currentFormMin()) {
2847                        showNote(nextMin);
2848                    } else {
2849                        hideNote();
2850                    }
2851                } else {
2852                    applyActionHtml(data.actionHtml);
2853                }
2854            }
2855
2856            if (data.ended && !data.awaitingClose) {
2857                stop();
2858            }
2859        }
2860
2861        function scheduleNext() {
2862            if (stopped) {
2863                return;
2864            }
2865            window.clearTimeout(pollTimer);
2866            pollTimer = window.setTimeout(poll, document.hidden ? POLL_HIDDEN_MS : (awaitingClose ? POLL_CLOSING_MS : POLL_MS));
2867        }
2868
2869        function stop() {
2870            stopped = true;
2871            window.clearTimeout(pollTimer);
2872        }
2873
2874        function poll() {
2875            if (inFlight || stopped) {
2876                return;
2877            }
2878            inFlight = true;
2879            setSpinning(true);
2880
2881            var requestUrl = url + (url.indexOf("?") === -1 ? "?" : "&") + "state=" + encodeURIComponent(stateKey);
2882            var minSpin = new Promise(function(resolve) {
2883                window.setTimeout(resolve, SPIN_MS);
2884            });
2885            var request = fetch(requestUrl, {
2886                headers: {
2887                    "X-Requested-With": "XMLHttpRequest"
2888                },
2889                credentials: "same-origin"
2890            }).then(function(resp) {
2891                return resp.ok ? resp.json() : null;
2892            }).catch(function() {
2893                return null;
2894            });
2895
2896            Promise.all([ request, minSpin ]).then(function(results) {
2897                var data = results[0];
2898                if (data && data.ok) {
2899                    if (typeof data.stateKey === "string") {
2900                        stateKey = data.stateKey;
2901                    }
2902                    if (data.changed) {
2903                        applyState(data);
2904                    } else if (data.ended && !data.awaitingClose) {
2905                        stop();
2906                    }
2907                }
2908                setSpinning(false);
2909                inFlight = false;
2910                scheduleNext();
2911            });
2912        }
2913
2914        function pollNow() {
2915            if (stopped) {
2916                return;
2917            }
2918            window.clearTimeout(pollTimer);
2919            poll();
2920        }
2921
2922        if (btn) {
2923            btn.addEventListener("click", pollNow);
2924        }
2925        if (noteApplyBtn) {
2926            noteApplyBtn.addEventListener("click", function() {
2927                applyActionHtml(pendingHtml);
2928            });
2929        }
2930        markTouchedOnInput();
2931        card.addEventListener("auction:timeup", pollNow);
2932        document.addEventListener("visibilitychange", function() {
2933            if (!document.hidden) {
2934                pollNow();
2935            } else {
2936                scheduleNext();
2937            }
2938        });
2939        window.addEventListener("focus", pollNow);
2940        scheduleNext();
2941    }
2942    function initAuctionBidStepper() {
2943        var stepper = document.querySelector("[data-bid-stepper]");
2944        if (!stepper) {
2945            return;
2946        }
2947        var input = stepper.querySelector("[data-bid-amount]");
2948        if (!input) {
2949            return;
2950        }
2951        var min = parseInt(input.getAttribute("data-bid-min"), 10);
2952        var step = parseInt(input.getAttribute("data-bid-step-value"), 10);
2953        if (isNaN(min)) {
2954            min = 1;
2955        }
2956        if (isNaN(step) || step < 1) {
2957            step = 1;
2958        }
2959        var decBtn = stepper.querySelector(".bid-stepper__btn--dec");
2960        // Шаг цены — это минимальное повышение, а не сетка допустимых сумм:
2961        // при шаге 50 ставка 1060 так же законна, как 1051. Кнопки ± ходят шагом,
2962        // руками можно ввести любую сумму не ниже минимальной.
2963        function sanitize(value) {
2964            if (isNaN(value) || value <= min) {
2965                return min;
2966            }
2967            return Math.round(value);
2968        }
2969        function currentValue() {
2970            return parseInt(input.value, 10);
2971        }
2972        function setValue(value) {
2973            if (value < min) {
2974                value = min;
2975            }
2976            input.value = value;
2977            if (decBtn) {
2978                decBtn.disabled = value <= min;
2979            }
2980        }
2981        function nudge(direction) {
2982            var v = currentValue();
2983            if (isNaN(v)) {
2984                setValue(min);
2985                return;
2986            }
2987            if (direction > 0) {
2988                setValue(sanitize(v) + step);
2989            } else {
2990                setValue(Math.max(min, sanitize(v) - step));
2991            }
2992        }
2993        stepper.querySelectorAll("[data-bid-step]").forEach(function(btn) {
2994            btn.addEventListener("click", function() {
2995                nudge(parseInt(btn.getAttribute("data-bid-step"), 10) || 0);
2996            });
2997        });
2998        input.addEventListener("change", function() {
2999            setValue(sanitize(currentValue()));
3000        });
3001        input.addEventListener("blur", function() {
3002            setValue(sanitize(currentValue()));
3003        });
3004        setValue(sanitize(currentValue()));
3005    }
3006    function initAuctionBidConfirm() {
3007        function escapeHtml(value) {
3008            var div = document.createElement("div");
3009            div.textContent = value == null ? "" : String(value);
3010            return div.innerHTML;
3011        }
3012        function removeExistingConfirm(form) {
3013            var existing = form.parentNode ? form.parentNode.querySelector(".auction-bid-confirm-panel") : null;
3014            if (existing) {
3015                existing.remove();
3016            }
3017        }
3018        function submitConfirmed(form) {
3019            form.setAttribute("data-confirmed", "1");
3020            form.submit();
3021        }
3022        function attachSlider(panel, form) {
3023            var slider = panel.querySelector("[data-bid-confirm-slider]");
3024            var handle = panel.querySelector("[data-bid-confirm-handle]");
3025            var fill = panel.querySelector("[data-bid-confirm-fill]");
3026            var idleLabel = panel.querySelector(".auction-slide-confirm__label-idle");
3027            var cancelBtn = panel.querySelector("[data-bid-confirm-cancel]");
3028            if (!slider || !handle || !fill) {
3029                return;
3030            }
3031            var dragging = false;
3032            var progress = 0;
3033            var confirmed = false;
3034            var confirmThreshold = .92;
3035            var coverThreshold = .6;
3036            function setCancelEnabled(enabled) {
3037                if (cancelBtn) {
3038                    cancelBtn.disabled = !enabled;
3039                }
3040            }
3041            function setProgress(nextProgress) {
3042                progress = Math.max(0, Math.min(1, nextProgress));
3043                var maxLeft = Math.max(0, slider.clientWidth - handle.offsetWidth);
3044                var left = Math.round(maxLeft * progress);
3045                handle.style.transform = "translateX(" + left + "px)";
3046                fill.style.width = Math.round(progress * 100) + "%";
3047                slider.classList.toggle("is-ready", progress >= confirmThreshold);
3048                if (idleLabel && !confirmed) {
3049                    idleLabel.style.opacity = Math.max(0, Math.min(1, 1 - progress * 1.7));
3050                }
3051                slider.classList.toggle("is-covering", !confirmed && progress >= coverThreshold);
3052            }
3053            function updateFromClientX(clientX) {
3054                var rect = slider.getBoundingClientRect();
3055                var maxLeft = Math.max(1, rect.width - handle.offsetWidth);
3056                setProgress((clientX - rect.left - handle.offsetWidth / 2) / maxLeft);
3057            }
3058            function resetSlider() {
3059                if (confirmed) {
3060                    return;
3061                }
3062                slider.classList.remove("is-dragging", "is-ready", "is-confirming", "is-covering");
3063                slider.classList.add("is-teasing");
3064                setProgress(0);
3065                setCancelEnabled(true);
3066            }
3067            function confirmSlider() {
3068                if (confirmed) {
3069                    return;
3070                }
3071                confirmed = true;
3072                setCancelEnabled(false);
3073                slider.classList.remove("is-dragging", "is-teasing", "is-covering");
3074                slider.classList.add("is-confirming", "is-confirmed");
3075                if (idleLabel) {
3076                    idleLabel.style.opacity = "";
3077                }
3078                setProgress(1);
3079                window.setTimeout(function() {
3080                    submitConfirmed(form);
3081                }, 220);
3082            }
3083            function onPointerMove(event) {
3084                if (!dragging) {
3085                    return;
3086                }
3087                event.preventDefault();
3088                updateFromClientX(event.clientX);
3089                if (progress >= confirmThreshold) {
3090                    dragging = false;
3091                    try {
3092                        handle.releasePointerCapture(event.pointerId);
3093                    } catch (err) {}
3094                    confirmSlider();
3095                }
3096            }
3097            function onPointerUp(event) {
3098                if (!dragging) {
3099                    return;
3100                }
3101                updateFromClientX(event.clientX);
3102                dragging = false;
3103                slider.classList.remove("is-dragging");
3104                try {
3105                    handle.releasePointerCapture(event.pointerId);
3106                } catch (err) {}
3107                if (progress >= confirmThreshold) {
3108                    confirmSlider();
3109                    return;
3110                }
3111                resetSlider();
3112            }
3113            handle.addEventListener("pointerdown", function(event) {
3114                if (confirmed) {
3115                    return;
3116                }
3117                event.preventDefault();
3118                dragging = true;
3119                setCancelEnabled(false);
3120                slider.classList.remove("is-teasing");
3121                slider.classList.add("is-dragging");
3122                try {
3123                    handle.setPointerCapture(event.pointerId);
3124                } catch (err) {}
3125                updateFromClientX(event.clientX);
3126            });
3127            handle.addEventListener("pointermove", onPointerMove);
3128            handle.addEventListener("pointerup", onPointerUp);
3129            handle.addEventListener("pointercancel", resetSlider);
3130            handle.addEventListener("keydown", function(event) {
3131                if (event.key !== "Enter" && event.key !== " ") {
3132                    return;
3133                }
3134                event.preventDefault();
3135                confirmSlider();
3136            });
3137            setProgress(0);
3138        }
3139        document.addEventListener("submit", function(e) {
3140            var form = e.target;
3141            if (!form || !form.matches || !form.matches("[data-bid-confirm]")) {
3142                return;
3143            }
3144            if (form.getAttribute("data-confirmed") === "1") {
3145                return;
3146            }
3147            e.preventDefault();
3148            e.stopPropagation();
3149            var amountInput = form.querySelector('input[name="amount"]');
3150            var amount = amountInput ? parseInt(amountInput.value, 10) : 0;
3151            var maxInput = form.querySelector('input[name="max_amount"]');
3152            var maxAmount = maxInput ? parseInt(maxInput.value, 10) : 0;
3153            if (isNaN(maxAmount) || maxAmount < 1) {
3154                maxAmount = 0;
3155            }
3156            // Предел ниже собственной ставки — не автоторг, а обычная ставка:
3157            // поле не трогаем (вдруг человек передумает), сервер такой предел просто не сохранит.
3158            if (maxAmount && amount && maxAmount < amount) {
3159                maxAmount = 0;
3160            }
3161            var autoOnly = form.hasAttribute("data-bid-auto-only");
3162            // С указанным пределом человек берёт на себя обязательство до этой суммы —
3163            // её и показываем крупно, но с приставкой «до», чтобы не читалось как немедленная цена.
3164            var withAuto = autoOnly || (maxAmount > amount);
3165            var headline = withAuto ? maxAmount : amount;
3166            if (!headline || headline < 1) {
3167                form.submit();
3168                return;
3169            }
3170            var formattedAmount = headline.toLocaleString("ru-RU") + " грн.";
3171            var pricePrefix = withAuto ? (form.getAttribute("data-auto-price-prefix") || "") : "";
3172            if (pricePrefix) {
3173                formattedAmount = pricePrefix + " " + formattedAmount;
3174            }
3175            var autoNote = "";
3176            if (!autoOnly && withAuto) {
3177                autoNote = (form.getAttribute("data-auto-note") || "Сейчас ваша ставка — %bid% грн.")
3178                    .replace("%bid%", amount.toLocaleString("ru-RU"))
3179                    .replace("%max%", maxAmount.toLocaleString("ru-RU"));
3180            }
3181            function text(autoAttr, attr, fallback) {
3182                return (withAuto ? form.getAttribute(autoAttr) : null) || form.getAttribute(attr) || fallback;
3183            }
3184            var question = text("data-auto-confirm-question", "data-confirm-question", "Вы подтверждаете ставку?");
3185            var description = text("data-auto-confirm-description", "data-confirm-description", "");
3186            var title = text("data-auto-confirm-title", "data-confirm-title", "Подтверждение ставки");
3187            var slideLabel = form.getAttribute("data-slide-label") || "Перетащите, чтобы подтвердить";
3188            var slideDoneLabel = text("data-auto-slide-done-label", "data-slide-done-label", "Ставка принята");
3189            var cancelLabel = form.getAttribute("data-cancel-label") || "Отмена";
3190            removeExistingConfirm(form);
3191            form.insertAdjacentHTML("afterend", '<div class="auction-bid-confirm-panel">' + '<div class="auction-bid-confirm-panel__title">' + escapeHtml(title) + "</div>" + '<div class="auction-bid-confirm-panel__question">' + escapeHtml(question) + "</div>" + '<div class="auction-bid-confirm-panel__price"><span>' + escapeHtml(formattedAmount) + "</span></div>" + (autoNote ? '<div class="auction-bid-confirm-panel__auto">' + escapeHtml(autoNote) + "</div>" : "") + (description ? '<div class="auction-bid-confirm-panel__description">' + escapeHtml(description) + "</div>" : "") + '<div class="auction-bid-confirm-panel__actions">' + '<div class="auction-slide-confirm is-teasing" data-bid-confirm-slider>' + '<div class="auction-slide-confirm__fill" data-bid-confirm-fill></div>' + '<div class="auction-slide-confirm__label">' + '<span class="auction-slide-confirm__label-idle">' + escapeHtml(slideLabel) + "</span>" + '<span class="auction-slide-confirm__label-done">' + escapeHtml(slideDoneLabel) + "</span>" + "</div>" + '<button type="button" class="auction-slide-confirm__handle" data-bid-confirm-handle aria-label="' + escapeHtml(slideLabel) + '">›</button>' + "</div>" + '<button type="button" class="auction-bid-confirm-panel__cancel" data-bid-confirm-cancel>' + escapeHtml(cancelLabel) + "</button>" + "</div>" + "</div>");
3192            var panel = form.parentNode.querySelector(".auction-bid-confirm-panel");
3193            if (!panel) {
3194                return;
3195            }
3196            attachSlider(panel, form);
3197            panel.querySelector("[data-bid-confirm-cancel]").addEventListener("click", function() {
3198                panel.remove();
3199            });
3200            panel.scrollIntoView({
3201                behavior: "smooth",
3202                block: "nearest"
3203            });
3204        }, true);
3205    }
3206    if (document.readyState === "loading") {
3207        document.addEventListener("DOMContentLoaded", function() {
3208            initAuctionForm();
3209            initAuctionTimer();
3210            initAuctionBidConfirm();
3211            initAuctionLiveState();
3212            initAuctionBidStepper();
3213        });
3214    } else {
3215        initAuctionForm();
3216        initAuctionTimer();
3217        initAuctionBidConfirm();
3218        initAuctionLiveState();
3219        initAuctionBidStepper();
3220    }
3221})();
3222
3223(function() {
3224    function markEmptyMonths(board) {
3225        var rows = board.querySelectorAll(".table-plan > tbody > tr");
3226        var currentMonth = null;
3227        var changedInMonth = 0;
3228        function flush() {
3229            if (currentMonth) {
3230                currentMonth.classList.toggle("is-empty", changedInMonth === 0);
3231            }
3232        }
3233        rows.forEach(function(row) {
3234            if (row.classList.contains("plan-month")) {
3235                flush();
3236                currentMonth = row;
3237                changedInMonth = 0;
3238            } else if (row.classList.contains("plan-total")) {} else if (row.classList.contains("is-changed")) {
3239                changedInMonth++;
3240            }
3241        });
3242        flush();
3243    }
3244    function initBoard(board) {
3245        var buttons = board.querySelectorAll(".js-plan-filter");
3246        if (!buttons.length) {
3247            return;
3248        }
3249        buttons.forEach(function(btn) {
3250            btn.addEventListener("click", function() {
3251                var mode = btn.getAttribute("data-filter");
3252                var changesOnly = mode === "changes";
3253                if (changesOnly) {
3254                    markEmptyMonths(board);
3255                }
3256                board.classList.toggle("is-changes-only", changesOnly);
3257                buttons.forEach(function(b) {
3258                    var active = b === btn;
3259                    b.classList.toggle("is-active", active);
3260                    b.setAttribute("aria-pressed", active ? "true" : "false");
3261                });
3262            });
3263        });
3264    }
3265    function init() {
3266        document.querySelectorAll(".js-plan-board").forEach(initBoard);
3267    }
3268    if (document.readyState === "loading") {
3269        document.addEventListener("DOMContentLoaded", init);
3270    } else {
3271        init();
3272    }
3273})();
3274
3275(function() {
3276    var scriptPromises = {};
3277    var chartAssetsPromise = null;
3278    var PRIMARY = "#00A0DF";
3279    var GRID = "#eef4f8";
3280    var AXIS_TEXT = "#687884";
3281    var DAY_MS = 24 * 60 * 60 * 1e3;
3282    function cssVar(scope, name, fallback) {
3283        if (!scope || !window.getComputedStyle) {
3284            return fallback;
3285        }
3286        var value = window.getComputedStyle(scope).getPropertyValue(name).trim();
3287        return value || fallback;
3288    }
3289    function cssAlpha(scope, name, fallback) {
3290        var value = parseFloat(cssVar(scope, name, fallback));
3291        return Number.isFinite(value) ? value : fallback;
3292    }
3293    function chartTheme(root) {
3294        var mutedRgb = cssVar(root, "--ua-muted-rgb", "104, 120, 132");
3295        var gridAlpha = cssAlpha(root, "--ua-chart-grid-alpha", .14);
3296        return {
3297            primary: cssVar(root, "--ua-primary", PRIMARY),
3298            grid: "rgba(" + mutedRgb + ", " + gridAlpha + ")",
3299            axisText: cssVar(root, "--ua-muted", AXIS_TEXT)
3300        };
3301    }
3302    function loadScript(src, globalName) {
3303        if (globalName && window[globalName]) {
3304            return Promise.resolve(window[globalName]);
3305        }
3306        if (scriptPromises[src]) {
3307            return scriptPromises[src];
3308        }
3309        scriptPromises[src] = new Promise(function(resolve, reject) {
3310            var script = document.createElement("script");
3311            script.src = src;
3312            script.async = true;
3313            script.onload = function() {
3314                resolve(globalName ? window[globalName] : true);
3315            };
3316            script.onerror = reject;
3317            document.head.appendChild(script);
3318        });
3319        return scriptPromises[src];
3320    }
3321    function loadChartAssets() {
3322        if (chartAssetsPromise) {
3323            return chartAssetsPromise;
3324        }
3325        chartAssetsPromise = loadScript("https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js", "Chart").then(function() {
3326            return loadScript("https://cdn.jsdelivr.net/npm/[email protected]/build/global/luxon.min.js", "luxon");
3327        }).then(function() {
3328            return loadScript("https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-adapter-luxon.umd.min.js");
3329        });
3330        return chartAssetsPromise;
3331    }
3332    function hexToRgba(hex, alpha) {
3333        var n = parseInt(hex.slice(1), 16);
3334        return "rgba(" + (n >> 16 & 255) + ", " + (n >> 8 & 255) + ", " + (n & 255) + ", " + alpha + ")";
3335    }
3336    function areaFill(hex) {
3337        return function(ctx) {
3338            var chart = ctx.chart;
3339            var area = chart.chartArea;
3340            if (!area) {
3341                return hexToRgba(hex, .12);
3342            }
3343            var gradient = chart.ctx.createLinearGradient(0, area.top, 0, area.bottom);
3344            gradient.addColorStop(0, hexToRgba(hex, .26));
3345            gradient.addColorStop(1, hexToRgba(hex, 0));
3346            return gradient;
3347        };
3348    }
3349    function numberLocale(language) {
3350        if (language === "uk" || language === "ua") {
3351            return "uk-UA";
3352        }
3353        if (language === "en") {
3354            return "en-US";
3355        }
3356        return "ru-RU";
3357    }
3358    function formatNumber(value, language) {
3359        return Number(value || 0).toLocaleString(numberLocale(language), {
3360            maximumFractionDigits: 0
3361        });
3362    }
3363    function formatDate(timestamp, language) {
3364        var locale = language === "ua" ? "uk" : language || "ru";
3365        return new Intl.DateTimeFormat(locale, {
3366            day: "2-digit",
3367            month: "2-digit",
3368            year: "numeric"
3369        }).format(new Date(timestamp));
3370    }
3371    function toPoints(rows) {
3372        return rows.map(function(row) {
3373            return {
3374                x: new Date(String(row.date || "")).getTime(),
3375                y: parseFloat(row.price)
3376            };
3377        }).filter(function(point) {
3378            return Number.isFinite(point.x) && Number.isFinite(point.y) && point.y > 0;
3379        }).sort(function(a, b) {
3380            return a.x - b.x;
3381        });
3382    }
3383    function filterByRange(points, range) {
3384        if (range === "all" || points.length === 0) {
3385            return points;
3386        }
3387        var days = parseInt(range, 10);
3388        if (!Number.isFinite(days) || days <= 0) {
3389            return points;
3390        }
3391        var latest = points[points.length - 1].x;
3392        var from = latest - days * DAY_MS;
3393        var filtered = points.filter(function(point) {
3394            return point.x >= from;
3395        });
3396        return filtered.length ? filtered : points;
3397    }
3398    function configurePeriodButtons(root, points) {
3399        var periods = root.querySelector(".market-price-chart__periods");
3400        var buttons = Array.prototype.slice.call(root.querySelectorAll(".js-market-price-period"));
3401        if (!periods || !buttons.length || points.length < 2) {
3402            if (periods) {
3403                periods.hidden = true;
3404            }
3405            return;
3406        }
3407        var spanDays = (points[points.length - 1].x - points[0].x) / DAY_MS;
3408        var visibleCount = 0;
3409        var allButton = null;
3410        buttons.forEach(function(button) {
3411            var range = button.getAttribute("data-range");
3412            var hide = false;
3413            if (range === "all") {
3414                allButton = button;
3415            } else {
3416                var days = parseInt(range, 10);
3417                hide = Number.isFinite(days) && spanDays <= days;
3418            }
3419            button.hidden = hide;
3420            if (!hide) {
3421                visibleCount++;
3422            }
3423        });
3424        periods.hidden = visibleCount <= 1;
3425        if (!root.querySelector(".js-market-price-period.is-active:not([hidden])") && allButton && !allButton.hidden) {
3426            setActivePeriod(root, allButton);
3427        }
3428    }
3429    function roundToStep(value, step, direction) {
3430        if (!Number.isFinite(value) || !Number.isFinite(step) || step <= 0) {
3431            return value;
3432        }
3433        return direction === "up" ? Math.ceil(value / step) * step : Math.floor(value / step) * step;
3434    }
3435    function choosePriceStep(span) {
3436        if (span >= 1e5) {
3437            return 1e4;
3438        }
3439        if (span >= 5e4) {
3440            return 5e3;
3441        }
3442        if (span >= 1e4) {
3443            return 1e3;
3444        }
3445        if (span >= 5e3) {
3446            return 500;
3447        }
3448        if (span >= 1e3) {
3449            return 100;
3450        }
3451        if (span >= 500) {
3452            return 50;
3453        }
3454        if (span >= 100) {
3455            return 20;
3456        }
3457        if (span >= 20) {
3458            return 5;
3459        }
3460        return 1;
3461    }
3462    function calculateYBounds(points) {
3463        if (!points.length) {
3464            return {};
3465        }
3466        var values = points.map(function(point) {
3467            return point.y;
3468        });
3469        var min = Math.min.apply(Math, values);
3470        var max = Math.max.apply(Math, values);
3471        var center = (min + max) / 2;
3472        var reference = Math.max(Math.abs(center), Math.abs(points[points.length - 1].y), 1);
3473        var dataSpan = Math.max(max - min, 0);
3474        var targetSpan = Math.max(dataSpan * 1.35, reference * .12);
3475        var rawMin = center - targetSpan / 2;
3476        var rawMax = center + targetSpan / 2;
3477        if (rawMin < 0 && min >= 0) {
3478            rawMin = 0;
3479        }
3480        var step = choosePriceStep(targetSpan);
3481        return {
3482            min: roundToStep(rawMin, step, "down"),
3483            max: roundToStep(rawMax, step, "up")
3484        };
3485    }
3486    function setState(root, name) {
3487        var canvasWrap = root.querySelector(".js-market-price-chart-canvas-wrap");
3488        var states = root.querySelectorAll(".js-market-price-chart-state");
3489        if (canvasWrap) {
3490            canvasWrap.hidden = name !== null;
3491        }
3492        states.forEach(function(state) {
3493            state.hidden = state.getAttribute("data-role") !== name;
3494        });
3495    }
3496    function updateStats(root, points, language, currencyLabel) {
3497        var stats = root.querySelector(".js-market-price-chart-stats");
3498        var summary = root.querySelector(".js-market-price-chart-summary");
3499        var periodStat = root.querySelector(".js-market-price-chart-period-stat");
3500        var hasStaticStats = !!root.querySelector("[data-market-chart-static-stat]");
3501        if (!points.length) {
3502            root.classList.toggle("has-market-price-stats", hasStaticStats);
3503            if (stats) {
3504                stats.hidden = !hasStaticStats;
3505            }
3506            if (periodStat) {
3507                periodStat.hidden = true;
3508            }
3509            return;
3510        }
3511        var first = points[0];
3512        var last = points[points.length - 1];
3513        var values = points.map(function(point) {
3514            return point.y;
3515        });
3516        var min = Math.min.apply(Math, values);
3517        var max = Math.max.apply(Math, values);
3518        var change = last.y - first.y;
3519        var changePercent = first.y > 0 ? change / first.y * 100 : 0;
3520        var changeText = (change > 0 ? "+" : "") + formatNumber(change, language) + " " + currencyLabel + " (" + (changePercent > 0 ? "+" : "") + changePercent.toLocaleString(numberLocale(language), {
3521            maximumFractionDigits: 1
3522        }) + "%)";
3523        var statMap = {
3524            min: formatNumber(min, language) + " " + currencyLabel,
3525            max: formatNumber(max, language) + " " + currencyLabel,
3526            change: changeText,
3527            range: (root.getAttribute("data-min-label") || "Минимум") + " " + formatNumber(min, language) + " · " + (root.getAttribute("data-max-label") || "Максимум") + " " + formatNumber(max, language)
3528        };
3529        Object.keys(statMap).forEach(function(key) {
3530            var node = root.querySelector('[data-market-chart-stat="' + key + '"]');
3531            if (node) {
3532                node.textContent = statMap[key];
3533                if (key === "change") {
3534                    node.classList.toggle("is-up", change > 0);
3535                    node.classList.toggle("is-down", change < 0);
3536                }
3537            }
3538        });
3539        if (summary) {
3540            summary.textContent = formatDate(first.x, language) + " - " + formatDate(last.x, language);
3541        }
3542        if (stats) {
3543            stats.hidden = false;
3544            root.classList.add("has-market-price-stats");
3545        }
3546        if (periodStat) {
3547            periodStat.hidden = false;
3548        }
3549    }
3550    function drawChart(root, points) {
3551        var canvas = root.querySelector(".js-market-price-chart-canvas");
3552        if (!canvas) {
3553            return;
3554        }
3555        var language = root.getAttribute("data-language") || "ru";
3556        var priceLabel = root.getAttribute("data-price-label") || "Цена";
3557        var currencyLabel = root.getAttribute("data-currency-label") || "грн";
3558        var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3559        var existing = root._marketPriceChart || null;
3560        var yBounds = calculateYBounds(points);
3561        var theme = chartTheme(root);
3562        updateStats(root, points, language, currencyLabel);
3563        if (existing) {
3564            existing.data.datasets[0].data = points;
3565            existing.data.datasets[0].borderColor = theme.primary;
3566            existing.data.datasets[0].backgroundColor = areaFill(theme.primary);
3567            existing.options.scales.y.min = yBounds.min;
3568            existing.options.scales.y.max = yBounds.max;
3569            existing.options.scales.x.grid.color = theme.grid;
3570            existing.options.scales.x.border.color = theme.grid;
3571            existing.options.scales.x.ticks.color = theme.axisText;
3572            existing.options.scales.y.grid.color = theme.grid;
3573            existing.options.scales.y.border.color = theme.grid;
3574            existing.options.scales.y.ticks.color = theme.axisText;
3575            existing.update();
3576            return;
3577        }
3578        root._marketPriceChart = new Chart(canvas, {
3579            type: "line",
3580            data: {
3581                datasets: [ {
3582                    label: priceLabel,
3583                    data: points,
3584                    borderColor: theme.primary,
3585                    borderWidth: 2,
3586                    tension: .25,
3587                    pointRadius: 0,
3588                    pointHitRadius: 14,
3589                    pointHoverRadius: 4,
3590                    fill: true,
3591                    backgroundColor: areaFill(theme.primary)
3592                } ]
3593            },
3594            options: {
3595                responsive: true,
3596                maintainAspectRatio: false,
3597                animation: {
3598                    duration: reducedMotion ? 0 : 300
3599                },
3600                parsing: false,
3601                normalized: true,
3602                interaction: {
3603                    mode: "nearest",
3604                    axis: "xy",
3605                    intersect: false
3606                },
3607                scales: {
3608                    x: {
3609                        type: "time",
3610                        adapters: {
3611                            date: {
3612                                locale: language === "ua" ? "uk" : language
3613                            }
3614                        },
3615                        time: {
3616                            tooltipFormat: "dd.MM.yyyy",
3617                            minUnit: "day"
3618                        },
3619                        grid: {
3620                            color: theme.grid,
3621                            drawTicks: false
3622                        },
3623                        border: {
3624                            color: theme.grid
3625                        },
3626                        ticks: {
3627                            maxRotation: 0,
3628                            autoSkip: true,
3629                            color: theme.axisText
3630                        }
3631                    },
3632                    y: {
3633                        min: yBounds.min,
3634                        max: yBounds.max,
3635                        grid: {
3636                            color: theme.grid,
3637                            drawTicks: false
3638                        },
3639                        border: {
3640                            color: theme.grid
3641                        },
3642                        ticks: {
3643                            color: theme.axisText,
3644                            callback: function(value) {
3645                                return formatNumber(value, language);
3646                            }
3647                        }
3648                    }
3649                },
3650                plugins: {
3651                    legend: {
3652                        display: false
3653                    },
3654                    decimation: {
3655                        enabled: false
3656                    },
3657                    tooltip: {
3658                        callbacks: {
3659                            label: function(item) {
3660                                return formatNumber(item.parsed.y, language) + " " + currencyLabel;
3661                            }
3662                        }
3663                    }
3664                }
3665            }
3666        });
3667    }
3668    function setActivePeriod(root, activeButton) {
3669        root.querySelectorAll(".js-market-price-period").forEach(function(button) {
3670            var isActive = button === activeButton;
3671            button.classList.toggle("is-active", isActive);
3672            button.setAttribute("aria-pressed", isActive ? "true" : "false");
3673        });
3674    }
3675    function initPriceChart(root) {
3676        var pricesUrl = root.getAttribute("data-prices-url");
3677        var errorLabel = root.getAttribute("data-error-label") || "Ошибка загрузки данных";
3678        var summary = root.querySelector(".js-market-price-chart-summary");
3679        var allPoints = [];
3680        if (!pricesUrl) {
3681            return;
3682        }
3683        function renderCurrentRange() {
3684            var active = root.querySelector(".js-market-price-period.is-active");
3685            var range = active ? active.getAttribute("data-range") : "all";
3686            var points = filterByRange(allPoints, range || "all");
3687            setState(root, points.length ? null : "empty");
3688            if (points.length) {
3689                drawChart(root, points);
3690            }
3691        }
3692        root.querySelectorAll(".js-market-price-period").forEach(function(button) {
3693            button.addEventListener("click", function() {
3694                if (button.classList.contains("is-active")) {
3695                    return;
3696                }
3697                setActivePeriod(root, button);
3698                renderCurrentRange();
3699            });
3700        });
3701        setState(root, "loading");
3702        loadChartAssets().then(function() {
3703            return fetch(pricesUrl, {
3704                headers: {
3705                    "X-Requested-With": "XMLHttpRequest"
3706                }
3707            });
3708        }).then(function(response) {
3709            if (!response.ok) {
3710                throw new Error("HTTP " + response.status);
3711            }
3712            return response.json();
3713        }).then(function(rows) {
3714            allPoints = toPoints(Array.isArray(rows) ? rows : []);
3715            if (!allPoints.length) {
3716                setState(root, "empty");
3717                return;
3718            }
3719            configurePeriodButtons(root, allPoints);
3720            renderCurrentRange();
3721        }).catch(function(error) {
3722            if (window.console && console.error) {
3723                console.error("Ошибка загрузки данных:", error);
3724            }
3725            if (summary) {
3726                summary.textContent = errorLabel;
3727            }
3728            setState(root, "error");
3729        });
3730    }
3731    function initCoinPriceCharts() {
3732        document.querySelectorAll(".js-coin-price-chart").forEach(initPriceChart);
3733    }
3734    if (document.readyState === "loading") {
3735        document.addEventListener("DOMContentLoaded", initCoinPriceCharts);
3736    } else {
3737        initCoinPriceCharts();
3738    }
3739})();
3740
3741(function() {
3742    function showSoldStamp(card) {
3743        var wrap = card.querySelector(".lot-image-wrap");
3744        if (!wrap) {
3745            return;
3746        }
3747        var old = wrap.querySelector(".sold-stamp");
3748        if (old) {
3749            old.remove();
3750        }
3751        var stamp = document.createElement("span");
3752        stamp.className = "sold-stamp";
3753        stamp.textContent = card.getAttribute("data-sold-label") || "Продано";
3754        stamp.addEventListener("animationend", function() {
3755            stamp.remove();
3756        });
3757        wrap.appendChild(stamp);
3758    }
3759    document.addEventListener("click", function(e) {
3760        var card = e.target.closest("[data-sold-card]");
3761        if (!card) {
3762            return;
3763        }
3764        if (e.target.closest("a")) {
3765            return;
3766        }
3767        e.preventDefault();
3768        showSoldStamp(card);
3769    });
3770})();
3771
3772/**
3773 * WYSIWYG-редактор комментариев (стиль Telegram).
3774 * Поле ввода с data-format-enabled (премиум-авторы) подменяется на
3775 * contenteditable: жирный/курсив/подчёркнутый/зачёркнутый видны сразу при наборе.
3776 * Исходный <textarea> остаётся скрытым полем формы — в него на каждый ввод
3777 * сериализуются маркеры [b]..[/b] / [i] / [u] / [s], которые сервер разворачивает
3778 * в теги на выводе. Выделение текста поднимает плавающую панель Ж/К/П/З.
3779 * API: window.CommentEditor { enhance, pull, insert, clear, isEnhanced }.
3780 */
3781(function() {
3782    var ENABLED = "textarea[data-format-enabled]";
3783
3784    function i18n(key, fb) {
3785        var b = document.getElementById("comment-i18n");
3786        if (!b) { return fb; }
3787        return b.getAttribute("data-" + key) || fb;
3788    }
3789
3790    // ---------- маркеры <-> html ----------
3791    function escapeHtml(s) {
3792        return s.replace(/[&<>"]/g, function(c) {
3793            return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
3794        });
3795    }
3796
3797    function markersToHtml(text) {
3798        return escapeHtml(text)
3799            .replace(/\[b\]([\s\S]*?)\[\/b\]/gi, "<b>$1</b>")
3800            .replace(/\[i\]([\s\S]*?)\[\/i\]/gi, "<i>$1</i>")
3801            .replace(/\[u\]([\s\S]*?)\[\/u\]/gi, "<u>$1</u>")
3802            .replace(/\[s\]([\s\S]*?)\[\/s\]/gi, "<s>$1</s>")
3803            .replace(/\n/g, "<br>");
3804    }
3805
3806    var TAG_MARK = { b: "b", strong: "b", i: "i", em: "i", u: "u", s: "s", strike: "s", del: "s" };
3807
3808    function serialize(node) {
3809        var out = "";
3810        var children = node.childNodes;
3811        for (var i = 0; i < children.length; i++) {
3812            var ch = children[i];
3813            if (ch.nodeType === 3) {
3814                out += ch.nodeValue;
3815            } else if (ch.nodeType === 1) {
3816                var tag = ch.tagName.toLowerCase();
3817                if (tag === "br") {
3818                    out += "\n";
3819                } else if (TAG_MARK[tag]) {
3820                    var m = TAG_MARK[tag];
3821                    var inner = serialize(ch);
3822                    out += inner ? "[" + m + "]" + inner + "[/" + m + "]" : "";
3823                } else if (tag === "div" || tag === "p") {
3824                    if (out && out.slice(-1) !== "\n") { out += "\n"; }
3825                    out += serialize(ch);
3826                } else {
3827                    out += serialize(ch);
3828                }
3829            }
3830        }
3831        return out;
3832    }
3833
3834    function normalize(s) {
3835        return s.replace(/ /g, " ")
3836            .replace(/[ \t]+\n/g, "\n")
3837            .replace(/\n{3,}/g, "\n\n")
3838            .replace(/\s+$/, "");
3839    }
3840
3841    // ---------- синхронизация ----------
3842    function updateEmpty(ed) {
3843        var empty = ed.textContent.replace(/[​ \s]/g, "").length === 0;
3844        ed.classList.toggle("is-empty", empty);
3845    }
3846
3847    function syncFromEditable(ed) {
3848        ed._ta.value = normalize(serialize(ed));
3849        updateEmpty(ed);
3850        ed._ta.dispatchEvent(new Event("input", { bubbles: true }));
3851    }
3852
3853    function loadIntoEditable(ed) {
3854        ed.innerHTML = markersToHtml(ed._ta.value || "");
3855        updateEmpty(ed);
3856    }
3857
3858    // ---------- превращение textarea в редактор ----------
3859    function enhance(ta) {
3860        if (!ta || ta._richEnhanced || !ta.matches || !ta.matches(ENABLED)) {
3861            return ta && ta._rich ? ta._rich : null;
3862        }
3863        var isInline = ta.className.indexOf("comment-inline-editor") >= 0;
3864        var ed = document.createElement("div");
3865        // NB: не даём редактору класс comment-inline-editor — по нему save/keydown
3866        // ищут исходный textarea; коллизия ломала бы сохранение.
3867        ed.className = "form-control ua-rich-editor" + (isInline ? " ua-rich-editor--inline" : "");
3868        ed.setAttribute("contenteditable", "true");
3869        ed.setAttribute("role", "textbox");
3870        ed.setAttribute("aria-multiline", "true");
3871        ed.setAttribute("data-placeholder", ta.getAttribute("placeholder") || "");
3872        var lbl = ta.getAttribute("aria-label");
3873        if (lbl) { ed.setAttribute("aria-label", lbl); }
3874        ed._ta = ta;
3875        ta._rich = ed;
3876        ta._richEnhanced = true;
3877        ta.classList.add("ua-rich-source");
3878        ta.setAttribute("tabindex", "-1");
3879        ta.setAttribute("aria-hidden", "true");
3880        ta.parentNode.insertBefore(ed, ta);
3881        loadIntoEditable(ed);
3882
3883        ed.addEventListener("input", function() { syncFromEditable(ed); });
3884        ed.addEventListener("paste", function(e) {
3885            e.preventDefault();
3886            var text = (e.clipboardData || window.clipboardData).getData("text/plain");
3887            document.execCommand("insertText", false, text);
3888        });
3889        ed.addEventListener("beforeinput", function(e) {
3890            if (e.inputType && e.inputType.indexOf("insert") === 0) {
3891                var sel = window.getSelection();
3892                var selLen = sel && !sel.isCollapsed ? String(sel).length : 0;
3893                var add = e.data ? e.data.length : 1;
3894                if (ed.textContent.length - selLen + add > 2000) {
3895                    e.preventDefault();
3896                }
3897            }
3898        });
3899        ed.addEventListener("focus", function() { updateEmpty(ed); });
3900
3901        // Кнопки форматирования: докнутая панель под полем. У основного композера
3902        // есть готовый слот [data-format-slot] в форме; у инлайн-редактора создаём
3903        // свою панельку рядом.
3904        var slot = ta.form ? ta.form.querySelector("[data-format-slot]") : null;
3905        if (slot) {
3906            buildFormatButtons(ed, slot);
3907        } else {
3908            var bar = document.createElement("div");
3909            bar.className = "ua-compose-toolbar ua-compose-toolbar--inline";
3910            buildFormatButtons(ed, bar);
3911            ed.parentNode.insertBefore(bar, ed.nextSibling);
3912        }
3913
3914        return ed;
3915    }
3916
3917    // ---------- форматирование ----------
3918    var MARK_CMD = { b: "bold", i: "italic", u: "underline", s: "strikeThrough" };
3919    var FMT_BUTTONS = [
3920        { marker: "b", label: "Ж", cls: "fmt-b", title: i18n("fmt-bold", "Жирный"), key: "b" },
3921        { marker: "i", label: "К", cls: "fmt-i", title: i18n("fmt-italic", "Курсив"), key: "i" },
3922        { marker: "u", label: "П", cls: "fmt-u", title: i18n("fmt-underline", "Подчёркнутый"), key: "u" },
3923        { marker: "s", label: "З", cls: "fmt-s", title: i18n("fmt-strike", "Зачёркнутый"), key: null }
3924    ];
3925
3926    function format(ed, marker) {
3927        if (!ed) { return; }
3928        ed.focus();
3929        try { document.execCommand("styleWithCSS", false, false); } catch (_) {}
3930        document.execCommand(MARK_CMD[marker], false, null);
3931        syncFromEditable(ed);
3932        updateActiveStates(ed);
3933    }
3934
3935    function buildFormatButtons(ed, container) {
3936        ed._fmtButtons = [];
3937        FMT_BUTTONS.forEach(function(b) {
3938            var el = document.createElement("button");
3939            el.type = "button";
3940            el.className = "ua-compose-btn ua-format-btn " + b.cls;
3941            el.textContent = b.label;
3942            el.title = b.title;
3943            el.setAttribute("aria-label", b.title);
3944            el.setAttribute("aria-pressed", "false");
3945            el.addEventListener("mousedown", function(e) {
3946                e.preventDefault(); // сохранить выделение в редакторе
3947                format(ed, b.marker);
3948            });
3949            container.appendChild(el);
3950            ed._fmtButtons.push({ marker: b.marker, el: el });
3951        });
3952    }
3953
3954    // Подсветка кнопок: что уже применено в текущем выделении/каретке
3955    // (queryCommandState — родной спутник execCommand). Частичное выделение
3956    // обычно даёт false — это ок.
3957    function updateActiveStates(ed) {
3958        if (!ed || !ed._fmtButtons) { return; }
3959        ed._fmtButtons.forEach(function(b) {
3960            var active = false;
3961            try { active = document.queryCommandState(MARK_CMD[b.marker]); } catch (_) {}
3962            b.el.classList.toggle("is-active", active);
3963            b.el.setAttribute("aria-pressed", active ? "true" : "false");
3964        });
3965    }
3966
3967    function editableFromSelection() {
3968        var sel = window.getSelection();
3969        if (!sel || sel.rangeCount === 0) { return null; }
3970        var node = sel.anchorNode;
3971        while (node) {
3972            if (node.nodeType === 1 && node.classList && node.classList.contains("ua-rich-editor")) {
3973                return node;
3974            }
3975            node = node.parentNode;
3976        }
3977        return null;
3978    }
3979
3980    document.addEventListener("selectionchange", function() {
3981        var ed = editableFromSelection();
3982        if (ed) { updateActiveStates(ed); }
3983    });
3984
3985    document.addEventListener("keydown", function(e) {
3986        var el = document.activeElement;
3987        if (!el || !el.classList || !el.classList.contains("ua-rich-editor")) { return; }
3988        if (!(e.ctrlKey || e.metaKey) || e.altKey) { return; }
3989        var btn = FMT_BUTTONS.filter(function(b) { return b.key === e.key.toLowerCase(); })[0];
3990        if (btn) {
3991            e.preventDefault();
3992            format(el, btn.marker);
3993        }
3994    });
3995
3996    // ---------- публичный API для интеграции с существующими обработчиками ----------
3997    window.CommentEditor = {
3998        enhance: enhance,
3999        pull: function(ta) { if (ta && ta._rich) { loadIntoEditable(ta._rich); } },
4000        insert: function(ta, text) {
4001            if (ta && ta._rich) {
4002                ta._rich.focus();
4003                document.execCommand("insertText", false, text);
4004                return true;
4005            }
4006            return false;
4007        },
4008        clear: function(ta) {
4009            if (!ta) { return; }
4010            ta.value = "";
4011            if (ta._rich) { loadIntoEditable(ta._rich); }
4012        },
4013        isEnhanced: function(ta) { return !!(ta && ta._richEnhanced); }
4014    };
4015
4016    function initAll() {
4017        Array.prototype.forEach.call(document.querySelectorAll(ENABLED), function(ta) { enhance(ta); });
4018    }
4019    if (document.readyState === "loading") {
4020        document.addEventListener("DOMContentLoaded", initAll);
4021    } else {
4022        initAll();
4023    }
4024})();
4025
4026/**
4027 * Прикрепление одной картинки к комментарию (премиум): выбор файла, клиентская
4028 * валидация типа/размера, превью с кнопкой удаления. Отправка — в multipart-ветке
4029 * submit-обработчика формы комментария.
4030 */
4031(function() {
4032    var ALLOWED = ["image/jpeg", "image/png", "image/webp", "image/gif"];
4033    var MAX_BYTES = 8 * 1024 * 1024;
4034
4035    function i18n(key, fb) {
4036        var b = document.getElementById("comment-i18n");
4037        if (!b) { return fb; }
4038        return b.getAttribute("data-" + key) || fb;
4039    }
4040
4041    function hidePreview(form) {
4042        if (!form) { return; }
4043        var preview = form.querySelector(".js-attach-preview");
4044        if (preview) {
4045            preview.setAttribute("hidden", "hidden");
4046            var img = preview.querySelector(".ua-attach-preview__img");
4047            if (img) { img.src = ""; }
4048        }
4049    }
4050
4051    function resetAttach(form) {
4052        if (!form) { return; }
4053        var input = form.querySelector(".js-attach-input");
4054        if (input) { input.value = ""; }
4055        hidePreview(form);
4056    }
4057
4058    function showPreview(form, file) {
4059        if (!form) { return; }
4060        var preview = form.querySelector(".js-attach-preview");
4061        if (!preview) { return; }
4062        var img = preview.querySelector(".ua-attach-preview__img");
4063        var reader = new FileReader();
4064        reader.onload = function() {
4065            if (img) { img.src = reader.result; }
4066            preview.removeAttribute("hidden");
4067        };
4068        reader.readAsDataURL(file);
4069    }
4070
4071    document.addEventListener("click", function(e) {
4072        var btn = e.target.closest && e.target.closest(".js-attach-image");
4073        if (btn) {
4074            e.preventDefault();
4075            var wrap = btn.closest("[data-format-toolbar]");
4076            var input = wrap && wrap.querySelector(".js-attach-input");
4077            if (input) { input.click(); }
4078            return;
4079        }
4080        var rm = e.target.closest && e.target.closest(".js-attach-remove");
4081        if (rm) {
4082            e.preventDefault();
4083            resetAttach(rm.closest("form"));
4084        }
4085    });
4086
4087    document.addEventListener("change", function(e) {
4088        var input = e.target;
4089        if (!input.classList || !input.classList.contains("js-attach-input")) { return; }
4090        var form = input.closest("form");
4091        var errBox = form ? form.querySelector(".js-comment-error") : null;
4092        var file = input.files && input.files[0];
4093        if (!file) { hidePreview(form); return; }
4094        if (ALLOWED.indexOf(file.type) < 0) {
4095            if (errBox) { errBox.textContent = i18n("attach-bad-type", "Допустимые форматы: JPG, PNG, WEBP, GIF"); }
4096            resetAttach(form);
4097            return;
4098        }
4099        if (file.size > MAX_BYTES) {
4100            if (errBox) { errBox.textContent = i18n("attach-too-big", "Размер картинки не должен превышать 8 МБ"); }
4101            resetAttach(form);
4102            return;
4103        }
4104        if (errBox) { errBox.textContent = ""; }
4105        showPreview(form, file);
4106    });
4107})();
4108
4109/* Поиск в шапке: одно поле на все ширины, всегда вплотную слева от аккаунта или бургера */
4110(function () {
4111    var navbar = document.querySelector(".ua-main-navbar");
4112    var form = navbar && navbar.querySelector("[data-navbar-search]");
4113    if (!navbar || !form) { return; }
4114
4115    var container = navbar.querySelector(".container");
4116    var collapse = navbar.querySelector("#navbar");
4117    var userNav = collapse && collapse.querySelector(".navbar-nav.ms-auto");
4118    var toggle = navbar.querySelector("[data-navbar-search-toggle]");
4119    var closeBtn = form.querySelector("[data-navbar-search-close]");
4120    var clearBtn = form.querySelector("[data-search-clear]");
4121    var field = form.querySelector(".ua-search");
4122    var input = form.querySelector(".ua-search__input");
4123    var burger = navbar.querySelector(".navbar-toggler");
4124    var wide = window.matchMedia("(min-width: 1200px)");
4125    var menuExpanded = window.matchMedia("(min-width: 992px)");
4126
4127    function syncFilled() {
4128        if (!field || !input) { return; }
4129        field.classList.toggle("is-filled", input.value.trim() !== "");
4130    }
4131
4132    function closeSearch() {
4133        navbar.classList.remove("is-search-open");
4134        if (toggle) { toggle.setAttribute("aria-expanded", "false"); }
4135    }
4136
4137    function openSearch() {
4138        if (collapse && collapse.classList.contains("show") && window.bootstrap) {
4139            window.bootstrap.Collapse.getOrCreateInstance(collapse).hide();
4140        }
4141        navbar.classList.add("is-search-open");
4142        if (toggle) { toggle.setAttribute("aria-expanded", "true"); }
4143        if (input) { input.focus(); }
4144    }
4145
4146    /* Поиск всегда стоит вплотную слева от правого блока шапки: от бургера на телефоне,
4147       от меню пользователя на остальных ширинах. Меняется только форма — лупа или поле. */
4148    function place() {
4149        if (wide.matches) {
4150            if (userNav && form.parentNode !== collapse) {
4151                userNav.parentNode.insertBefore(form, userNav);
4152            }
4153            closeSearch();
4154        } else if (container && form.parentNode !== container) {
4155            container.appendChild(form);
4156        }
4157
4158        if (!toggle) { return; }
4159        if (menuExpanded.matches) {
4160            if (userNav && toggle.parentNode !== collapse) {
4161                userNav.parentNode.insertBefore(toggle, userNav);
4162            }
4163        } else if (burger && toggle.nextElementSibling !== burger) {
4164            container.insertBefore(toggle, burger);
4165        }
4166    }
4167
4168    if (toggle) {
4169        toggle.addEventListener("click", function () {
4170            if (navbar.classList.contains("is-search-open")) { closeSearch(); } else { openSearch(); }
4171        });
4172    }
4173
4174    if (closeBtn) {
4175        closeBtn.addEventListener("click", closeSearch);
4176    }
4177
4178    if (clearBtn && input) {
4179        clearBtn.addEventListener("click", function () {
4180            input.value = "";
4181            syncFilled();
4182            input.focus();
4183        });
4184    }
4185
4186    if (input) {
4187        input.addEventListener("input", syncFilled);
4188    }
4189
4190    document.addEventListener("keydown", function (e) {
4191        if (e.key === "Escape" && navbar.classList.contains("is-search-open")) { closeSearch(); }
4192    });
4193
4194    [wide, menuExpanded].forEach(function (mq) {
4195        if (mq.addEventListener) {
4196            mq.addEventListener("change", place);
4197        } else if (mq.addListener) {
4198            mq.addListener(place);
4199        }
4200    });
4201
4202    place();
4203    syncFilled();
4204})();
4205
4206/* Переключение оформления сайта: светлая / как в системе / тёмная.
4207   Выбор хранится в cookie ua_theme, поэтому сервер отдаёт нужную тему сразу,
4208   без вспышки светлого фона. Применяется мгновенно, без перезагрузки. */
4209(function () {
4210    var COOKIE = "ua_theme";
4211    var VALUES = ["light", "system", "dark"];
4212    var root = document.documentElement;
4213    var media = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
4214
4215    /* Гостям тема не применяется: сайт для них всегда светлый.
4216       Сервер помечает разрешение атрибутом data-theme-allowed. */
4217    function isAllowed() {
4218        return root.getAttribute("data-theme-allowed") === "1";
4219    }
4220
4221    function readPref() {
4222        if (!isAllowed()) { return "light"; }
4223        /* Сервер уже подставил значение из аккаунта — оно главнее локальной cookie,
4224           иначе выбор, сделанный на другом устройстве, сюда не доедет. */
4225        var value = root.getAttribute("data-theme-pref");
4226        if (VALUES.indexOf(value) === -1) {
4227            var m = document.cookie.match(/(?:^|;\s*)ua_theme=([^;]*)/);
4228            value = m ? decodeURIComponent(m[1]) : null;
4229        }
4230        return VALUES.indexOf(value) === -1 ? "light" : value;
4231    }
4232
4233    function savePref(value) {
4234        document.cookie = COOKIE + "=" + value + ";path=/;max-age=31536000;samesite=lax";
4235
4236        /* И в аккаунт — чтобы выбор работал в любом браузере пользователя. */
4237        try {
4238            fetch("/theme/preference/" + value, { method: "POST", credentials: "same-origin", keepalive: true });
4239        } catch (e) { /* тема уже применена локально, ошибка сети не критична */ }
4240    }
4241
4242    function isDark(pref) {
4243        return pref === "dark" || (pref === "system" && !!(media && media.matches));
4244    }
4245
4246    function apply(pref) {
4247        root.setAttribute("data-theme-pref", pref);
4248        if (isDark(pref)) {
4249            root.setAttribute("data-bs-theme", "dark");
4250        } else {
4251            root.removeAttribute("data-bs-theme");
4252        }
4253
4254        var meta = document.querySelector('meta[name="theme-color"]');
4255        if (meta) {
4256            meta.setAttribute("content", isDark(pref) ? "#0E1116" : "#00a0df");
4257        }
4258
4259        document.querySelectorAll(".js-theme-option").forEach(function (btn) {
4260            var active = btn.getAttribute("data-theme-value") === pref;
4261            btn.classList.toggle("is-active", active);
4262            btn.setAttribute("aria-checked", active ? "true" : "false");
4263        });
4264    }
4265
4266    var initial = readPref();
4267    apply(initial);
4268    if (isAllowed()) {
4269        document.cookie = COOKIE + "=" + initial + ";path=/;max-age=31536000;samesite=lax";
4270    }
4271
4272    document.addEventListener("click", function (e) {
4273        var btn = e.target.closest ? e.target.closest(".js-theme-option") : null;
4274        if (!btn) { return; }
4275        e.preventDefault();
4276        var pref = btn.getAttribute("data-theme-value");
4277        if (VALUES.indexOf(pref) === -1 || !isAllowed()) { return; }
4278        savePref(pref);
4279        apply(pref);
4280    });
4281
4282    if (media) {
4283        var onChange = function () {
4284            if (readPref() === "system") { apply("system"); }
4285        };
4286        if (media.addEventListener) {
4287            media.addEventListener("change", onChange);
4288        } else if (media.addListener) {
4289            media.addListener(onChange);
4290        }
4291    }
4292})();
4293
4294/* Одноразовый за сессию замер того, какое оформление стоит у пользователя в ОС:
4295   нужен, чтобы решать про тему сайта по своим цифрам. Только для авторизованных. */
4296(function () {
4297    if (document.documentElement.getAttribute("data-theme-allowed") !== "1") { return; }
4298
4299    try {
4300        if (sessionStorage.getItem("ua_scheme_probe") === "1") { return; }
4301    } catch (e) { return; }
4302
4303    var media = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
4304    var scheme = media ? (media.matches ? "dark" : "light") : "unknown";
4305    var url = "/ux/scheme/" + scheme;
4306
4307    var send = function () {
4308        try {
4309            if (navigator.sendBeacon) {
4310                navigator.sendBeacon(url);
4311            } else {
4312                fetch(url, { method: "POST", keepalive: true, credentials: "same-origin" });
4313            }
4314            sessionStorage.setItem("ua_scheme_probe", "1");
4315        } catch (e) { /* замер не должен мешать сайту */ }
4316    };
4317
4318    if ("requestIdleCallback" in window) {
4319        requestIdleCallback(send, { timeout: 3000 });
4320    } else {
4321        setTimeout(send, 1500);
4322    }
4323})();

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.