PageSourceSearch

https://infogorzow.com/wp-content/plugins/gorzow-miejsca/assets/js/konta.js?ver=1790947966

js infogorzow.com collected 2026-10-03 02:34:48 UTC 50,372 bytes, 833 lines download raw bytes

1/**
2 * ============================================================
3 * GORZÓW KONTA - front-end paska logowania/rejestracji + modala.
4 * Plik identyczny w obu wtyczkach (assets/js/konta.js). Działa
5 * niezależnie od script.js każdej wtyczki - po zalogowaniu blokuje
6 * (na zielono, jak dotychczasowe "rozpoznanie po IP") wszystkie pola
7 * "nick zgłaszającego" znalezione na stronie, więc nie trzeba nic
8 * zmieniać w istniejącej logice formularzy.
9 * ============================================================
10 */
11(function () {
12    'use strict';
13    if (typeof gorzowKontaConfig === 'undefined') return;
14    if (window.__gorzowKontaZaladowany) return; // dwie wtyczki na stronie = dwa <div id="gk-pasek-konta"> mogą istnieć, ale skrypt inicjujemy raz na kontener
15    window.__gorzowKontaZaladowany = true;
16
17    const cfg = gorzowKontaConfig;
18    let stanKonta = null; // { zalogowany, login, nick, email } | null (jeszcze nie sprawdzono)
19    let captchaDane = null;
20
21    function escHtml(t) {
22        const d = document.createElement('div');
23        d.textContent = String(t == null ? '' : t);
24        return d.innerHTML;
25    }
26
27    // Strony z pamięci podręcznej mają stary nonce - pobieramy świeży i
28    // przekazujemy go też wtyczkom (Mapa Miejsc, Ceny Paliw).
29    async function odswiezNonce() {
30        if (!cfg.restUrlNonce) return false;
31        try {
32            const r = await fetch(cfg.restUrlNonce, { credentials: 'same-origin', cache: 'no-store' });
33            if (!r.ok) return false;
34            const j = await r.json();
35            if (!j || !j.nonce) return false;
36            cfg.nonce = j.nonce;
37            ['gorzowMiejscaConfig', 'gorzowPaliwaConfig'].forEach((n) => { if (window[n]) window[n].nonce = j.nonce; });
38            return true;
39        } catch (e) {
40            return false;
41        }
42    }
43
44    async function postJson(url, dane, ponowienie) {
45        const body = new URLSearchParams(Object.assign({ tf: cfg.czasFormularza, www: '' }, dane));
46        let resp;
47        try {
48            resp = await fetch(url, {
49                method: 'POST',
50                credentials: 'same-origin',
51                headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-WP-Nonce': cfg.nonce },
52                body: body.toString(),
53            });
54        } catch (e) {
55            return { ok: false, dane: { message: 'Brak połączenia z internetem - spróbuj ponownie.' } };
56        }
57        let j = {};
58        try { j = await resp.json(); } catch (e) { /* noop */ }
59        const zlyNonce = resp.status === 403 && j && /zla_autoryzacja|invalid_nonce/.test(String(j.code || ''));
60        if (zlyNonce && !ponowienie && await odswiezNonce()) {
61            return postJson(url, dane, true);
62        }
63        if (resp.status === 429 && !j.message) j.message = 'Zbyt wiele prób - odczekaj chwilę.';
64        return { ok: resp.ok, dane: j };
65    }
66
67    async function pobierzStanKonta() {
68        try {
69            const resp = await fetch(cfg.restUrlJa, { headers: { 'X-WP-Nonce': cfg.nonce } });
70            stanKonta = await resp.json();
71        } catch (e) {
72            stanKonta = { zalogowany: false };
73        }
74        window.gorzowKontaStan = stanKonta;
75        try { document.dispatchEvent(new CustomEvent('gorzow-konta:stan', { detail: stanKonta })); } catch (e) { /* noop */ }
76        return stanKonta;
77    }
78
79    function zamknijMenuKonta() {
80        document.querySelectorAll('.gk-konto-menu').forEach((m) => { m.hidden = true; });
81        document.querySelectorAll('.gk-konto-przycisk').forEach((b) => b.setAttribute('aria-expanded', 'false'));
82    }
83
84    /**
85     * Wspólne okno "Czy na pewno?" (używają go też wtyczki, np. przy
86     * usuwaniu danych powiązanych z urządzeniem). Zwraca Promise<boolean>.
87     * Przycisk potwierdzenia jest aktywny dopiero po zaznaczeniu
88     * "Rozumiem, że tego nie da się cofnąć".
89     */
90    function potwierdz(opcje) {
91        const o = opcje || {};
92        return new Promise((resolve) => {
93            otworzModal(`
94                <button type="button" class="gk-modal-zamknij" data-gk-potwierdz="nie" aria-label="Zamknij"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg></button>
95                <div class="gk-potwierdz-ikona" aria-hidden="true">⚠️</div>
96                <h2>${escHtml(o.tytul || 'Na pewno?')}</h2>
97                <p class="gk-modal-info">${escHtml(o.tresc || '')}</p>
98                <label class="gk-potwierdz-check"><input type="checkbox" id="gk-potwierdz-check"> Rozumiem, że tej operacji nie da się cofnąć</label>
99                <div class="gk-potwierdz-przyciski">
100                    <button type="button" class="gk-pasek-btn" data-gk-potwierdz="nie">Anuluj</button>
101                    <button type="button" class="gk-modal-submit gk-modal-submit-usun" data-gk-potwierdz="tak" disabled>${escHtml(o.przycisk || 'Tak, usuń')}</button>
102                </div>`);
103            const modal = document.getElementById('gk-modal-konto');
104            const check = document.getElementById('gk-potwierdz-check');
105            const tak = modal.querySelector('[data-gk-potwierdz="tak"]');
106            check.addEventListener('change', () => { tak.disabled = !check.checked; });
107            const koniec = (wynik) => {
108                modal.removeEventListener('click', klik);
109                zamknijModal();
110                resolve(wynik);
111            };
112            const klik = (e) => {
113                const b = e.target.closest('[data-gk-potwierdz]');
114                if (!b || b.disabled) return;
115                koniec(b.dataset.gkPotwierdz === 'tak');
116            };
117            modal.addEventListener('click', klik);
118        });
119    }
120    window.gorzowKonta = { potwierdz: potwierdz };
121
122    function renderUsuniecie() {
123        otworzModal(`
124            <button type="button" class="gk-modal-zamknij" data-gk="zamknij" aria-label="Zamknij"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg></button>
125            <div class="gk-potwierdz-ikona" aria-hidden="true">⚠️</div>
126            <h2>Usunąć konto?</h2>
127            <p class="gk-modal-info">Konto <strong>${escHtml(stanKonta && stanKonta.nick || '')}</strong> zostanie usunięte w Mapie Miejsc i w Cenach Paliw. Dla bezpieczeństwa wyślemy Ci e-mail z linkiem - konto zniknie dopiero po jego kliknięciu.</p>
128            <div class="gk-modal-status" id="gk-modal-status"></div>
129            <form id="gk-form-usun">
130                <label class="gk-modal-pole">Twoje aktualne hasło<input type="password" name="haslo" required autocomplete="current-password"></label>
131                <label class="gk-potwierdz-check"><input type="checkbox" name="rozumiem" id="gk-usun-check"> Na pewno chcę usunąć konto - rozumiem, że tego nie da się cofnąć</label>
132                <div class="gk-honeypot" aria-hidden="true"><label>Zostaw puste<input type="text" name="www" tabindex="-1" autocomplete="off"></label></div>
133                <div class="gk-potwierdz-przyciski">
134                    <button type="button" class="gk-pasek-btn" data-gk="zamknij">Anuluj</button>
135                    <button type="submit" class="gk-modal-submit gk-modal-submit-usun" disabled>Usuń konto</button>
136                </div>
137            </form>`);
138        const form = document.getElementById('gk-form-usun');
139        const przycisk = form.querySelector('button[type="submit"]');
140        const odswiez = () => { przycisk.disabled = !(form.rozumiem.checked && form.haslo.value.length > 0); };
141        form.addEventListener('input', odswiez);
142        form.addEventListener('change', odswiez);
143        form.addEventListener('submit', async (e) => {
144            e.preventDefault();
145            if (przycisk.disabled) return;
146            const status = document.getElementById('gk-modal-status');
147            status.className = 'gk-modal-status';
148            status.textContent = 'Wysyłanie…';
149            przycisk.disabled = true;
150            const { ok, dane } = await postJson(cfg.restUrlUsunZadaj, { haslo: form.haslo.value });
151            status.className = ok ? 'gk-modal-status gk-modal-status-ok' : 'gk-modal-status gk-modal-status-blad';
152            status.textContent = dane.wiadomosc || dane.message || (ok ? 'Sprawdź e-mail.' : 'Nie udało się.');
153            if (ok) form.querySelectorAll('input').forEach((i) => { i.disabled = true; });
154            else odswiez();
155        });
156    }
157
158    /** Blokuje (nick + odznaka konta) wszystkie znane pola nicku na stronie. */
159    function zablokujPolaNickiem(nick) {
160        document.querySelectorAll('input[name="zglaszajacy"], input[name="autor"]').forEach((pole) => {
161            pole.value = nick;
162            pole.readOnly = true;
163            pole.classList.add('gm-pole-zablokowane', 'gp-pole-zablokowane');
164        });
165        document.querySelectorAll('[id$="-moj-nick-formularz"], [id*="-moj-nick-opinia-"]').forEach((box) => {
166            if (box.dataset.gkWypelnione) return; // nie nadpisuj, jeśli własna wtyczka już coś tam wstawiła (odznaki itp.)
167        });
168    }
169
170    // Po zalogowaniu pigułka konta trafia do prawego rogu nagłówka wtyczki
171    // (Mapa miejsc / Ceny paliw), zamiast zajmować osobny, pusty wiersz.
172    function przeniesDoNaglowka(pasek) {
173        let naglowek = pasek.previousElementSibling;
174        if (pasek.parentElement && pasek.parentElement.matches('.gm-header, .gp-header')) naglowek = pasek.parentElement;
175        if (!naglowek || !naglowek.matches('.gm-header, .gp-header')) return;
176        naglowek.classList.add('gk-naglowek-z-kontem');
177        pasek.classList.add('gk-pasek-w-naglowku');
178        if (pasek.parentElement !== naglowek) naglowek.appendChild(pasek);
179    }
180
181    function renderPasek() {
182        const pasek = document.getElementById('gk-pasek-konta');
183        if (!pasek) return;
184        if (!stanKonta) {
185            pasek.innerHTML = '<span class="gk-pasek-ladowanie">…</span>';
186            return;
187        }
188        if (stanKonta.zalogowany) {
189            // Po zalogowaniu: zamiast paska z logowaniem - małe menu konta
190            // (Wyloguj / Usuń konto). "Usuń konto" jest schowane w menu
191            // i wymaga potwierdzenia.
192            const nick = String(stanKonta.nick || '');
193            pasek.classList.add('gk-pasek-zalogowany');
194            pasek.innerHTML = `<div class="gk-konto">
195                    <button type="button" class="gk-konto-przycisk" data-gk="menu" aria-haspopup="true" aria-expanded="false">
196                        ${htmlAwatara(nick)}
197                        <span class="gk-konto-nick">${escHtml(nick)}</span>
198                        <svg class="gk-konto-strzalka" viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
199                    </button>
200                    <div class="gk-konto-menu" role="menu" hidden>
201                        <div class="gk-konto-menu-glowa">
202                            ${htmlAwatara(nick)}
203                            <span><small>Zalogowano jako</small><strong>${escHtml(nick)}</strong>${stanKonta.email ? `<em>${escHtml(stanKonta.email)}</em>` : ''}</span>
204                        </div>
205                        <button type="button" class="gk-konto-pozycja" role="menuitem" data-gk="awatar"><span class="gk-konto-pozycja-ikona" aria-hidden="true">📷</span>${stanKonta.awatar ? 'Zmień zdjęcie profilowe' : 'Dodaj zdjęcie profilowe'}</button>
206                        ${(window.gorzowKontaMenu || []).map((d) => `<button type="button" class="gk-konto-pozycja" role="menuitem" data-gk-dodatek="${escHtml(d.id)}"><span class="gk-konto-pozycja-ikona" aria-hidden="true">${escHtml(d.ikona || '•')}</span>${escHtml(d.etykieta)}</button>`).join('')}
207                        ${(window.gorzowKontaMenu || []).length ? '<div class="gk-konto-menu-kreska"></div>' : ''}
208                        <button type="button" class="gk-konto-pozycja" role="menuitem" data-gk="wyloguj">
209                            <svg viewBox="0 0 20 20" width="16" height="16" fill="none" aria-hidden="true"><path d="M8 4H5a1 1 0 00-1 1v10a1 1 0 001 1h3M13 13.5L16.5 10 13 6.5M16.5 10H8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
210                            Wyloguj się
211                        </button>
212                        <div class="gk-konto-menu-kreska"></div>
213                        <button type="button" class="gk-konto-pozycja gk-konto-pozycja-usun" role="menuitem" data-gk="usun">
214                            <svg viewBox="0 0 20 20" width="16" height="16" fill="none" aria-hidden="true"><path d="M4.5 6h11M8 6V4.5h4V6M6 6l.7 9.5a1 1 0 001 .9h4.6a1 1 0 001-.9L14 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
215                            Usuń konto…
216                        </button>
217                    </div>
218                </div>`;
219            zablokujPolaNickiem(stanKonta.nick);
220            przeniesDoNaglowka(pasek);
221        } else {
222            // Bez logowania: zamiast dużej belki - mała, dyskretna pigułka
223            // "Zaloguj się" w rogu nagłówka z krótkim menu (Zaloguj / Załóż
224            // konto). Konto jest opcjonalne - nie zabiera miejsca.
225            pasek.classList.remove('gk-pasek-zalogowany');
226            pasek.classList.add('gk-pasek-gosc');
227            pasek.innerHTML = `<div class="gk-konto">
228                    <button type="button" class="gk-konto-przycisk gk-konto-gosc" data-gk="menu" aria-haspopup="true" aria-expanded="false" title="Konto jest opcjonalne - pozwala używać tego samego nicku na każdym urządzeniu">
229                        <span class="gk-pasek-awatar gk-pasek-awatar-gosc" aria-hidden="true"><svg viewBox="0 0 24 24" width="15" height="15" fill="none"><circle cx="12" cy="8.5" r="3.6" stroke="currentColor" stroke-width="2"/><path d="M5 19.5c1.2-3.4 3.9-5.1 7-5.1s5.8 1.7 7 5.1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>
230                        <span class="gk-konto-nick">Zaloguj się</span>
231                        <svg class="gk-konto-strzalka" viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
232                    </button>
233                    <div class="gk-konto-menu gk-konto-menu-gosc" role="menu" hidden>
234                        <div class="gk-konto-menu-info"><strong>Konto jest opcjonalne</strong><small>Możesz korzystać ze wszystkiego bez logowania. Konto zachowuje Twój nick, punkty i paszport na każdym urządzeniu.</small></div>
235                        <button type="button" class="gk-konto-pozycja gk-konto-pozycja-glowna" role="menuitem" data-gk="logowanie">
236                            <svg viewBox="0 0 20 20" width="16" height="16" fill="none" aria-hidden="true"><path d="M11 4h4a1 1 0 011 1v10a1 1 0 01-1 1h-4M8 13.5L11.5 10 8 6.5M11.5 10H3.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
237                            Zaloguj się
238                        </button>
239                        <button type="button" class="gk-konto-pozycja" role="menuitem" data-gk="rejestracja">
240                            <svg viewBox="0 0 20 20" width="16" height="16" fill="none" aria-hidden="true"><circle cx="10" cy="10" r="7" stroke="currentColor" stroke-width="1.6"/><path d="M10 6.8v6.4M6.8 10h6.4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
241                            Załóż darmowe konto
242                        </button>
243                    </div>
244                </div>`;
245            przeniesDoNaglowka(pasek);
246        }
247    }
248
249    // Zdjęcie profilowe albo pierwsza litera nicku.
250    function htmlAwatara(nick, klasa) {
251        const url = stanKonta && stanKonta.awatar ? String(stanKonta.awatar) : '';
252        return url
253            ? `<span class="gk-pasek-awatar gk-ma-zdjecie${klasa ? ' ' + klasa : ''}" aria-hidden="true"><img src="${escHtml(url)}" alt="" loading="lazy" decoding="async"></span>`
254            : `<span class="gk-pasek-awatar${klasa ? ' ' + klasa : ''}" aria-hidden="true">${escHtml(String(nick || '').charAt(0).toUpperCase() || '?')}</span>`;
255    }
256    window.gorzowKontaAwatar = htmlAwatara;
257
258    // Zmniejszenie i przycięcie zdjęcia do kwadratu w przeglądarce
259    // (mniejszy plik = szybsze wysyłanie; serwer i tak przycina do 256 px).
260    function przygotujAwatar(plik) {
261        return new Promise((resolve, reject) => {
262            if (!/^image\/(jpeg|png|webp)$/.test(plik.type)) { reject(new Error('Wybierz zdjęcie JPG, PNG lub WEBP.')); return; }
263            if (plik.size > 15 * 1024 * 1024) { reject(new Error('Plik jest za duży (maks. 15 MB).')); return; }
264            const url = URL.createObjectURL(plik);
265            const img = new Image();
266            img.onload = () => {
267                const bok = Math.min(img.naturalWidth, img.naturalHeight);
268                if (bok < 64) { URL.revokeObjectURL(url); reject(new Error('Zdjęcie jest za małe (min. 64 x 64 px).')); return; }
269                const c = document.createElement('canvas');
270                c.width = c.height = 512;
271                const ctx = c.getContext('2d');
272                ctx.fillStyle = '#ffffff';
273                ctx.fillRect(0, 0, 512, 512);
274                ctx.drawImage(img, (img.naturalWidth - bok) / 2, (img.naturalHeight - bok) / 2, bok, bok, 0, 0, 512, 512);
275                URL.revokeObjectURL(url);
276                c.toBlob((b) => (b ? resolve({ blob: b, podglad: c.toDataURL('image/jpeg', 0.85) }) : reject(new Error('Nie udało się przygotować zdjęcia.'))), 'image/jpeg', 0.88);
277            };
278            img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Nie udało się odczytać zdjęcia.')); };
279            img.src = url;
280        });
281    }
282
283    function renderAwatar() {
284        if (!stanKonta || !stanKonta.zalogowany) { renderLogowanie(); return; }
285        const nick = String(stanKonta.nick || '');
286        const ma = !!stanKonta.awatar;
287        otworzModal(`${naglowekModala('uzytkownik', 'Zdjęcie profilowe', 'Widoczne przy Twoim nicku w obu serwisach.', 'fiolet')}
288            <div class="gk-aw">
289                <label class="gk-aw-podglad" for="gk-aw-plik" title="Wybierz zdjęcie">
290                    ${ma ? `<img src="${escHtml(stanKonta.awatar)}" alt="">` : `<span>${escHtml(nick.charAt(0).toUpperCase() || '?')}</span>`}
291                    <i aria-hidden="true"><svg viewBox="0 0 20 20" width="16" height="16" fill="none"><path d="M4 7h2.5l1.2-2h4.6l1.2 2H16v8.5H4z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><circle cx="10" cy="11" r="2.6" stroke="currentColor" stroke-width="1.6"/></svg></i>
292                </label>
293                <input type="file" id="gk-aw-plik" accept="image/jpeg,image/png,image/webp" hidden>
294                <div class="gk-aw-strefa" data-gk-aw="strefa">Przeciągnij zdjęcie tutaj albo <button type="button" class="gk-link" data-gk-aw="wybierz">wybierz z urządzenia</button><small>JPG, PNG lub WEBP • zdjęcie zostanie przycięte do kwadratu</small></div>
295                <div class="gk-m-status" data-gk-aw="status" aria-live="polite"></div>
296                <div class="gk-aw-przyciski">
297                    ${ma ? '<button type="button" class="gk-btn gk-btn-drugi" data-gk-aw="usun">Usuń zdjęcie</button>' : ''}
298                    <button type="button" class="gk-btn gk-btn-glowny" data-gk-aw="zapisz" disabled>Zapisz zdjęcie</button>
299                </div>
300            </div>`);
301        const modal = document.getElementById('gk-modal-konto');
302        const plik = modal.querySelector('#gk-aw-plik');
303        const podglad = modal.querySelector('.gk-aw-podglad');
304        const status = modal.querySelector('[data-gk-aw="status"]');
305        const zapisz = modal.querySelector('[data-gk-aw="zapisz"]');
306        const strefa = modal.querySelector('[data-gk-aw="strefa"]');
307        let gotowy = null;
308        const pokazStatus = (t, blad) => { status.textContent = t; status.classList.toggle('is-blad', !!blad); };
309        const przyjmij = async (f) => {
310            if (!f) return;
311            try {
312                pokazStatus('Przygotowuję zdjęcie…');
313                gotowy = await przygotujAwatar(f);
314                podglad.querySelector('img, span') && podglad.querySelector('img, span').remove();
315                podglad.insertAdjacentHTML('afterbegin', `<img src="${gotowy.podglad}" alt="">`);
316                pokazStatus('Wygląda dobrze? Kliknij „Zapisz zdjęcie”.');
317                zapisz.disabled = false;
318            } catch (err) {
319                gotowy = null;
320                zapisz.disabled = true;
321                pokazStatus(err.message, true);
322            }
323        };
324        plik.addEventListener('change', () => przyjmij(plik.files[0]));
325        modal.querySelector('[data-gk-aw="wybierz"]').addEventListener('click', () => plik.click());
326        ['dragover', 'dragenter'].forEach((t) => strefa.addEventListener(t, (e) => { e.preventDefault(); strefa.classList.add('is-nad'); }));
327        ['dragleave', 'drop'].forEach((t) => strefa.addEventListener(t, (e) => { e.preventDefault(); strefa.classList.remove('is-nad'); }));
328        strefa.addEventListener('drop', (e) => przyjmij(e.dataTransfer && e.dataTransfer.files[0]));
329        const wyslij = async (url, dane) => {
330            const zrob = () => fetch(url, { method: 'POST', credentials: 'same-origin', headers: { 'X-WP-Nonce': cfg.nonce }, body: dane });
331            let r = await zrob();
332            if (r.status === 403 && await odswiezNonce()) r = await zrob();
333            let j = {};
334            try { j = await r.json(); } catch (e) { /* nic */ }
335            return { ok: r.ok, j };
336        };
337        const poZmianie = (url) => {
338            stanKonta = Object.assign({}, stanKonta, { awatar: url || '' });
339            window.gorzowKontaStan = stanKonta;
340            renderPasek();
341            try { document.dispatchEvent(new CustomEvent('gorzow-konta:stan', { detail: stanKonta })); } catch (e) { /* nic */ }
342        };
343        zapisz.addEventListener('click', async () => {
344            if (!gotowy || !cfg.restUrlAwatar) return;
345            zapisz.disabled = true;
346            pokazStatus('Wysyłanie…');
347            const fd = new FormData();
348            fd.append('plik', gotowy.blob, 'awatar.jpg');
349            const { ok, j } = await wyslij(cfg.restUrlAwatar, fd);
350            if (ok && j.awatar) {
351                poZmianie(j.awatar);
352                pokazStatus('Zapisano! 🎉');
353                setTimeout(zamknijModal, 700);
354            } else {
355                pokazStatus(j.message || 'Nie udało się zapisać zdjęcia.', true);
356                zapisz.disabled = false;
357            }
358        });
359        const usun = modal.querySelector('[data-gk-aw="usun"]');
360        if (usun) usun.addEventListener('click', async () => {
361            usun.disabled = true;
362            const { ok, j } = await wyslij(cfg.restUrlAwatarUsun, new FormData());
363            if (ok) { poZmianie(''); zamknijModal(); } else { pokazStatus(j.message || 'Nie udało się usunąć zdjęcia.', true); usun.disabled = false; }
364        });
365    }
366
367    function otworzModal(tresc) {
368        const tlo = document.getElementById('gk-modal-tlo');
369        const modal = document.getElementById('gk-modal-konto');
370        if (!tlo || !modal) return;
371        modal.innerHTML = tresc;
372        modal.classList.remove('gk-modal-szeroki');
373        modal.classList.add('gk-modal-v2');
374        modal.setAttribute('aria-labelledby', 'gk-m-tytul');
375        tlo.hidden = false;
376        requestAnimationFrame(() => tlo.classList.add('gk-widoczny'));
377        document.body.style.overflow = 'hidden';
378        const pierwszePole = modal.querySelector('input:not([type="hidden"]):not([tabindex="-1"])');
379        if (pierwszePole && window.innerWidth > 640) pierwszePole.focus();
380    }
381    function zamknijModal() {
382        const tlo = document.getElementById('gk-modal-tlo');
383        if (tlo) { tlo.hidden = true; tlo.classList.remove('gk-widoczny'); }
384        document.body.style.overflow = '';
385    }
386
387    async function pobierzCaptche() {
388        try {
389            const resp = await fetch(cfg.restUrlCaptcha, { headers: { 'X-WP-Nonce': cfg.nonce } });
390            captchaDane = await resp.json();
391        } catch (e) {
392            captchaDane = { token: '', pytanie: '?' };
393        }
394        return captchaDane;
395    }
396
397    function polePytanieCaptcha() {
398        if (captchaDane && captchaDane.recaptchaSiteKey) {
399            return `<div class="g-recaptcha" data-sitekey="${escHtml(captchaDane.recaptchaSiteKey)}" data-callback="gorzowKontaRecaptchaOk"></div><input type="hidden" name="recaptcha_token" id="gk-recaptcha-token">`;
400        }
401        return `<label class="gk-modal-pole gk-m-captcha-pole"><span>Ile to jest <strong>${escHtml(captchaDane ? captchaDane.pytanie : '…')}</strong>?</span>
402            <input type="text" inputmode="numeric" name="captcha_odpowiedz" maxlength="3" required autocomplete="off"></label>
403            <input type="hidden" name="captcha_token" value="${esc
403Html(captchaDane ? captchaDane.token : '')}">`;
404    }
405
406    window.gorzowKontaRecaptchaOk = function (token) {
407        const pole = document.getElementById('gk-recaptcha-token');
408        if (pole) pole.value = token;
409    };
410
411    // ------------------------------------------------------------
412    // Ładniejsze okna logowania / rejestracji / resetu (v2)
413    // ------------------------------------------------------------
414    const IKONY = {
415        uzytkownik: '<circle cx="10" cy="7" r="3.2"/><path d="M4 16.5c1-3 3.3-4.5 6-4.5s5 1.5 6 4.5"/>',
416        klodka: '<rect x="4.5" y="9" width="11" height="8" rx="2"/><path d="M7 9V6.8a3 3 0 0 1 6 0V9"/>',
417        mail: '<rect x="3" y="5" width="14" height="10" rx="2"/><path d="M3.5 6l6.5 5 6.5-5"/>',
418        znaczek: '<path d="M10 3l2 4.2 4.5.6-3.3 3.1.8 4.6L10 13.3l-4 2.2.8-4.6L3.5 7.8 8 7.2z"/>',
419        tarcza: '<path d="M10 2.8l6 2.4v4.3c0 3.8-2.6 6.4-6 7.7-3.4-1.3-6-3.9-6-7.7V5.2z"/><path d="M7.5 10l1.8 1.8 3.4-3.6"/>',
420        oko: '<path d="M2.5 10s2.8-5 7.5-5 7.5 5 7.5 5-2.8 5-7.5 5-7.5-5-7.5-5z"/><circle cx="10" cy="10" r="2.3"/>',
421        okoZamkniete: '<path d="M3 3l14 14M8.3 5.3A7.7 7.7 0 0 1 10 5c4.7 0 7.5 5 7.5 5a13 13 0 0 1-2.3 2.9M12.2 12.4A2.3 2.3 0 0 1 8 10c0-.6.2-1.2.6-1.6M5.5 7.2A12.6 12.6 0 0 0 2.5 10s2.8 5 7.5 5c1 0 1.9-.2 2.7-.5"/>',
422        klucz: '<circle cx="7" cy="12.5" r="3"/><path d="M9.2 10.3L16 3.5M13.5 6l2 2M11.8 7.7l1.5 1.5"/>',
423        wejscie: '<path d="M11 4h4a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-4M8 13.5L11.5 10 8 6.5M11.5 10H3.5"/>',
424        plus: '<circle cx="10" cy="10" r="7"/><path d="M10 6.8v6.4M6.8 10h6.4"/>',
425    };
426    const ikona = (nazwa, r) => `<svg viewBox="0 0 20 20" width="${r || 18}" height="${r || 18}" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${IKONY[nazwa]}</svg>`;
427
428    const pamiecFormularza = { login: '' };
429
430    function bokModala() {
431        return `<aside class="gk-m-bok" aria-hidden="true">
432            <span class="gk-m-bok-logo">G</span>
433            <strong>Jedno konto,<br>dwa serwisy</strong>
434            <p>Mapa Miejsc i Ceny Paliw Gorzowa - z tym samym nickiem na każdym urządzeniu.</p>
435            <ul>
436                <li><span>📍</span>Dodawaj miejsca i opinie pod swoim nickiem</li>
437                <li><span>🏅</span>Zbieraj punkty, odznaki i miejsca w rankingu</li>
438                <li><span>⛽</span>Zgłaszaj ceny paliw i ustaw alerty</li>
439                <li><span>🔒</span>Bez reklam i spamu - tylko to, co potrzebne</li>
440            </ul>
441        </aside>`;
442    }
443
444    function otworzModalKonta(tresc) {
445        otworzModal(`<div class="gk-m-uklad">${bokModala()}<div class="gk-m-glowny">${tresc}</div></div>`);
446        const modal = document.getElementById('gk-modal-konto');
447        if (modal) modal.classList.add('gk-modal-szeroki');
448    }
449
450    function naglowekModala(ikonaNazwa, tytul, podtytul, kolor) {
451        return `<button type="button" class="gk-modal-zamknij" data-gk="zamknij" aria-label="Zamknij"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg></button>
452            <div class="gk-m-glowa">
453                <span class="gk-m-ikona gk-m-ikona-${kolor || 'fiolet'}">${ikona(ikonaNazwa, 22)}</span>
454                <h2 id="gk-m-tytul">${escHtml(tytul)}</h2>
455                ${podtytul ? `<p>${escHtml(podtytul)}</p>` : ''}
456            </div>`;
457    }
458
459    function zakladki(aktywna) {
460        return `<div class="gk-m-zakladki" role="tablist">
461            <button type="button" role="tab" aria-selected="${aktywna === 'logowanie'}" class="${aktywna === 'logowanie' ? 'is-on' : ''}" data-gk="logowanie">Logowanie</button>
462            <button type="button" role="tab" aria-selected="${aktywna === 'rejestracja'}" class="${aktywna === 'rejestracja' ? 'is-on' : ''}" data-gk="rejestracja">Załóż konto</button>
463        </div>`;
464    }
465
466    function pole(o) {
467        const haslo = o.typ === 'password';
468        const wartosc = o.nazwa === 'login' && pamiecFormularza.login ? ` value="${esc
468Html(pamiecFormularza.login)}"` : '';
469        return `<label class="gk-m-pole gk-m-pole-plywajace">
470            <span class="gk-m-wejscie">
471                ${ikona(o.ikona)}
472                <input type="${o.typ || 'text'}" name="${o.nazwa}" ${o.atrybuty || ''}${wartosc} placeholder=" ">
473                <span class="gk-m-etykieta">${escHtml(o.etykieta)}${o.placeholder ? `<em> - ${escHtml(o.placeholder)}</em>` : ''}</span>
474                ${haslo ? `<button type="button" class="gk-m-oko" data-gk-oko aria-label="Pokaż hasło" title="Pokaż hasło">${ikona('oko')}</button>` : ''}
475            </span>
476            ${haslo ? '<small class="gk-m-capslock" hidden>⇪ Caps Lock jest włączony</small>' : ''}
477            ${o.podpowiedz ? `<small class="gk-m-podpowiedz" data-gk-podpowiedz="${o.nazwa}">${o.podpowiedz}</small>` : ''}
478        </label>`;
479    }
480
481    function przyciskWyslij(tekst, ik) {
482        return `<button type="submit" class="gk-modal-submit gk-m-wyslij"><span class="gk-m-spinner" aria-hidden="true"></span>${ik ? ikona(ik) : ''}<span>${escHtml(tekst)}</span></button>`;
483    }
484
485    function ustawLadowanie(form, tak) {
486        const b = form.querySelector('.gk-m-wyslij');
487        if (!b) return;
488        b.disabled = tak;
489        b.classList.toggle('is-laduje', tak);
490    }
491
492    function ustawStatus(typ, tekst) {
493        const status = document.getElementById('gk-modal-status');
494        if (!status) return;
495        status.className = 'gk-modal-status' + (typ ? ' gk-modal-status-' + typ : '');
496        status.textContent = tekst || '';
497    }
498
499    // Siła hasła: 0-4.
500    function silaHasla(h) {
501        if (!h) return 0;
502        let p = 0;
503        if (h.length >= 8) p++;
504        if (/[a-ząćęłńóśźż]/i.test(h) && /\d/.test(h)) p++;
505        if (/[a-ząćęłńóśźż]/.test(h) && /[A-ZĄĆĘŁŃÓŚŹŻ]/.test(h)) p++;
506        if (/[^A-Za-z0-9ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/.test(h) || h.length >= 12) p++;
507        if (h.length < 8) p = Math.min(p, 1);
508        return p;
509    }
510
511    function captchaHtml() {
512        return `<div class="gk-m-captcha">
513            <span class="gk-m-captcha-ikona">${ikona('tarcza')}</span>
514            ${polePytanieCaptcha()}
515        </div>`;
516    }
517
518    async function renderLogowanie() {
519        otworzModalKonta(`
520            ${naglowekModala('wejscie', 'Witaj ponownie', 'Jedno konto działa w Mapie Miejsc i w Cenach Paliw.')}
521            ${zakladki('logowanie')}
522            <div class="gk-modal-status" id="gk-modal-status" role="status"></div>
523            <form id="gk-form-logowanie" novalidate>
524                ${pole({ etykieta: 'Login lub e-mail', nazwa: 'login', ikona: 'uzytkownik', atrybuty: 'required autocomplete="username" maxlength="100"', placeholder: 'np. jan.kowalski' })}
525                ${pole({ etykieta: 'Hasło', nazwa: 'haslo', typ: 'password', ikona: 'klodka', atrybuty: 'required autocomplete="current-password" maxlength="200"', placeholder: '••••••••' })}
526                <div id="gk-login-captcha"></div>
527                <div class="gk-honeypot" aria-hidden="true"><label>Zostaw puste<input type="text" name="www" tabindex="-1" autocomplete="off"></label></div>
528                <div class="gk-m-wiersz"><a href="#" data-gk="reset">Nie pamiętasz hasła?</a></div>
529                ${przyciskWyslij('Zaloguj się', 'wejscie')}
530            </form>
531            <p class="gk-m-stopka">${ikona('tarcza', 14)} Połączenie chronione • limit prób logowania</p>`);
532
533        const form = document.getElementById('gk-form-logowanie');
534        form.addEventListener('submit', async (e) => {
535            e.preventDefault();
536            if (!form.login.value.trim() || !form.haslo.value) {
537                ustawStatus('blad', 'Podaj login i hasło.');
538                return;
539            }
540            ustawLadowanie(form, true);
541            ustawStatus('', 'Logowanie…');
542            const { ok, dane } = await postJson(cfg.restUrlLogowanie, Object.fromEntries(new FormData(form)));
543            ustawLadowanie(form, false);
544            if (ok) {
545                const glowny = form.closest('.gk-m-glowny') || form.parentElement;
546                glowny.innerHTML = `<div class="gk-m-zalogowano"><span class="gk-m-ptaszek" aria-hidden="true"><svg viewBox="0 0 52 52"><circle cx="26" cy="26" r="24"/><path d="M15 27l7 7 15-16"/></svg></span>
547                    <strong>Cześć, ${escHtml(dane.nick || '')}!</strong><small>Zalogowano - odświeżam stronę…</small></div>`;
548                stanKonta = { zalogowany: true, nick: dane.nick };
549                window.gorzowKontaStan = stanKonta;
550                // Przeładowanie odświeża nick w formularzach, rankingach i
551                // profilach obu wtyczek (bez tego część stanu była nieaktualna).
552                setTimeout(() => window.location.reload(), 1100);
553                return;
554            }
555            const wymagaCaptchy = dane && (dane.code === 'gorzow_konta_wymagana_captcha' || (dane.data && dane.data.wymagaCaptchy));
556            if (wymagaCaptchy) {
557                await pobierzCaptche();
558                const k = document.getElementById('gk-login-captcha');
559                if (k) {
560                    k.innerHTML = captchaHtml();
561                    const pc = k.querySelector('input[name="captcha_odpowiedz"]');
562                    if (pc) pc.focus();
563                }
564            }
565            ustawStatus('blad', (dane && dane.message) || 'Nie udało się zalogować.');
566            form.haslo.value = wymagaCaptchy ? form.haslo.value : '';
567        });
568    }
569
570    async function renderRejestracja() {
571        otworzModalKonta(`
572            ${naglowekModala('plus', 'Załóż darmowe konto', 'Twój nick w rankingach Mapy Miejsc i Cen Paliw - na każdym urządzeniu.', 'zielony')}
573            ${zakladki('rejestracja')}
574            <div class="gk-modal-status" id="gk-modal-status" role="status"></div>
575            <form id="gk-form-rejestracja" autocomplete="off" novalidate>
576                <div class="gk-m-dwa">
577                    ${pole({ etykieta: 'Login', nazwa: 'login', ikona: 'uzytkownik', atrybuty: 'minlength="3" maxlength="25" required autocomplete="username"', placeholder: 'do logowania', podpowiedz: '3-25 znaków: litery, cyfry, . - _' })}
578                    ${pole({ etykieta: 'Nick publiczny', nazwa: 'nick', ikona: 'znaczek', atrybuty: 'minlength="2" maxlength="30" required', placeholder: 'widoczny w rankingu' })}
579                </div>
580                ${pole({ etykieta: 'E-mail', nazwa: 'email', typ: 'email', ikona: 'mail', atrybuty: 'required autocomplete="email" maxlength="100"', placeholder: 'na link aktywacyjny' })}
581                ${pole({ etykieta: 'Hasło', nazwa: 'haslo', typ: 'password', ikona: 'klodka', atrybuty: 'minlength="8" maxlength="200" required autocomplete="new-password"', placeholder: 'min. 8 znaków, litera i cyfra' })}
582                <div class="gk-m-sila" aria-live="polite"><span class="gk-m-sila-paski"><i></i><i></i><i></i><i></i></span><small class="gk-m-sila-opis">Wpisz hasło</small></div>
583                ${pole({ etykieta: 'Powtórz hasło', nazwa: 'haslo2', typ: 'password', ikona: 'klodka', atrybuty: 'minlength="8" maxlength="200" required autocomplete="new-password"', placeholder: 'wpisz hasło jeszcze raz', podpowiedz: '&nbsp;' })}
584                <div id="gk-captcha-kontener">${captchaHtml()}</div>
585                <div class="gk-honeypot" aria-hidden="true"><label>Zostaw puste<input type="text" name="www" tabindex="-1" autocomplete="off"></label></div>
586                ${przyciskWyslij('Załóż konto', 'plus')}
587            </form>
588            <p class="gk-m-stopka">${ikona('tarcza', 14)} Hasło przechowujemy wyłącznie w formie zaszyfrowanej</p>`);
589        pobierzCaptche().then(() => {
590            const k = document.getElementById('gk-captcha-kontener');
591            if (k) k.innerHTML = captchaHtml();
592            if (captchaDane && captchaDane.recaptchaSiteKey && !window.__gkRecaptchaScript) {
593                window.__gkRecaptchaScript = true;
594                const sc = document.createElement('script');
595                sc.src = 'https://www.google.com/recaptcha/api.js';
596                sc.async = true; sc.defer = true;
597                document.head.appendChild(sc);
598            }
599        });
600
601        const form = document.getElementById('gk-form-rejestracja');
602        const sila = form.querySelector('.gk-m-sila');
603        const opisSily = form.querySelector('.gk-m-sila-opis');
604        const zgodnosc = form.querySelector('[data-gk-podpowiedz="haslo2"]');
605        const OPISY = ['Za krótkie', 'Słabe', 'Średnie', 'Dobre', 'Silne'];
606        const odswiez = () => {
607            const s = silaHasla(form.haslo.value);
608            sila.dataset.sila = form.haslo.value ? String(s) : '';
609            opisSily.textContent = form.haslo.value ? OPISY[s] + (s < 2 ? ' - dodaj cyfrę i więcej znaków' : '') : 'Wpisz hasło';
610            if (!form.haslo2.value) {
611                zgodnosc.innerHTML = '&nbsp;';
612                zgodnosc.className = 'gk-m-podpowiedz';
613            } else if (form.haslo2.value === form.haslo.value) {
614                zgodnosc.textContent = '✓ Hasła są zgodne';
615                zgodnosc.className = 'gk-m-podpowiedz is-ok';
616            } else {
617                zgodnosc.textContent = 'Hasła się różnią';
618                zgodnosc.className = 'gk-m-podpowiedz is-blad';
619            }
620        };
621        form.addEventListener('input', odswiez);
622
623        form.addEventListener('submit', async (e) => {
624            e.preventDefault();
625            const d = Object.fromEntries(new FormData(form));
626            if (!/^[A-Za-z0-9._-]{3,25}$/.test(d.login || '')) { ustawStatus('blad', 'Login: 3-25 znaków (litery, cyfry, kropka, myślnik, podkreślenie).'); form.login.focus(); return; }
627            if ((d.nick || '').trim().length < 2) { ustawStatus('blad', 'Nick musi mieć co najmniej 2 znaki.'); form.nick.focus(); return; }
628            if (!form.email.checkValidity() || !d.email) { ustawStatus('blad', 'Podaj prawidłowy adres e-mail.'); form.email.focus(); return; }
629            if (silaHasla(d.haslo) < 2 || !/\d/.test(d.haslo) || !/[A-Za-ząćęłńóśźżĄĆĘŁŃÓŚŹŻ]/.test(d.haslo)) { ustawStatus('blad', 'Hasło: min. 8 znaków, co najmniej jedna litera i jedna cyfra.'); form.haslo.focus(); return; }
630            if (d.haslo !== d.haslo2) { ustawStatus('blad', 'Hasła nie są takie same.'); form.haslo2.focus(); return; }
631
632            ustawLadowanie(form, true);
633            ustawStatus('', 'Zakładanie konta…');
634            const { ok, dane } = await postJson(cfg.restUrlRejestracja, d);
635            ustawLadowanie(form, false);
636            if (ok) {
637                const modal = document.getElementById('gk-modal-konto');
638                modal.innerHTML = `${naglowekModala('mail', 'Prawie gotowe!', '', 'z
638ielony')}
639                    <div class="gk-m-sukces"><p>${escHtml(dane.wiadomosc || 'Konto założone.')}</p>
640                    <button type="button" class="gk-modal-submit gk-m-wyslij" data-gk="logowanie"><span>Przejdź do logowania</span></button></div>`;
641                return;
642            }
643            ustawStatus('blad', (dane && (dane.message || dane.wiadomosc)) || 'Nie udało się założyć konta.');
644            await pobierzCaptche();
645            const k = document.getElementById('gk-captcha-kontener');
646            if (k) k.innerHTML = captchaHtml();
647        });
648    }
649
650    function renderReset() {
651        otworzModalKonta(`
652            ${naglowekModala('klucz', 'Reset hasła', 'Podaj login lub e-mail - wyślemy link do ustawienia nowego hasła.', 'niebieski')}
653            <div class="gk-modal-status" id="gk-modal-status" role="status"></div>
654            <form id="gk-form-reset" novalidate>
655                ${pole({ etykieta: 'Login lub e-mail', nazwa: 'login', ikona: 'mail', atrybuty: 'required maxlength="100" autocomplete="username"' })}
656                <div class="gk-honeypot" aria-hidden="true"><label>Zostaw puste<input type="text" name="www" tabindex="-1" autocomplete="off"></label></div>
657                ${przyciskWyslij('Wyślij link resetujący', 'mail')}
658            </form>
659            <p class="gk-modal-linki"><a href="#" data-gk="logowanie">← Wróć do logowania</a></p>`);
660        const form = document.getElementById('gk-form-reset');
661        form.addEventListener('submit', async (e) => {
662            e.preventDefault();
663            if (!form.login.value.trim()) { ustawStatus('blad', 'Podaj login lub e-mail.'); return; }
664            ustawLadowanie(form, true);
665            ustawStatus('', 'Wysyłanie…');
666            const { ok, dane } = await postJson(cfg.restUrlResetZadaj, Object.fromEntries(new FormData(form)));
667            ustawLadowanie(form, false);
668            ustawStatus(ok ? 'ok' : 'blad', dane.wiadomosc || dane.message || 'Gotowe.');
669            if (ok) form.querySelectorAll('input,button').forEach((i) => { i.disabled = true; });
670        });
671    }
672
673    document.addEventListener('click', async (e) => {
674        const oko = e.target.closest('[data-gk-oko]');
675        if (oko) {
676            const pole = oko.parentElement.querySelector('input');
677            const pokaz = pole.type === 'password';
678            pole.type = pokaz ? 'text' : 'password';
679            oko.innerHTML = ikona(pokaz ? 'okoZamkniete' : 'oko');
680            oko.setAttribute('aria-label', pokaz ? 'Ukryj hasło' : 'Pokaż hasło');
681            return;
682        }
683        const dodatek = e.target.closest('[data-gk-dodatek]');
684        if (dodatek) {
685            zamknijMenuKonta();
686            document.dispatchEvent(new CustomEvent('gorzow-konta:menu', { detail: { id: dodatek.dataset.gkDodatek } }));
687            return;
688        }
689        const cel = e.target.closest('[data-gk]');
690        if (!cel) return;
691        const akcja = cel.dataset.gk;
692        if (akcja === 'zamknij') { zamknijModal(); return; }
693        if (akcja === 'menu') {
694            const menu = cel.parentElement.querySelector('.gk-konto-menu');
695            const otworz = menu && menu.hidden;
696            zamknijMenuKonta();
697            if (menu && otworz) {
698                menu.hidden = false;
699                cel.setAttribute('aria-expanded', 'true');
700            }
701            return;
702        }
703        zamknijMenuKonta();
704        if (['logowanie', 'rejestracja', 'reset'].includes(akcja)) {
705            e.preventDefault();
706            if (akcja === 'logowanie') renderLogowanie();
707            if (akcja === 'rejestracja') renderRejestracja();
708            if (akcja === 'reset') renderReset();
709        }
710        if (akcja === 'wyloguj') {
711            cel.disabled = true;
712            await postJson(cfg.restUrlWylogowanie, {});
713            window.location.reload();
714        }
715        if (akcja === 'usun') {
716            renderUsuniecie();
717        }
718        if (akcja === 'awatar') {
719            zamknijMenuKonta();
720            renderAwatar();
721        }
722    });
723    // Tło zamyka okno tylko przy pełnym kliknięciu w tło (nie przy
724    // zaznaczaniu tekstu, które kończy się poza oknem) i nie wtedy, gdy
725    // w formularzu są już wpisane dane - wtedy okno tylko "drgnie".
726    let wcisnietoNaTle = false;
727    const tloModala = document.getElementById('gk-modal-tlo');
728    if (tloModala) {
729        tloModala.addEventListener('pointerdown', (e) => { wcisnietoNaTle = e.target === tloModala; });
730        tloModala.addEventListener('click', (e) => {
731            if (e.target !== tloModala || !wcisnietoNaTle) return;
732            const modal = document.getElementById('gk-modal-konto');
733            const wypelnione = modal && Array.from(modal.querySelectorAll('input:not([type="hidden"]):not([tabindex="-1"]):not([type="checkbox"])')).some((i) => i.value && !i.disabled);
734            if (wypelnione) {
735                modal.classList.remove('gk-drgnij');
736                void modal.offsetWidth;
737                modal.classList.add('gk-drgnij');
738                return;
739            }
740            zamknijModal();
741        });
742    }
743    // Zapamiętanie loginu przy przełączaniu zakładek + ostrzeżenie Caps Lock.
744    document.addEventListener('input', (e) => {
745        if (e.target && e.target.name === 'login' && e.target.closest('#gk-modal-konto')) pamiecFormularza.login = e.target.value;
746    });
747    const sprawdzCapsLock = (e) => {
748        if (!e.target || e.target.type !== 'password' && !(e.target.name === 'haslo' || e.target.name === 'haslo2')) return;
749        if (typeof e.getModifierState !== 'function') return;
750        const pole = e.target.closest('.gk-m-pole');
751        const info = pole && pole.querySelector('.gk-m-capslock');
752        if (info) info.hidden = !e.getModifierState('CapsLock');
753    };
754    document.addEventListener('keyup', sprawdzCapsLock);
755    document.addEventListener('keydown', sprawdzCapsLock);
756    document.addEventListener('keydown', (e) => {
757        if (e.key === 'Escape') { zamknijModal(); zamknijMenuKonta(); }
758    });
759    document.addEventListener('click', (e) => {
760        if (!e.target.closest('.gk-konto')) zamknijMenuKonta();
761    });
762
763    // Strona starsza niż 6 h (pamięć podręczna) - najpierw świeży nonce.
764    const start = async () => {
765        if (cfg.czasFormularza && (Date.now() / 1000 - Number(cfg.czasFormularza)) > 6 * 3600) {
766            await odswiezNonce();
767        }
768        await pobierzStanKonta();
769        renderPasek();
770    };
771    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
772    else start();
773})();
774
775/**
776 * Ochrona przed "czarnymi" hoverami z motywu strony (np. Elementor/Astra
777 * ustawiają button:hover / button:focus na czarne tło i biały tekst).
778 * Dla każdego przycisku w module zapamiętujemy jego zwykłe tło i kolor,
779 * a przy najechaniu / kliknięciu (focus) przywracamy je. Nasze własne,
780 * zaprojektowane efekty hover (z !important) działają dalej.
781 */
782(function antyCzarnyHover(korzenie, idStylu) {
783  if (document.getElementById(idStylu)) return;
784  // :not(#gz-x) podnosi ważność reguły ponad typowe reguły motywu.
785  const sel = korzenie.map((k) => `${k} button:not(#gz-x):is(:hover,:focus,:active), ${k} [type="button"]:not(#gz-x):is(:hover,:focus,:active), ${k} [type="submit"]:not(#gz-x):is(:hover,:focus,:active)`).join(',');
786  const st = document.createElement('style');
787  st.id = idStylu;
788  st.textContent = `${sel}{background-color:var(--gz-bg,transparent);color:var(--gz-c,inherit);text-decoration:none}`;
789  (document.head || document.documentElement).appendChild(st);
790
791  const zapamietaj = (b, proba) => {
792    if (!(b instanceof HTMLElement) || b.dataset.gzHv) return;
793    if (b.matches(':hover, :focus')) {
794      // Przycisk jest właśnie wskazany/kliknięty - spróbuj ponownie za chwilę.
795      if ((proba || 0) < 20) setTimeout(() => zapamietaj(b, (proba || 0) + 1), 500);
796      return;
797    }
798    const cs = getComputedStyle(b);
799    b.style.setProperty('--gz-bg', cs.backgroundColor);
800    b.style.setProperty('--gz-c', cs.color);
801    b.dataset.gzHv = '1';
802  };
803  const skanuj = (el) => {
804    if (!el || !el.querySelectorAll) return;
805    if (el.matches && el.matches('button, [type="button"], [type="submit"]')) zapamietaj(el);
806    el.querySelectorAll('button, [type="button"], [type="submit"]').forEach(zapamietaj);
807  };
808  // Zmiana klasy (np. aktywny/nieaktywny) = nowe tło - odświeżamy zapamiętane.
809  const odswiez = (b) => { delete b.dataset.gzHv; b.style.removeProperty('--gz-bg'); b.style.removeProperty('--gz-c'); requestAnimationFrame(() => zapamietaj(b)); };
810  let kolejka = new Set();
811  let zaplanowane = false;
812  const obs = new MutationObserver((lista) => {
813    lista.forEach((m) => {
814      if (m.type === 'attributes') {
815        const t = m.target;
816        if (t.matches && t.matches('button, [type="button"], [type="submit"]') && (m.attributeName === 'class' || m.attributeName === 'aria-pressed' || m.attributeName === 'aria-selected')) odswiez(t);
817      } else m.addedNodes.forEach((n) => kolejka.add(n));
818    });
819    if (!zaplanowane && kolejka.size) {
820      zaplanowane = true;
821      requestAnimationFrame(() => { kolejka.forEach(skanuj); kolejka = new Set(); zaplanowane = false; });
822    }
823  });
824  const start = () => {
825    korzenie.forEach((k) => document.querySelectorAll(k).forEach((r) => {
826      skanuj(r);
827      obs.observe(r, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'aria-pressed', 'aria-selected'] });
828    }));
829    // Moduły dokładające okna do <body> (galeria, okna konta).
830    obs.observe(document.body, { childList: true });
831  };
832  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
833})(['#gk-modal-tlo', '.gk-pasek'], 'gk-antyhover');

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.