PageSourceSearch

https://cottbus.de/wp-content/plugins/pr-event-calendar/assets/js/frontend.js?ver=11.1.0

js cottbus.de collected 2026-10-02 05:26:03 UTC 123,159 bytes, 2,503 lines download raw bytes

1/**
2 * Event Kalender – Frontend JavaScript
3 * Version: 1.0.0
4 */
5/* global precFrontend, FullCalendar */
6(function ($) {
7    'use strict';
8
9    /**
10     * Text aus einem Kalender-Datensatz sicher aufbereiten.
11     *
12     * WARUM DAS NOETIG IST: Die Werte (Titel, Ort, Kategorie) kommen als JSON
13     * vom Server und werden hier zu HTML zusammengesetzt. Wird ein solcher Wert
14     * ungeprueft in einen HTML-String geschrieben und dieser dann per innerHTML
15     * eingesetzt, fuehrt der Browser darin enthaltene Auszeichnung aus - das
16     * waere gespeichertes Cross-Site-Scripting, sichtbar fuer JEDEN Besucher.
17     *
18     * precText() liefert reinen Text: erst werden Entitaeten aufgeloest (damit
19     * "Kunst & Kultur" auch wirklich "Kunst & Kultur" heisst), danach wird
20     * das Ergebnis wieder maskiert. Uebrig bleibt genau der sichtbare Text.
21     */
22    /**
23     * Symbol als SVG (seit v11.1.0) — kommt fertig vom Server (PREC_Symbole, feste Zeichenketten).
24     * Ersetzt die Emojis, die je Betriebssystem anders (Windows: schwarz-weiß) aussahen.
25     */
26    function precSym(name) {
27        return (window.precFrontend && precFrontend.sym && precFrontend.sym[name]) ? precFrontend.sym[name] : '';
28    }
29
30    /**
31     * Ortsteil-Auswahl an die gewählten Orte anpassen (seit v11.1.0).
32     * - Erscheint erst, wenn im Orte-Filter ein Ort gewählt ist — und nur, wenn es dort Ortsteile gibt.
33     * - Ohne Orte-Filter in der Leiste gilt der Ort des Kurzbefehls; ohne jeden Ort alle Ortsteile.
34     * - Mehrere Orte: Ortsteile je Ort gruppiert (optgroup).
35     * Rückgabe: true, wenn sich die Auswahl dadurch geändert hat (gewählter Ortsteil passt nicht mehr).
36     */
37    function precOtSync($swrap) {
38        var $sel = $swrap.find('.prec-district-select').first();
39        if (!$sel.length) return false;
40        var orte = $sel.data('orte') || {}, alle = $sel.data('alle') || {};
41        if (typeof orte === 'string') { try { orte = JSON.parse(orte); } catch (e) { orte = {}; } }
42        if (typeof alle === 'string') { try { alle = JSON.parse(alle); } catch (e) { alle = {}; } }
43        var hatOrtFilter = ($sel.attr('data-ort-filter') === '1') && $swrap.find('.prec-loc-check').length > 0;
44        var gruppen = [];
45        if (hatOrtFilter) {
46            $swrap.find('.prec-loc-check:checked').each(function () {
47                var slugs = String($(this).val() || '').split(','), liste = {};
48                slugs.forEach(function (sl) { var o = orte[$.trim(sl)] || {}; Object.keys(o).forEach(function (k) { liste[k] = (liste[k] || 0) + (+o[k] || 0); }); });
49                if (Object.keys(liste).length) gruppen.push({ name: $(this).data('name') || '', liste: liste });
50            });
51        } else {
52            var fest = String($swrap.data('prec-locked-location') || '').split(',').map($.trim).filter(Boolean), liste = {};
53            if (fest.length) {
54                fest.forEach(function (sl) { var o = orte[sl] || {}; Object.keys(o).forEach(function (k) { liste[k] = (liste[k] || 0) + (+o[k] || 0); }); });
55            } else { liste = alle; }
56            if (Object.keys(liste).length) gruppen.push({ name: '', liste: liste });
57        }
58        var vor = String($sel.data('vorauswahl') || ''), vorher = String($sel.val() || '');
59        var sortiert = function (o) { return Object.keys(o).sort(function (a, b) { return a.localeCompare(b, 'de'); }); };
60        $sel.empty().append($('<option>', { value: '', text: 'Alle Ortsteile' }));
61        var werte = {};
62        gruppen.forEach(function (g) {
63            var $ziel = (gruppen.length > 1) ? $('<optgroup>', { label: g.name }).appendTo($sel) : $sel;
64            sortiert(g.liste).forEach(function (k) { werte[k] = 1; $ziel.append($('<option>', { value: k, text: k + ' (' + g.liste[k] + ')' })); });
65        });
66        // Vorgabe des Kurzbefehls nur behalten, solange kein anderer Ort gewählt ist (sonst leere Trefferliste)
67        if (vor && !werte[vor] && (!hatOrtFilter || !gruppen.length)) { $sel.append($('<option>', { value: vor, text: vor.replace(/\|/g, ' + ') })); werte[vor] = 1; }
68        var neu = werte[vorher] ? vorher : '';
69        $sel.val(neu);
70        var $w = $sel.closest('.prec-district-wrap');
71        var zeigen = gruppen.length > 0 || (!!vor && !!werte[vor]);
72        if (zeigen) { $w.removeAttr('hidden'); } else { $w.attr('hidden', 'hidden'); }
73        return neu !== vorher;
74    }
75    window.precOtSync = precOtSync;
76
77    function precText(wert) {
78        if (wert === null || wert === undefined) return '';
79        var box = document.createElement('textarea');
80        box.innerHTML = String(wert);          // Entitaeten aufloesen
81        var klartext = box.value;
82        box.textContent = klartext;            // wieder maskieren
83        return box.innerHTML;
84    }
85
86    /** Wert fuer ein HTML-Attribut absichern (Anfuehrungszeichen inbegriffen). */
87    function precAttr(wert) {
88        return precText(wert).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
89    }
90
91    'use strict';
92
93    /* ====================================================================
94       Namespace – MUSS vor document.ready definiert sein
95       ==================================================================== */
96    var PREC = window.PREC = window.PREC || {};
97
98    /* ====================================================================
99       Kategorie-Filter
100       ==================================================================== */
101    PREC.filter = {
102
103        activeCat:  '*',
104        activeDays: 0,
105        dateFrom:   '',
106        dateTo:     '',
107        searchQuery: '',
108
109        init: function () {
110            var self = this;
111
112            // Kategorie-Buttons
113            $(document).on('click', '.prec-filter-btn', function (e) {
114                var $btn  = $(this);
115                var $wrap = $btn.closest('.prec-shortcode-wrap');
116                $btn.closest('.prec-filter-cats').find('.prec-filter-btn').removeClass('active');
117                $btn.addClass('active');
118                self.activeCat = $btn.data('filter');
119                // Eine ueber Filter-Buttons gesetzte Kategorie verfaellt, sobald
120                // der Nutzer selbst in der Leiste waehlt - und mit ihr ALLE
121                // uebrigen Button-Sperren (Stadt, Datum, past, Ort, Suche).
122                // Sonst filtert der Button unsichtbar weiter mit, obwohl der
123                // Nutzer laengst selbst uebernommen hat (Mischzustand).
124                $wrap.data('prec-ext-category', '');
125                var fbtnOrig = $wrap.data('prec-fbtn-orig');
126                if (fbtnOrig) {
127                    $wrap.data('prec-locked-date-from', fbtnOrig.von       || '');
128                    $wrap.data('prec-locked-date-to',   fbtnOrig.bis       || '');
129                    $wrap.data('prec-locked-search',    fbtnOrig.search    || '');
130                    $wrap.data('prec-locked-venue',     fbtnOrig.venue     || '');
131                    $wrap.data('prec-locked-organizer', fbtnOrig.organizer || '');
132                    $wrap.data('prec-locked-city',      fbtnOrig.city      || '');
133                    $wrap.data('prec-locked-past',      fbtnOrig.past      || '');
134                    self.dateFrom = fbtnOrig.von || '';
135                    self.dateTo   = fbtnOrig.bis || '';
136                }
137                // Aktiven Filter-Button optisch zuruecksetzen.
138                $('.prec-fbtn-aktiv').removeClass('prec-fbtn-aktiv');
139
140                // AJAX-Reload: Neue Events für gewählte Kategorie laden
141                var $layout = $wrap.find('div[class*="prec-layout-"]').first();
142                if ($layout.length && typeof precFrontend !== 'undefined') {
143                    var cat = self.activeCat === '*' ? '' : self.activeCat;
144                    _precLoadEvents($layout, 1, null, cat);
145                } else {
146                    // Fallback: Client-Side Filter
147                    self.applyAll( $wrap );
148                }
149            });
150
151            // ── Time-Dropdown (Schnellfilter) ──────────────────────
152            $(document).on('click', '#prec-time-dropdown-btn', function (e) {
153                e.stopPropagation();
154                var $btn  = $(this);
155                var $menu = $btn.siblings('#prec-time-dropdown-menu');
156                var isOpen = $menu.hasClass('open');
157                $('.prec-time-dropdown-menu').removeClass('open');
158                $('[id="prec-time-dropdown-btn"]').attr('aria-expanded','false');
159                if (!isOpen) { $menu.addClass('open'); $btn.attr('aria-expanded','true'); }
160            });
161            $(document).on('click', function () {
162                $('.prec-time-dropdown-menu').removeClass('open');
163                $('[id="prec-time-dropdown-btn"]').attr('aria-expanded','false');
164            });
165            $(document).on('click', '.prec-time-opt', function (e) {
166                e.stopPropagation();
167                var $opt  = $(this);
168                var $wrap = $opt.closest('.prec-shortcode-wrap');
169                var days  = parseInt($opt.data('days')) || 0;
170                var label = $opt.text().trim();
171                $opt.closest('.prec-time-dropdown-menu').find('.prec-time-opt').removeClass('active');
172                $opt.addClass('active');
173                $opt.closest('.prec-time-dropdown-wrap').find('#prec-time-label')
174                    .html(precSym('kalender') + ' ').append(document.createTextNode(label));
175                // a11y (seit v7.0.5): aria-label des Buttons mit der aktuellen Auswahl aktualisieren
176                $opt.closest('.prec-time-dropdown-wrap').find('#prec-time-dropdown-btn')
177                    .attr('aria-label', 'Zeitraum-Filter, aktuell: ' + label);
178                $opt.closest('.prec-time-dropdown-menu').removeClass('open');
179                $opt.closest('.prec-time-dropdown-wrap').find('#prec-time-dropdown-btn')
180                    .attr('aria-expanded','false').toggleClass('active', days !== 0);
181                self.activeDays = days;
182                self.dateFrom   = '';
183                self.dateTo     = '';
184                $wrap.find('#prec-date-from, #prec-date-to').val('');
185                $wrap.find('#prec-daterange-display').val('').attr('placeholder','Zeitraum wählen');
186                self.updateResetBtn($wrap);
187                var $layout = $wrap.find('div[class*="prec-layout-"]').first();
188                if ($layout.length && typeof _precLoadEvents === 'function') {
189                    _precLoadEvents($layout, 1);
190                } else { self.applyAll($wrap); }
191            });
192
193            // ── Daterange-Picker (Kalender-Popup) ──
194            (function initDateRangePicker(){
195                var drFrom = '', drTo = '', drMonth = new Date();
196                var drMode = 'range'; // 'range' oder 'single'
197                var months_de = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
198                var days_de = ['Mo','Di','Mi','Do','Fr','Sa','So'];
199
200                function pad(n){ return n < 10 ? '0'+n : ''+n; }
201                function toISO(d){ return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); }
202                function fromISO(s){ var p=s.split('-'); return new Date(+p[0],+p[1]-1,+p[2]); }
203                function fmtDE(s){ if(!s) return ''; var p=s.split('-'); return p[2]+'.'+p[1]+'.'+p[0]; }
204
205                function renderCalendar(){
206                    var $grid = $('#prec-dr-grid');
207                    if (!$grid.length) return;
208                    var y = drMonth.getFullYear(), m = drMonth.getMonth();
209                    $('#prec-dr-month-label').text(months_de[m]+' '+y);
210
211                    // Modus-Buttons aktualisieren
212                    $('#prec-dr-mode-range').toggleClass('active', drMode==='range');
213                    $('#prec-dr-mode-single').toggleClass('active', drMode==='single');
214
215                    var html = '';
216                    days_de.forEach(function(d){ html += '<div style="font-weight:600;color:#6b7280;padding:4px 0;">'+d+'</div>'; });
217
218                    var first = new Date(y,m,1), last = new Date(y,m+1,0);
219                    var startDay = (first.getDay()+6)%7;
220                    var today = toISO(new Date());
221
222                    for(var i=0;i<startDay;i++) html += '<div></div>';
223
224                    for(var d=1;d<=last.getDate();d++){
225                        var iso = y+'-'+pad(m+1)+'-'+pad(d);
226                        var cls = 'prec-dr-day';
227                        if(iso===drFrom || iso===drTo) cls += ' prec-dr-selected';
228                        else if(drFrom && drTo && iso>drFrom && iso<drTo) cls += ' prec-dr-range';
229                        if(iso===today) cls += ' prec-dr-today';
230                        html += '<div class="'+cls+'" data-date="'+iso+'" style="padding:6px 0;border-radius:6px;cursor:pointer;">'+d+'</div>';
231                    }
232                    $grid.html(html);
233                }
234
235                // Ganzes Popup: stopPropagation damit Document-Handler es nicht schließt
236                $(document).on('click', '#prec-daterange-popup', function(e){
237                    e.stopPropagation();
238                });
239
240                // Popup öffnen/schließen
241                $(document).on('click', '#prec-daterange-display', function(e){
242                    e.stopPropagation();
243                    var $popup = $('#prec-daterange-popup');
244                    if($popup.is(':visible')){ $popup.hide(); return; }
245                    if(drFrom) drMonth = fromISO(drFrom);
246                    else drMonth = new Date();
247                    renderCalendar();
248                    $popup.show();
249                });
250                $(document).on('click', function(){
251                    $('#prec-daterange-popup').hide();
252                });
253
254                // Monat navigieren
255                $(document).on('click', '.prec-dr-nav', function(){
256                    drMonth.setMonth(drMonth.getMonth() + parseInt($(this).data('dir')));
257                    renderCalendar();
258                });
259
260                // Modus umschalten (Click)
261                $(document).on('click', '#prec-dr-mode-single', function(){
262                    drMode = 'single'; drTo = '';
263                    updateDisplay(); renderCalendar();
264                });
265                $(document).on('click', '#prec-dr-mode-range', function(){
266                    drMode = 'range';
267                    renderCalendar();
268                });
269                // a11y (seit v7.0.6): Mode-Switcher per Tastatur (Space/Enter) bedienen,
270                // damit Screenreader-Nutzer das role="button" auch wirklich nutzen können.
271                $(document).on('keydown', '#prec-dr-mode-single, #prec-dr-mode-range', function(e){
272                    if (e.key === ' ' || e.key === 'Enter' || e.keyCode === 32 || e.keyCode === 13) {
273                        e.preventDefault();
274                        $(this).trigger('click');
275                    }
276                });
277
278                // Tag klicken
279                $(document).on('click', '.prec-dr-day', function(){
280                    var iso = $(this).data('date');
281                    if(drMode === 'single'){
282                        drFrom = iso; drTo = '';
283                        updateDisplay();
284                        renderCalendar();
285                    } else {
286                        // Range-Modus
287                        if(!drFrom || (drFrom && drTo)){
288                            drFrom = iso; drTo = '';
289                        } else {
290                            if(iso < drFrom){ drTo = drFrom; drFrom = iso; }
291                            else { drTo = iso; }
292                        }
293                        updateDisplay();
294                        renderCalendar();
295                    }
296                });
297
298                function updateDisplay(){
299                    var $disp = $('#prec-daterange-display');
300                    if(drMode === 'single' && drFrom){
301                        $disp.val(fmtDE(drFrom));
302                    } else if(drFrom && drTo){
303                        $disp.val(fmtDE(drFrom)+' – '+fmtDE(drTo));
304                    } else if(drFrom){
305                        $disp.val(fmtDE(drFrom)+' – …');
306                    } else {
307                        $disp.val('').attr('placeholder','Zeitraum wählen');
308                    }
309                }
310
311                // Anwenden
312                $(document).on('click', '#prec-dr-apply', function(){
313                    var $wrap = $(this).closest('.prec-shortcode-wrap');
314                    $wrap.find('#prec-date-from').val(drFrom);
315                    $wrap.find('#prec-date-to').val(drTo || drFrom);
316                    self.dateFrom = drFrom;
317                    self.dateTo   = drTo || drFrom;
318                    self.activeDays = 0;
319                    $wrap.find('.prec-time-opt').removeClass('active').first().addClass('active');
320                    $wrap.find('#prec-time-label').html(precSym('kalender') + ' Alle Termine');
321                    // a11y (seit v7.0.5): aria-label zurücksetzen
322                    $wrap.find('#prec-time-dropdown-btn')
323                        .attr('aria-label', 'Zeitraum-Filter, aktuell: Alle Termine')
324                        .removeClass('active');
325                    self.updateResetBtn($wrap);
326                    $('#prec-daterange-popup').hide();
327                    var $layout = $wrap.find('div[class*="prec-layout-"]').first();
328                    if($layout.length && typeof _precLoadEvents === 'function'){
329                        _precLoadEvents($layout, 1);
330                    }
331                });
332
333                // Zurücksetzen
334                $(document).on('click', '#prec-dr-clear', function(){
335                    drFrom = ''; drTo = '';
336                    updateDisplay();
337                    renderCalendar();
338                    var $wrap = $(this).closest('.prec-shortcode-wrap');
339                    $wrap.find('#prec-date-from, #prec-date-to').val('');
340                    self.dateFrom = ''; self.dateTo = '';
341                });
342            })();
343
344            // CSS für Daterange-Picker
345            if(!$('#prec-dr-styles').length){
346                $('head').append('<style id="prec-dr-styles">'
347                    +'.prec-dr-day:hover{background:#e0f2fe !important;}'
348                    +'.prec-dr-selected{background:#2271b1 !important;color:#fff !important;font-weight:600;}'
349                    +'.prec-dr-range{background:#dbeafe !important;color:#1d4ed8 !important;}'
350                    +'.prec-dr-today{font-weight:700;box-shadow:inset 0 0 0 2px #2271b1;}'
351                    +'.prec-dr-mode{display:inline-block;padding:4px 10px;font-size:11px;border:1px solid #dcdcde;border-radius:4px;cursor:pointer;background:#f9fafb;color:#6b7280;}'
352                    +'.prec-dr-mode.active{background:#2271b1;color:#fff;border-color:#2271b1;}'
353                    +'.prec-dr-mode:hover:not(.active){background:#f3f4f6;}'
354                    +'</style>');
355            }
356
357            // ALLE Filter zurücksetzen
358            $(document).on('click', '#prec-reset-all', function () {
359                var $wrap       = $(this).closest('.prec-shortcode-wrap');
360                self.activeCat  = '*';
361                self.activeDays = 0;
362                self.dateFrom   = '';
363                self.dateTo     = '';
364                self.searchQuery = '';
365                $wrap.find('#prec-date-from, #prec-date-to').val('');
366                $wrap.find('#prec-daterange-display').val('').attr('placeholder','Zeitraum wählen');
367                $wrap.find('#prec-event-search').val('');
368                $wrap.find('.prec-time-btn').removeClass('active').first().addClass('active');
369                // a11y (seit v7.0.6): Time-Dropdown-Label und sein aria-label zurücksetzen,
370                // damit Screenreader nach Reset nicht mehr die alte Auswahl ansagen.
371                $wrap.find('.prec-time-opt').removeClass('active').first().addClass('active');
372                $wrap.find('#prec-time-label').html(precSym('kalender') + ' Alle Termine');
373                $wrap.find('#prec-time-dropdown-btn')
374                    .attr('aria-label', 'Zeitraum-Filter, aktuell: Alle Termine')
375                    .removeClass('active');
376
377                // Seit v6.9.7: Reset bringt den Filter zurück zum INITIAL-Zustand des Shortcodes
378                // — also zur Shortcode-Vorauswahl (location="cottbus", category="kultur"),
379                // NICHT zu „Alle Orte / Alle Kategorien". Das ist intuitiver: Reset = zurück
380                // zum Ausgangszustand, wie die Seite ursprünglich geladen wurde.
381
382                // Kategorie zurücksetzen
383                var preselectedCat = $wrap.data('prec-locked-category') || '';
384                $wrap.find('.prec-filter-btn').removeClass('active');
385                if (preselectedCat) {
386                    var $preBtn = $wrap.find('.prec-filter-btn[data-filter="' + preselectedCat + '"]').first();
387                    if ($preBtn.length) {
388                        $preBtn.addClass('active');
389                    } else {
390                        $wrap.find('.prec-filter-btn[data-filter="*"]').first().addClass('active');
391                    }
392                } else {
393                    $wrap.find('.prec-filter-btn[data-filter="*"]').first().addClass('active');
394                }
395
396                // Standort zurücksetzen
397                var preselectedLoc = $wrap.data('prec-locked-location') || '';
398                $wrap.find('.prec-loc-check').prop('checked', false);
399                if (preselectedLoc) {
400                    var preselectedLocs = preselectedLoc.toString().split(',').map(function(s){return s.trim();}
400).filter(Boolean);
401                    var checkedCount = 0;
402                    preselectedLocs.forEach(function(slug){
403                        // seit v11.1.0: ein Eintrag kann mehrere gleichnamige Orte bündeln (data-slugs)
404                        var $cb = $wrap.find('.prec-loc-check').filter(function(){
405                            return (' ' + ($(this).attr('data-slugs') || $(this).val()) + ' ').indexOf(' ' + slug + ' ') !== -1;
406                        }).first();
407                        if ($cb.length && !$cb.prop('checked')) { $cb.prop('checked', true); checkedCount++; }
408                    });
409                    $wrap.find('.prec-loc-check-all').prop('checked', checkedCount === 0);
410                    // Button-Label aktualisieren
411                    var $btn = $wrap.find('#prec-location-btn .prec-loc-label');
412                    var locText;
413                    if (checkedCount === 0)      { locText = 'Alle Orte'; }
414                    else if (checkedCount === 1) { locText = $wrap.find('.prec-loc-check:checked').first().data('name') || preselectedLocs[0]; }
415                    else                          { locText = checkedCount + ' Orte'; }
416                    $btn.text(locText);
417                    // a11y (seit v7.0.5): aria-label des Buttons synchron
418                    $wrap.find('#prec-location-btn').attr('aria-label', 'Orte-Filter, aktuell: ' + locText);
419                } else {
420                    $wrap.find('.prec-loc-check-all').prop('checked', true);
421                    $wrap.find('#prec-location-btn .prec-loc-label').text('Alle Orte');
422                    $wrap.find('#prec-location-btn').attr('aria-label', 'Orte-Filter, aktuell: Alle Orte');
423                }
424                $wrap.find('#prec-location-btn').css({'border-color':'#dcdcde','background':'#fff'});
425
426                // Seit v10.9.0: Ortsteil + Non-Profit auf die Vorauswahl des Kurzbefehls zurück
427                // Seit v11.1.0: Liste zuerst an die (zurückgesetzten) Orte anpassen
428                $wrap.find('.prec-district-select').each(function () { $(this).val(''); });
429                precOtSync($wrap);
430                $wrap.find('.prec-district-select').each(function () { $(this).val(($(this).data('vorauswahl') || '') + ''); });
431                $wrap.find('.prec-np-check').each(function () { $(this).prop('checked', ($(this).data('vorauswahl') + '') === '1'); });
432
433                $wrap.find('#prec-search-bar').slideUp(200);
434                self.updateResetBtn( $wrap );
435                var $layout = $wrap.find('div[class*="prec-layout-"]').first();
436                if ($layout.length && typeof _precLoadEvents === 'function') {
437                    _precLoadEvents($layout, 1);
438                } else {
439                    self.applyAll( $wrap );
440                }
441            });
442
443            // Ort-Filter
444            // ── ORT-FILTER (Mehrfach-Checkboxen) ────────────────────────
445            // Dropdown öffnen/schließen
446            $(document).on('click', '#prec-location-btn', function (e) {
447                e.stopPropagation();
448                var $dd = $(this).siblings('#prec-location-dropdown');
449                var open = $dd.is(':visible');
450                $('.prec-location-dropdown').hide();
451                $('.prec-location-btn').attr('aria-expanded','false');
452                if (!open) {
453                    $dd.show();
454                    $(this).attr('aria-expanded','true');
455                    // Smart-Positioning: rechts ausrichten wenn kein Platz links
456                    var ddRight = $dd.offset().left + $dd.outerWidth();
457                    if (ddRight > $(window).width() - 10) {
458                        $dd.css({ left: 'auto', right: '0' });
459                    } else {
460                        $dd.css({ left: '0', right: 'auto' });
461                    }
462                    // Such-Feld fokussieren (seit v6.9.5)
463                    var $search = $dd.find('.prec-loc-search');
464                    if ($search.length) {
465                        setTimeout(function(){ $search.trigger('focus'); }, 50);
466                    }
467                }
468            });
469
470            // Such-Filter im Standort-Dropdown (seit v6.9.5)
471            // Filtert die Liste live nach dem eingegebenen Text — case-insensitive,
472            // matcht den Anfang oder beliebige Stelle im Ortsnamen.
473            // Seit v6.9.9: Bei aktiver Suche werden auch initial versteckte Orte
474            // (prec-loc-hidden-default) sichtbar — der User soll alle Orte durchsuchen können.
475            $(document).on('input', '.prec-loc-search', function(){
476                var q = ($(this).val() || '').toLowerCase().trim();
477                var $dd = $(this).closest('.prec-location-dropdown');
478                var $items = $dd.find('.prec-loc-item').not('.prec-loc-all');
479                var visibleCount = 0;
480                $items.each(function(){
481                    var name = ($(this).data('loc-name') || '').toString();
482                    var match = q === '' || name.indexOf(q) !== -1;
483                    // Bei Suche: alle Orte berücksichtigen (auch initial versteckte)
484                    if (q !== '') {
485                        $(this).css('display', match ? 'flex' : 'none');
486                    } else {
487                        // Suche leer: initial versteckte Orte wieder verstecken,
488                        // außer der Toggle-Button wurde geklickt (Klasse entfernt)
489                        var isHiddenByDefault = $(this).hasClass('prec-loc-hidden-default');
490                        $(this).css('display', isHiddenByDefault ? 'none' : 'flex');
491                    }
492                    if (match && $(this).css('display') !== 'none') visibleCount++;
493                });
494                // Toggle-Button bei aktiver Suche ausblenden
495                $dd.find('.prec-loc-show-all').toggle(q === '');
496                $dd.find('.prec-loc-noresults').toggle(visibleCount === 0 && q !== '');
497            });
498
499            // Toggle-Button „Alle weiteren Orte anzeigen" (seit v6.9.9)
500            $(document).on('click', '.prec-loc-show-all', function(){
501                var $dd = $(this).closest('.prec-location-dropdown');
502                $dd.find('.prec-loc-hidden-default').css('display', 'flex').removeClass('prec-loc-hidden-default');
503                $(this).hide();
504            });
505
506            // „Alle Orte" Checkbox-Click zeigt auch alle versteckten Orte
507            $(document).on('change', '.prec-loc-check-all', function(){
508                if ($(this).is(':checked')) {
509                    var $dd = $(this).closest('.prec-location-dropdown');
510                    $dd.find('.prec-loc-hidden-default').css('display', 'flex').removeClass('prec-loc-hidden-default');
511                    $dd.find('.prec-loc-show-all').hide();
512                }
513            });
514
515            // Esc-Taste schließt Dropdown
516            $(document).on('keydown', '.prec-loc-search', function(e){
517                if (e.key === 'Escape') {
518                    $(this).val('').trigger('input');
519                    $(this).closest('.prec-location-dropdown').hide();
520                    $(this).closest('.prec-location-filter-wrap').find('.prec-location-btn').attr('aria-expanded','false');
521                }
522            });
523            $(document).on('click', function (e) {
524                if (!$(e.target).closest('.prec-location-filter-wrap').length) {
525                    $('.prec-location-dropdown').hide();
526                    $('.prec-location-btn').attr('aria-expanded','false');
527                }
528            });
529            // "Alle Orte" Checkbox
530            $(document).on('change', '.prec-loc-check-all', function () {
531                var $fwrap = $(this).closest('.prec-location-filter-wrap');
532                if ($(this).is(':checked')) $fwrap.find('.prec-loc-check').prop('checked', false);
533                precUpdateLocationFilter($fwrap.closest('.prec-shortcode-wrap'));
534            });
535            // Einzel-Checkbox
536            $(document).on('change', '.prec-loc-check', function () {
537                var $fwrap = $(this).closest('.prec-location-filter-wrap');
538                var any = $fwrap.find('.prec-loc-check:checked').length > 0;
539                $fwrap.find('.prec-loc-check-all').prop('checked', !any);
540                precUpdateLocationFilter($fwrap.closest('.prec-shortcode-wrap'));
541            });
542            // Ortsteil-Auswahl und „Nur Non-Profit" (seit v10.9.0) — kombinierbar mit allen übrigen Filtern
543            $(document).on('change', '.prec-district-select, .prec-np-check', function () {
544                var $swrap = $(this).closest('.prec-shortcode-wrap');
545                if (!$swrap.length) return;
546                self.updateResetBtn($swrap);
547                var $layout = $swrap.find('div[class*="prec-layout-"]').first();
548                if ($layout.length && typeof _precLoadEvents === 'function') _precLoadEvents($layout, 1);
549            });
550            function precUpdateLocationFilter($swrap) {
551                var $btn    = $swrap.find('#prec-location-btn');
552                var checked = $swrap.find('.prec-loc-check:checked');
553                var locText;
554                if (checked.length === 0) {
555                    locText = 'Alle Orte';
556                    $btn.find('.prec-loc-label').text(locText);
557                    $btn.attr('aria-expanded','false');
558                } else if (checked.length === 1) {
559                    locText = checked.first().data('name');
560                    $btn.find('.prec-loc-label').text(locText);
561                    $btn.attr('aria-expanded','true');
562                } else {
563                    locText = checked.length + ' Orte';
564                    $btn.find('.prec-loc-label').text(locText);
565                    $btn.attr('aria-expanded','true');
566                }
567                // a11y (seit v7.0.5): aria-label des Buttons mit aktueller Auswahl
568                $btn.attr('aria-label', 'Orte-Filter, aktuell: ' + locText);
569                precOtSync($swrap); // seit v11.1.0: Ortsteile der gewählten Orte
570                self.updateResetBtn($swrap);
571                var $layout = $swrap.find('div[class*="prec-layout-"]').first();
572                if ($layout.length && typeof _precLoadEvents === 'function') _precLoadEvents($layout, 1);
573            }
574
575            // Layout-Switcher
576            // Masonry auf Mobile (≤480px) aus dem Layout-Switcher entfernen
577            function precAdjustLayoutSwitcher() {
578                var vw = window.innerWidth || document.documentElement.clientWidth;
579                var $sel = $('#prec-layout-switcher');
580                if (!$sel.length) return;
581                if (vw <= 480) {
582                    // Masonry-Option ausblenden (disabled + class)
583                    $sel.find('option[value="masonry"]').prop('disabled', true).hide();
584                    // Falls gerade Masonry aktiv: auf Grid wechseln
585                    if ($sel.val() === 'masonry') {
586                        $sel.val('grid').trigger('change');
587                    }
588                } else {
589                    $sel.find('option[value="masonry"]').prop('disabled', false).show();
590                }
591            }
592            precAdjustLayoutSwitcher();
593            $(window).on('resize', function() { precAdjustLayoutSwitcher(); });
594
595            $(document).on('change', '#prec-layout-switcher', function () {
596                var newLayout = $(this).val();
597                var $wrap     = $(this).closest('.prec-shortcode-wrap');
598                // Layout-Wrapper: nur DIV-Elemente mit prec-layout- Klasse (nicht das Select-Dropdown)
599                var $old      = $wrap.find('div[class*="prec-layout-"]').first();
600                if (!$old.length) return;
601
602                // Alte Layout-Klasse entfernen, neue setzen
603                var oldClass = ($old.attr('class').match(/prec-layout-[\w-]+/) || [])[0];
604                if (oldClass) $old.removeClass(oldClass);
605                $old.addClass('prec-layout-' + newLayout);
606
607                // Container-Klasse austauschen
608                var containerMap = {
609                    'grid': 'prec-events-grid', 'image-cards': 'prec-ic2-grid',
610                    'color-cards': 'prec-cc-grid', 'image-grid': 'prec-ig-grid',
611                    'color-list': 'prec-cl-list', 'list': 'prec-events-list',
612                    'horizontal': 'prec-horizontal-list', 'masonry': 'prec-masonry-grid',
613                    'timeline': 'prec-timeline'
614                };
615                var $list = $old.find('.prec-events-grid, .prec-ic2-grid, .prec-cc-grid, .prec-ig-grid, .prec-cl-list, .prec-events-list, .prec-horizontal-list, .prec-masonry-grid, .prec-timeline').first();
616                if ($list.length) {
617                    // Alle Container-Klassen entfernen
618                    for (var k in containerMap) $list.removeClass(containerMap[k]);
619                    $list.addClass(containerMap[newLayout] || 'prec-events-grid');
620                    $list.removeAttr('style'); // Inline-Grid-Styles entfernen
621                }
622
623                // AJAX-Reload mit neuem Layout
624                _precLoadEvents($old, 1);
625
626                // Auswahl merken
627                try { localStorage.setItem('prec_user_layout', newLayout); } catch(e) {}
628            });
629
630            // Suche: Toggle-Button (original)
631            $(document).on('click', '#prec-search-toggle', function () {
632                var $wrap = $(this).closest('.prec-shortcode-wrap') || $(this).closest('.prec-filter-bar-wrap').parent();
633                var $bar  = $wrap.find('#prec-search-bar');
634                if ( !$bar.length ) $bar = $(this).closest('.prec-filter-bar-wrap').find('#prec-search-bar');
635                if ( $bar.is(':visible') ) {
636                    $bar.slideUp(200);
637                } else {
638                    $bar.slideDown(300, function(){ $bar.find('#prec-event-search').focus(); });
639                }
640            });
641
642            // Suche: Schließen
643            $(document).on('click', '#prec-search-close', function () {
644                var $wrap = $(this).closest('.prec-shortcode-wrap') || $(this).closest('.prec-filter-bar-wrap').parent();
645                self.searchQuery = '';
646                $(this).closest('.prec-search-bar').find('#prec-event-search').val('');
647                $(this).closest('.prec-search-bar').slideUp(200);
648                self.updateResetBtn( $wrap );
649                var $layout = $wrap.find('div[class*="prec-layout-"]').first();
650                if ($layout.length && typeof _precLoadEvents === 'function') {
651                    _precLoadEvents($layout, 1);
652                } else {
653                    self.applyAll( $wrap );
654                }
655            });
656
657            // Suche: Live-Eingabe mit Debounce
658            var searchTimer;
659            $(document).on('input', '#prec-event-search', function () {
660                var $wrap = $(this).closest('.prec-shortcode-wrap') || $(this).closest('.prec-filter-bar-wrap').parent();
661                self.searchQuery = $(this).val().toLowerCase().trim();
662                self.updateResetBtn( $wrap );
663                clearTimeout(searchTimer);
664                searchTimer = setTimeout(function() {
665                    var $layout = $wrap.find('div[class*="prec-layout-"]').first();
666                    if ($layout.length && typeof _precLoadEvents === 'function') {
667                        _precLoadEvents($layout, 1);
668                    } else {
669                        self.applyAll( $wrap );
670                    }
671                }, 400);
672            });
673
674            // Kategorie-Overflow: "+X" Button wenn Buttons nicht in eine Zeile passen
675            var self = this;
676            // Erst nach kurzer Verzögerung prüfen (Layout muss gerendert sein)
677            setTimeout(function() { self.checkCatOverflow(); }, 150);
678            $(window).on('resize', function() { self.checkCatOverflow(); });
679            $(document).on('click', '#prec-cat-more', function() {
680                var $inner = $('#prec-cat-inner');
681                var isExpanded = $inner.hasClass('prec-cat-expanded');
682                $inner.toggleClass('prec-cat-expanded');
683                if (isExpanded) {
684                    // Zuklappen
685                    $(this).html('+' + $(this).data('hiddenCount')).removeClass('prec-cat-less');
686                } else {
687                    // Aufklappen
688                    $(this).html('−').addClass('prec-cat-less');
689                }
690            });
691        },
692
693        checkCatOverflow: function() {
694            var $inner = $('#prec-cat-inner');
695            var $more = $('#prec-cat-more');
696            if (!$inner.length) return;
697
698            // Reset
699            $inner.removeClass('prec-cat-expanded');
700            $more.removeClass('prec-cat-less');
701            $inner.find('.prec-filter-btn').show();
702            $more.hide();
703
704            var maxH = 42; // Eine Zeile (Button-Höhe + gap)
705            if ($inner[0].scrollHeight <= maxH) return;
706
707            // Buttons zählen die nicht sichtbar sind
708            var hidden = 0;
709            var btns = $inner.find('.prec-filter-btn');
710            btns.each(function(i) {
711                if (i === 0) return; // "Alle" immer zeigen
712                if (this.offsetTop > btns.first()[0].offsetTop) {
713                    hidden++;
714                }
715            });
716
717            if (hidden > 0) {
718                $more.data('hiddenCount', hidden).html('+' + hidden).show();
719            }
720        },
721
722        updateResetBtn: function( $wrap ) {
723            var active = this.activeDays > 0 || !!this.dateFrom || !!this.dateTo || this.activeCat !== '*' || !!this.searchQuery;
724            // Seit v10.9.0: abweichender Ortsteil / Non-Profit zählt als aktiver Filter
725            if (!active && $wrap && $wrap.length) {
726                $wrap.find('.prec-district-select').each(function () { if (($(this).val() || '') !== (($(this).data('vorauswahl') || '') + '')) active = true; });
727                $wrap.find('.prec-np-check').each(function () { if ($(this).is(':checked') !== (($(this).data('vorauswahl') + '') === '1')) active = true; });
728            }
729            $wrap.find('#prec-reset-all')[ active ? 'show' : 'hide' ]();
730        },
731
732        applyAll: function ( $wrap ) {
733            if ( !$wrap || !$wrap.length ) {
734                console.error('[EK] applyAll: $wrap leer oder nicht gefunden!');
735                return;
736            }
737            var self  = this;
738            // Server-seitiges Datum verwenden (WordPress-Zeitzone, kein UTC-Versatz)
739            var todayStr = (precFrontend && precFrontend.today) ? precFrontend.today : (function(){
740                var d = new Date();
741                return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
742            })();
743            var todayParts = todayStr.split('-');
744            var today = new Date(+todayParts[0], +todayParts[1]-1, +todayParts[2]);
745            today.setHours(0, 0, 0, 0);
746
747            var rangeFrom = null, rangeTo = null;
748
749            if ( self.dateFrom || self.dateTo ) {
750                if ( self.dateFrom ) rangeFrom = new Date( self.dateFrom + 'T00:00:00' );
751                if ( self.dateTo )   rangeTo   = new Date( self.dateTo   + 'T23:59:59' );
752            } else if ( self.activeDays === 1 ) {
753                rangeFrom = new Date( today );
754                rangeTo   = new Date( today );
755                rangeTo.setHours(23, 59, 59, 0);
756            } else if ( self.activeDays > 1 ) {
757                rangeFrom = new Date( today );
758                rangeTo   = new Date( today );
759                rangeTo.setDate( rangeTo.getDate() + self.activeDays - 1 );
760                rangeTo.setHours(23, 59, 59, 0);
761            }
762
763            var anyVisible = false;
764            $wrap.find('.prec-event-card').each(function () {
765                var $card      = $(this);
766                var categories = ' ' + ($card.data('categories') || $card.data('category') || '') + ' ';
767                var cardDate   = $card.data('date') || '';
768                var cardTime   = $card.data('time') || '00:00';
769
770                var catMatch  = self.activeCat === '*' || categories.indexOf(' ' + self.activeCat + ' ') !== -1;
771                var dateMatch = true;
772                if ( cardDate && (rangeFrom || rangeTo) ) {
773                    var dt = new Date( cardDate + 'T' + cardTime );
774                    if ( rangeFrom && dt < rangeFrom ) dateMatch = false;
775                    if ( rangeTo   && dt > rangeTo   ) dateMatch = false;
776                } else if ( (rangeFrom || rangeTo) && !cardDate ) {
777                    dateMatch = false;
778                }
779
780                // Textsuche
781                var textMatch = true;
782                if ( self.searchQuery ) {
783                    var cardText = ($card.text() || '').toLowerCase();
784                    textMatch = cardText.indexOf( self.searchQuery ) !== -1;
785                }
786
787                var show = catMatch && dateMatch && textMatch;
788                $card[ show ? 'show' : 'hide' ]();
789                if (show) anyVisible = true;
790            });
791
792            this.updateResetBtn( $wrap );
793            $wrap.find('.prec-no-events-filtered').remove();
794            if ( !anyVisible ) {
795                var msg = self.searchQuery
796                    ? 'Keine Veranstaltungen für „' + precText(self.searchQuery) + '" gefunden.'
797                    : (self.dateFrom || self.dateTo || self.activeDays > 0)
798                    ? 'Keine Veranstaltungen im gewählten Zeitraum.'
799                    : 'Keine Veranstaltungen in dieser Kategorie.';
800                $wrap.find('.prec-events-wrap').append(
801                    '<p class="prec-no-events prec-no-events-filtered">' + msg + '</p>'
802                );
803            }
804        }
805    };
806
807    /* ====================================================================
808       Document ready — alle Module starten
809       ==================================================================== */
810    $(function () {
811        // seit v11.1.0: Ortsteil-Auswahl an die vorausgewählten Orte anpassen (ohne Neuladen)
812        $('.prec-shortcode-wrap').each(function () { precOtSync($(this)); });
813        if (PREC.filter)       PREC.filter.init();
814        if (PREC.catOverflow)  PREC.catOverflow.init();
815        if (PREC.registration) PREC.registration.init();
816        if (PREC.sharing)      PREC.sharing.init();
817        if (PREC.slider)       PREC.slider.init();
818        if (PREC.sidebar)      PREC.sidebar.init();
819        if (PREC.countdown)    PREC.countdown.init();
820        if (PREC.calendar)     PREC.calendar.init();
821        if (PREC.map)          PREC.map.init();
822        if (PREC.gallery)      PREC.gallery.init();
823    });
824
825    /* ====================================================================
826       Kategorie-Overflow: wird von PREC.filter.checkCatOverflow gesteuert
827       ==================================================================== */
828    PREC.catOverflow = {
829        init: function () { /* Deaktiviert — PREC.filter.init() übernimmt */ }
830    };
831
832    PREC.sidebar = {
833        init: function () {
834            // Sidebar-Boxen: Immer offen, kein Toggle.
835            // Alle Inhalte fließen natürlich nach unten.
836        }
837    };
838
839    PREC.countdown = {
840        init: function () {
841            // Legacy countdown (shortcode)
842            this._initSimple();
843            // Flip clock countdown (sidebar box)
844            this._initFlip();
845        },
846
847        _initSimple: function () {
848            var els = document.querySelectorAll('.prec-countdown[data-target]');
849            els.forEach(function (el) {
850                var target = new Date(el.getAttribute('data-target').replace(/-/g, '/')).getTime();
851                if (isNaN(target)) return;
852                function update() {
853                    var diff = target - Date.now();
854                    if (diff <= 0) { el.classList.add('prec-countdown-ended'); clearInterval(t); return; }
855                    var d = Math.floor(diff / 86400000);
856                    var h = Math.floor((diff % 86400000) / 3600000);
857                    var m = Math.floor((diff % 3600000) / 60000);
858                    var s = Math.floor((diff % 60000) / 1000);
859                    var q = el.querySelector.bind(el);
860                    if (q('.prec-cd-days'))    q('.prec-cd-days').textContent = d;
861                    if (q('.prec-cd-hours'))   q('.prec-cd-hours').textContent  = h < 10 ? '0' + h : h;
862                    if (q('.prec-cd-minutes')) q('.prec-cd-minutes').textContent = m < 10 ? '0' + m : m;
863                    if (q('.prec-cd-seconds')) q('.prec-cd-seconds').textContent = s < 10 ? '0' + s : s;
864                }
865                update();
866                var t = setInterval(update, 1000);
867            });
868        },
869
870        _initFlip: function () {
871            var clocks = document.querySelectorAll('.prec-flip-clock[data-target]');
872            clocks.forEach(function (clock) {
873                var target = new Date(clock.getAttribute('data-target').replace(/-/g, '/')).getTime();
874                if (isNaN(target)) return;
875
876                var cards = {};
877                clock.querySelectorAll('.prec-flip-card').forEach(function (c) {
878                    cards[c.getAttribute('data-unit')] = {
879                        el: c,
880                        front: c.querySelector('.prec-flip-front span'),
881                        back: c.querySelector('.prec-flip-back span'),
882                        current: '--',
883                        busy: false
884                    };
885                });
886
887                function pad(n) { return n < 10 ? '0' + n : '' + n; }
888
889                function flipTo(card, newVal) {
890                    if (card.current === newVal || card.busy) return;
891                    card.busy = true;
892
893                    // Neue Zahl auf die Rückseite schreiben
894                    card.back.textContent = newVal;
895
896                    // Flip-Animation starten
897                    card.el.classList.add('prec-flipping');
898
899                    // Nach der CSS-Transition (800ms): aufräumen
900                    setTimeout(function () {
901                        card.el.classList.remove('prec-flipping');
902                        // Vorderseite übernimmt die neue Zahl
903                        card.front.textContent = newVal;
904                        card.current = newVal;
905                        card.busy = false;
906                    }, 820);
907                }
908
909                function update() {
910                    var diff = target - Date.now();
911                    if (diff <= 0) {
912                        Object.keys(cards).forEach(function (k) { flipTo(cards[k], '0'); });
913                        clearInterval(timer);
914                        return;
915                    }
916                    var d = Math.floor(diff / 86400000);
917                    var h = Math.floor((diff % 86400000) / 3600000);
918                    var m = Math.floor((diff % 3600000) / 60000);
919                    var s = Math.floor((diff % 60000) / 1000);
920
921                    if (cards.days)    flipTo(cards.days,    '' + d);
922                    if (cards.hours)   flipTo(cards.hours,   pad(h));
923                    if (cards.minutes) flipTo(cards.minutes, pad(m));
924                    if (cards.seconds) flipTo(cards.seconds, pad(s));
925                }
926
927                // Initialwerte ohne Animation setzen
928                var diff0 = target - Date.now();
929                if (diff0 > 0) {
930                    var d0 = Math.floor(diff0 / 86400000);
931                    var h0 = Math.floor((diff0 % 86400000) / 3600000);
932                    var m0 = Math.floor((diff0 % 3600000) / 60000);
933                    var s0 = Math.floor((diff0 % 60000) / 1000);
934                    if (cards.days)    { cards.days.front.textContent = '' + d0; cards.days.current = '' + d0; }
935                    if (cards.hours)   { cards.hours.front.textContent = pad(h0); cards.hours.current = pad(h0); }
936                    if (cards.minutes) { cards.minutes.front.textContent = pad(m0); cards.minutes.current = pad(m0); }
937                    if (cards.seconds) { cards.seconds.front.textContent = pad(s0); cards.seconds.current = pad(s0); }
938                }
939
940                var timer = setInterval(update, 1000);
941            });
942        }
943    };
944
945    PREC.registration = {
946        init: function () {
947            $(document).on('submit', '.prec-reg-form', function (e) {
948                e.preventDefault();
949                var $form = $(this);
950                var $btn  = $form.find('button[type="submit"]');
951                var $msg  = $form.find('.prec-form-message');
952
953                $btn.prop('disabled', true).text('Wird gesendet …');
954                $msg.html('');
955
956                $.post(precFrontend.ajaxUrl, $form.serialize() + '&action=prec_register&nonce=' + precFrontend.nonce, function (res) {
957                    if (res.success) {
958                        $msg.html('<div class="prec-notice prec-notice-success">✅ ' + res.data.message + '</div>');
959                        $form[0].reset();
960                    } else {
961                        $msg.html('<div class="prec-notice prec-notice-error">❌ ' + res.data.message + '</div>');
962                    }
963                }).fail(function () {
964                    $msg.html('<div class="prec-notice prec-notice-error">❌ Ein Fehler ist aufgetreten.</div>');
965                }).always(function () {
966                    $btn.prop('disabled', false).text('Jetzt anmelden');
967                });
968            });
969        }
970    };
971
972    /* ====================================================================
973       Social Sharing (copy link)
974       ==================================================================== */
975    PREC.sharing = {
976        init: function () {
977            $(document).on('click', '.prec-share-copy', function (e) {
978                e.preventDefault();
979                var url = $(this).attr('href') || window.location.href;
980                if (navigator.clipboard) {
981                    navigator.clipboard.writeText(url).then(function () {
982                        PREC.sharing.toast('Link kopiert! 📋');
983                    });
984                } else {
985                    var ta = document.createElement('textarea');
986                    ta.value = url;
987                    document.body.appendChild(ta);
988                    ta.select();
989                    document.execCommand('copy');
990                    document.body.removeChild(ta);
991                    PREC.sharing.toast('Link kopiert! 📋');
992                }
993            });
994        },
995        toast: function (msg) {
996            var $t = $('<div class="prec-toast">' + msg + '</div>').css({
997                position: 'fixed', bottom: '24px', right: '24px', background: '#333', color: '#fff',
998                padding: '10px 20px', borderRadius: '8px', zIndex: 9999, fontSize: '14px', opacity: 0
999            }).appendTo('body').animate({ opacity: 1 }, 200);
1000            setTimeout(function () { $t.animate({ opacity: 0 }, 300, function () { $t.remove(); }); }, 2500);
1001        }
1002    };
1003
1004    /* ====================================================================
1005       FullCalendar
1006       ==================================================================== */
1007    PREC.calendar = {
1008        init: function () {
1009            var $wrap = $('#prec-fullcalendar');
1010            if (!$wrap.length) return;
1011
1012            if (typeof FullCalendar === 'undefined') {
1013                $(window).on('load', PREC.calendar.init);
1014                return;
1015            }
1016
1017            // Tooltip-Element
1018            var $tip = $('<div class="prec-cal-tooltip"></div>').appendTo('body').hide();
1019
1020            var calendar = new FullCalendar.Calendar($wrap[0], {
1021                initialView: $wrap.data('view') === 'week'   ? 'timeGridWeek'  :
1022                             $wrap.data('view') === 'day'    ? 'timeGridDay'   :
1023                             $wrap.data('view') === 'agenda' ? 'listMonth'     : 'dayGridMonth',
1024                locale: 'de',
1025                firstDay: 1, // Montag
1026                buttonText: {
1027                    today: 'Heute',
1028                    month: 'Monat',
1029                    week:  'Woche',
1030                    day:   'Tag',
1031                    list:  'Liste'
1032                },
1033                headerToolbar: {
1034                    left:   'prev,next today',
1035                    center: 'title',
1036                    right:  'dayGridMonth,timeGridWeek,listMonth'
1037                },
1038                height: 'auto',
1039                dayMaxEvents: 4,
1040                moreLinkText: function(n) { return '+' + n + ' weitere'; },
1041                moreLinkClassNames: ['prec-cal-more-link'],
1042                events: {
1043                    url:     precFrontend.ajaxUrl,
1044                    method:  'GET',
1045                    extraParams: function () {
1046                        return {
1047                            action:     'prec_calendar_events',
1048                            nonce:      $wrap.data('nonce'),
1049                            category:   $wrap.data('category')    || '',
1050                            tag:        $wrap.data('tag')         || '',
1051                            location:   $wrap.data('location')    || '',
1052                            event_type: $wrap.data('event-type')  || '',
1053                            featured:   $wrap.data('featured')    || '',
1054                            district:   $wrap.attr('data-district')  || '', // seit v10.9.0
1055                            nonprofit:  $wrap.attr('data-nonprofit') || ''
1056                        };
1057                    }
1058                },
1059                eventContent: function(arg) {
1060                    var props = arg.event.extendedProps || {};
1061                    var time  = props.time || '';
1062                    var title = arg.event.title || '';
1063                    var view  = arg.view.type;
1064
1065                    // Listenansicht: einfacher Text
1066                    if (view === 'listMonth') {
1067                        return { html: '<span class="prec-cal-list-title">' + precText(title) + '</span>' };
1068                    }
1069
1070                    // Monats-/Wochenansicht: Design-Pill
1071                    var html = '<div class="prec-cal-pill">';
1072                    if (time && !arg.event.allDay) {
1073                        html += '<span class="prec-cal-pill-time">' + precText(time) + '</span>';
1074                    }
1075                    html += '<span class="prec-cal-pill-title">' + precText(title) + '</span>';
1076                    html += '</div>';
1077                    return { html: html };
1078                },
1079                eventClick: function (info) {
1080                    info.jsEvent.preventDefault();
1081                    $tip.hide();
1082                    if (info.event.url) window.location.href = info.event.url;
1083                },
1084                eventDidMount: function (info) {
1085                    var props = info.event.extendedProps || {};
1086                    var el    = info.el;
1087                    var cleanTitle = precText(info.event.title);
1088
1089                    // Hover-Tooltip
1090                    $(el).on('mouseenter', function(e) {
1091                        var html = '<div class="prec-cal-tip-inner">';
1092                        if (props.thumb) {
1093                            html += '<img class="prec-cal-tip-img" src="' + precAttr(props.thumb) + '" alt="">';
1094                        }
1095                        html += '<div class="prec-cal-tip-body">';
1096                        html += '<strong class="prec-cal-tip-title">
1096' + cleanTitle + '</strong>';
1097                        if (props.time) {
1098                            html += '<div class="prec-cal-tip-meta">🕐 ' + precText(props.time) + ' Uhr</div>';
1099                        }
1100                        var loc = [];
1101                        if (props.venue) loc.push(props.venue);
1102                        if (props.city) loc.push(props.city);
1103                        if (loc.length) {
1104                            html += '<div class="prec-cal-tip-meta">📍 ' + precText(loc.join(', ')) + '</div>';
1105                        }
1106                        if (props.category) {
1107                            html += '<div class="prec-cal-tip-cat"><span class="prec-cal-tip-cat-badge">' + precText(props.category) + '</span></div>';
1108                        }
1109                        if (props.excerpt) {
1110                            html += '<div class="prec-cal-tip-excerpt">' + precText(props.excerpt) + '</div>';
1111                        }
1112                        html += '</div></div>';
1113                        $tip.html(html).show();
1114                        PREC.calendar._posTip(e, $tip);
1115                    }).on('mousemove', function(e) {
1116                        PREC.calendar._posTip(e, $tip);
1117                    }).on('mouseleave', function() {
1118                        $tip.hide();
1119                    });
1120                },
1121                loading: function (isLoading) {
1122                    if (isLoading) {
1123                        $wrap.find('.prec-calendar-loading').show();
1124                    } else {
1125                        $wrap.find('.prec-calendar-loading').hide();
1126                    }
1127                }
1128            });
1129
1130            calendar.render();
1131        },
1132        _posTip: function(e, $tip) {
1133            var x = e.pageX + 16;
1134            var y = e.pageY + 12;
1135            var tw = $tip.outerWidth();
1136            var ww = $(window).width() + $(window).scrollLeft();
1137            if (x + tw + 20 > ww) x = e.pageX - tw - 16;
1138            $tip.css({ left: x, top: y });
1139        }
1140    };
1141
1142    /* ====================================================================
1143       Multi-marker map
1144       ==================================================================== */
1145    PREC.map = {
1146        init: function () {
1147            var $maps = $('.prec-events-map');
1148            if (!$maps.length) return;
1149
1150            $maps.each(function () {
1151                var $el      = $(this);
1152                var markers  = $el.data('markers') || [];
1153                var zoom     = parseInt($el.data('zoom')) || 10;
1154                var lat      = parseFloat($el.data('lat'));
1155                var lng      = parseFloat($el.data('lng'));
1156
1157                if (!markers.length) return;
1158
1159                // Auto center
1160                if (!lat || !lng) {
1161                    lat = parseFloat(markers[0].lat);
1162                    lng = parseFloat(markers[0].lng);
1163                }
1164
1165                var provider = typeof precFrontend.mapProvider !== 'undefined' ? precFrontend.mapProvider : 'osm';
1166
1167                if (provider === 'google' && typeof google !== 'undefined') {
1168                    PREC.map.initGoogle($el[0], markers, lat, lng, zoom);
1169                } else if (typeof L !== 'undefined') {
1170                    PREC.map.initLeaflet($el[0], markers, lat, lng, zoom);
1171                }
1172            });
1173        },
1174
1175        initLeaflet: function (el, markers, lat, lng, zoom) {
1176            var map = L.map(el, { scrollWheelZoom: false }).setView([lat, lng], zoom);
1177            L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
1178                maxZoom: 19,
1179                attribution: '© OpenStreetMap'
1180            }).addTo(map);
1181
1182            markers.forEach(function (m) {
1183                // Titel und Datum werden hier zu HTML zusammengesetzt, das
1184                // Leaflet als Markup rendert - ohne Maskierung waere das eine
1185                // Einladung zu gespeichertem Cross-Site-Scripting.
1186                var popup = '<strong><a href="' + precAttr(m.url) + '">' + precText(m.title) + '</a></strong>';
1187                if (m.date) popup += '<br><small>📅 ' + precText(m.date) + '</small>';
1188                L.marker([m.lat, m.lng]).addTo(map).bindPopup(popup);
1189            });
1190
1191            if (markers.length > 1) {
1192                var bounds = L.latLngBounds(markers.map(function (m) { return [m.lat, m.lng]; }));
1193                map.fitBounds(bounds, { padding: [30, 30] });
1194            }
1195
1196            // Strg+Scroll zum Zoomen (wie Google Maps cooperative)
1197            el.style.position = 'relative';
1198
1199            // Dauerhafter dezenter Hinweis unten links
1200            var badge = document.createElement('div');
1201            badge.innerHTML = '<span style="margin-right:5px;">⌨</span> Strg + Scrollen zum Zoomen';
1202            badge.style.cssText = 'position:absolute;bottom:8px;left:8px;z-index:998;background:rgba(255,255,255,.92);color:#374151;font-size:11px;font-weight:500;padding:5px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.12);pointer-events:none;line-height:1.3;backdrop-filter:blur(4px);';
1203            el.appendChild(badge);
1204
1205            // Vollbild-Overlay bei Scroll ohne Strg
1206            var overlay = document.createElement('div');
1207            overlay.textContent = 'Strg + Scrollen zum Zoomen';
1208            overlay.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4);color:#fff;font-size:15px;font-weight:600;z-index:999;pointer-events:none;opacity:0;transition:opacity .3s;border-radius:inherit;letter-spacing:.3px;';
1209            el.appendChild(overlay);
1210
1211            var hideTimer;
1212            el.addEventListener('wheel', function(e){
1213                if (e.ctrlKey || e.metaKey) {
1214                    map.scrollWheelZoom.enable();
1215                    badge.style.opacity = '0';
1216                } else {
1217                    map.scrollWheelZoom.disable();
1218                    overlay.style.opacity = '1';
1219                    badge.style.opacity = '0';
1220                    clearTimeout(hideTimer);
1221                    hideTimer = setTimeout(function(){ overlay.style.opacity = '0'; badge.style.opacity = '1'; }, 1200);
1222                }
1223            }, { passive: true });
1224            el.addEventListener('mouseleave', function(){ map.scrollWheelZoom.disable(); overlay.style.opacity = '0'; badge.style.opacity = '1'; });
1225        },
1226
1227        initGoogle: function (el, markers, lat, lng, zoom) {
1228            var map = new google.maps.Map(el, { center: { lat: lat, lng: lng }, zoom: zoom, gestureHandling: 'cooperative' });
1229            var bounds = new google.maps.LatLngBounds();
1230            var infoWindow = new google.maps.InfoWindow();
1231
1232            markers.forEach(function (m) {
1233                var marker = new google.maps.Marker({
1234                    position: { lat: m.lat, lng: m.lng },
1235                    map: map,
1236                    title: m.title
1237                });
1238                bounds.extend(marker.position);
1239                marker.addListener('click', function () {
1240                    // Gleiche Begruendung wie beim Leaflet-Popup weiter oben.
1241                    var content = '<strong><a href="' + precAttr(m.url) + '">' + precText(m.title) + '</a></strong>';
1242                    if (m.date) content += '<br><small>📅 ' + precText(m.date) + '</small>';
1243                    infoWindow.setContent(content);
1244                    infoWindow.open(map, marker);
1245                });
1246            });
1247
1248            if (markers.length > 1) map.fitBounds(bounds);
1249        }
1250    };
1251
1252    /* ====================================================================
1253       Lightbox – Professionelle Slider-Galerie
1254       ==================================================================== */
1255    PREC.gallery = {
1256        $overlay: null,
1257        $img: null,
1258        $counter: null,
1259        items: [],
1260        current: 0,
1261        touchStartX: 0,
1262
1263        init: function () {
1264            var self = this;
1265
1266            // Click auf Gallery-Item → Lightbox öffnen
1267            $(document).on('click', '.prec-gallery-item[data-lightbox], a.prec-gallery-item', function (e) {
1268                e.preventDefault();
1269                var $clicked = $(this);
1270                var group = $clicked.closest('.prec-gallery-grid, .prec-event-gallery');
1271
1272                // Alle Bilder der Gruppe sammeln
1273                self.items = [];
1274                self.captions = []; // Bildautor je Bild (seit v10.3.1)
1275                group.find('a.prec-gallery-item, .prec-gallery-item[data-lightbox]').each(function () {
1276                    self.items.push($(this).attr('href'));
1277                    self.captions.push($(this).attr('data-caption') || '');
1278                });
1279                if (!self.items.length) {
1280                    self.items.push($clicked.attr('href'));
1281                    self.captions.push($clicked.attr('data-caption') || '');
1282                }
1283
1284                // Index des geklickten Bildes
1285                self.current = self.items.indexOf($clicked.attr('href'));
1286                if (self.current < 0) self.current = 0;
1287
1288                self.open();
1289            });
1290
1291            // Keyboard
1292            $(document).on('keydown.precLightbox', function (e) {
1293                if (!self.$overlay || !self.$overlay.is(':visible')) return;
1294                if (e.key === 'Escape') self.close();
1295                if (e.key === 'ArrowRight') self.next();
1296                if (e.key === 'ArrowLeft') self.prev();
1297            });
1298        },
1299
1300        build: function () {
1301            if (this.$overlay) return;
1302            var html = '<div class="prec-lb">' +
1303                '<div class="prec-lb-backdrop"></div>' +
1304                '<button class="prec-lb-close" aria-label="Schließen"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>' +
1305                '<button class="prec-lb-nav prec-lb-prev" aria-label="Vorheriges Bild"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>' +
1306                '<button class="prec-lb-nav prec-lb-next" aria-label="Nächstes Bild"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg></button>' +
1307                '<div class="prec-lb-stage">' +
1308                    '<div class="prec-lb-spinner"></div>' +
1309                    '<img class="prec-lb-img" src="" alt="">' +
1310                '</div>' +
1311                '<div class="prec-lb-footer">' +
1312                    '<span class="prec-lb-caption"></span>' +
1313                    '<span class="prec-lb-counter"></span>' +
1314                '</div>' +
1315            '</div>';
1316
1317            this.$overlay = $(html).appendTo('body');
1318            this.$img = this.$overlay.find('.prec-lb-img');
1319            this.$counter = this.$overlay.find('.prec-lb-counter');
1320            this.$caption = this.$overlay.find('.prec-lb-caption');
1321
1322            var self = this;
1323
1324            // Close
1325            this.$overlay.find('.prec-lb-close').on('click', function () { self.close(); });
1326            this.$overlay.find('.prec-lb-backdrop').on('click', function () { self.close(); });
1327
1328            // Nav
1329            this.$overlay.find('.prec-lb-prev').on('click', function (e) { e.stopPropagation(); self.prev(); });
1330            this.$overlay.find('.prec-lb-next').on('click', function (e) { e.stopPropagation(); self.next(); });
1331
1332            // Touch/Swipe
1333            this.$overlay[0].addEventListener('touchstart', function (e) {
1334                self.touchStartX = e.changedTouches[0].screenX;
1335            }, { passive: true });
1336            this.$overlay[0].addEventListener('touchend', function (e) {
1337                var diff = e.changedTouches[0].screenX - self.touchStartX;
1338                if (Math.abs(diff) > 50) {
1339                    diff > 0 ? self.prev() : self.next();
1340                }
1341            }, { passive: true });
1342        },
1343
1344        open: function () {
1345            this.build();
1346            this.show(false);
1347            this.$overlay.addClass('prec-lb-visible');
1348            $('body').css('overflow', 'hidden');
1349        },
1350
1351        close: function () {
1352            var self = this;
1353            this.$overlay.removeClass('prec-lb-visible');
1354            $('body').css('overflow', '');
1355            setTimeout(function () {
1356                self.$img.attr('src', '');
1357            }, 300);
1358        },
1359
1360        show: function (animate) {
1361            var self = this;
1362            var src = this.items[this.current];
1363            if (!src) return;
1364
1365            // Counter + Bildautor
1366            this.$counter.text((this.current + 1) + ' / ' + this.items.length);
1367            if (this.$caption) this.$caption.text((this.captions && this.captions[this.current]) || '');
1368
1369            // Nav Buttons
1370            this.$overlay.find('.prec-lb-prev').toggle(this.items.length > 1);
1371            this.$overlay.find('.prec-lb-next').toggle(this.items.length > 1);
1372
1373            // Loading state
1374            this.$overlay.addClass('prec-lb-loading');
1375            if (animate) this.$img.addClass('prec-lb-img-fade');
1376
1377            // Preload
1378            var img = new Image();
1379            img.onload = function () {
1380                self.$img.attr('src', src);
1381                self.$overlay.removeClass('prec-lb-loading');
1382                if (animate) {
1383                    setTimeout(function () { self.$img.removeClass('prec-lb-img-fade'); }, 50);
1384                }
1385            };
1386            img.onerror = function () {
1387                self.$overlay.removeClass('prec-lb-loading');
1388            };
1389            img.src = src;
1390        },
1391
1392        next: function () {
1393            this.current = (this.current + 1) % this.items.length;
1394            this.show(true);
1395        },
1396
1397        prev: function () {
1398            this.current = (this.current - 1 + this.items.length) % this.items.length;
1399            this.show(true);
1400        }
1401    };
1402
1403    /* ------------------------------------------------------------------ */
1404    /*  Buchungssystem                                                      */
1405    /* ------------------------------------------------------------------ */
1406    PREC.booking = {
1407        init: function () {
1408            if (!$('.prec-booking-form').length) return;
1409
1410            $(document).on('change', '.prec-booking-date', function () {
1411                var $form     = $(this).closest('.prec-booking-form');
1412                var eventId   = $form.data('event-id');
1413                var date      = $(this).val();
1414                var $slotsWrap= $form.find('.prec-booking-slots');
1415                var $grid     = $form.find('.prec-slots-grid');
1416                var $fields   = $form.find('.prec-booking-fields');
1417
1418                $grid.html('<p>Lade Zeitslots…</p>');
1419                $slotsWrap.show();
1420                $fields.hide();
1421
1422                $.post(precFrontend.ajaxUrl, {
1423                    action: 'prec_get_timeslots',
1424                    nonce: precFrontend.nonce,
1425                    event_id: eventId,
1426                    date: date
1427                }, function (res) {
1428                    $grid.empty();
1429                    if (res.success && res.data.length) {
1430                        res.data.forEach(function (slot) {
1431                            var $btn = $('<button type="button" class="prec-slot-btn">').text(slot.label).data('time', slot.time);
1432                            if (slot.available === 0) $btn.prop('disabled', true).addClass('disabled');
1433                            $grid.append($btn);
1434                        });
1435                    } else {
1436                        $grid.html('<p>Keine Zeitslots verfügbar.</p>');
1437                    }
1438                });
1439            });
1440
1441            $(document).on('click', '.prec-slot-btn:not(.disabled)', function () {
1442                var $form = $(this).closest('.prec-booking-form');
1443                $('.prec-slot-btn').removeClass('selected');
1444                $(this).addClass('selected');
1445                $form.data('selected-time', $(this).data('time'));
1446                $form.find('.prec-booking-fields').show();
1447            });
1448
1449            $(document).on('click', '.prec-book-submit', function () {
1450                var $form  = $(this).closest('.prec-booking-form');
1451                var $msg   = $form.find('.prec-booking-message');
1452                var $btn   = $(this);
1453                var time   = $form.data('selected-time');
1454
1455                if (!time) { $msg.text('Bitte einen Zeitslot wählen.'); return; }
1456
1457                $btn.prop('disabled', true);
1458                $.post(precFrontend.ajaxUrl, {
1459                    action: 'prec_book_timeslot',
1460                    nonce: precFrontend.nonce,
1461                    event_id: $form.data('event-id'),
1462                    date: $form.find('[name=booking_date]').val(),
1463                    time: time,
1464                    name: $form.find('[name=booking_name]').val(),
1465                    email: $form.find('[name=booking_email]').val(),
1466                    phone: $form.find('[name=booking_phone]').val(),
1467                    seats: $form.find('[name=booking_seats]').val()
1468                }, function (res) {
1469                    $btn.prop('disabled', false);
1470                    $msg.text(res.data.message || (res.success ? 'Erfolgreich!' : 'Fehler'));
1471                    $msg.css('color', res.success ? 'green' : 'red');
1472                });
1473            });
1474        }
1475    };
1476
1477    // =========================================================
1478    // AJAX PAGINATION
1479    // =========================================================
1480
1481    /**
1482     * Externe Filter anwenden (seit v10.2.0, genutzt von den Filter-Buttons).
1483     *
1484     * Setzt exakt die Werte, die auch Filterleiste und Blaettern verwenden -
1485     * es gibt keinen zweiten Datenpfad. Ein leeres Objekt setzt alles zurueck.
1486     * Rueckgabe false, wenn auf der Seite keine Liste steht.
1487     */
1488    PREC.applyExternalFilter = function (cfg) {
1489        cfg = cfg || {};
1490        var $outer  = $('.prec-shortcode-wrap').first();
1491        // WICHTIG: mit div qualifizieren - [class*="prec-layout-"] traefe sonst
1492        // auch das Layout-Auswahlfeld (select.prec-layout-select) in der
1493        // Filterleiste, das im DOM VOR der Liste steht.
1494        var $layout = $outer.find('div[class*="prec-layout-"]').first();
1495        if (!$layout.length) return false;
1496
1497        // Urzustand der Sperr-Attribute einmalig sichern, BEVOR der erste
1498        // Button sie ueberschreibt. Der Reset-Klick (leeres cfg) stellt so die
1499        // eigenen Shortcode-Attribute des Kalenders wieder her (venue=, city=,
1500        // date_from= usw.), statt sie pauschal zu loeschen.
1501        if (!$outer.data('prec-fbtn-orig')) {
1502            $outer.data('prec-fbtn-orig', {
1503                von:       ($outer.data('prec-locked-date-from') || '') + '',
1504                bis:       ($outer.data('prec-locked-date-to')   || '') + '',
1505                search:    ($outer.data('prec-locked-search')    || '') + '',
1506                venue:     ($outer.data('prec-locked-venue')     || '') + '',
1507                organizer: ($outer.data('prec-locked-organizer') || '') + '',
1508                city:      ($outer.data('prec-locked-city')      || '') + '',
1509                past:      ($outer.data('prec-locked-past')      || '') + '',
1510                category:  ''
1511            });
1512        }
1513        var reset = !cfg.von && !cfg.bis && !cfg.search && !cfg.venue
1514                 && !cfg.organizer && !cfg.city && !cfg.category && !cfg.past;
1515        var ziel  = reset ? $outer.data('prec-fbtn-orig') : cfg;
1516
1517        if (PREC.filter) {
1518            PREC.filter.dateFrom   = ziel.von || '';
1519            PREC.filter.dateTo     = ziel.bis || '';
1520            PREC.filter.activeDays = 0;
1521            PREC.filter.searchQuery = '';
1522        }
1523        // Sichtbare Suchfelder leeren - die Ladefunktion liest die Suche
1524        // direkt aus den Eingabefeldern, nicht nur aus dem Filter-Zustand.
1525        $outer.find('#prec-event-search, .prec-search-input, [data-prec-search]').val('');
1526        $outer.data('prec-locked-date-from', ziel.von       || '');
1527        $outer.data('prec-locked-date-to',   ziel.bis       || '');
1528        $outer.data('prec-locked-search',    ziel.search    || '');
1529        $outer.data('prec-locked-venue',     ziel.venue     || '');
1530        $outer.data('prec-locked-organizer', ziel.organizer || '');
1531        $outer.data('prec-locked-city',      ziel.city      || '');
1532        $outer.data('prec-locked-past',      ziel.past      || '');
1533
1534        // Aktiven Kategorie-Knopf der Filterleiste optisch zuruecksetzen,
1535        // damit keine widerspruechliche Auswahl angezeigt wird.
1536        $outer.find('.prec-filter-btn.active').removeClass('active');
1537        $outer.find('.prec-filter-btn[data-filter="*"]').addClass('active');
1538
1539        // Kategorie am Wrapper merken: Die Ladefunktion liest die Kategorie
1540        // sonst nur aus der Filterleiste - beim Blaettern ginge die
1541        // Button-Kategorie verloren.
1542        $outer.data('prec-ext-category', ziel.category || '');
1543
1544        _precLoadEvents($layout, 1, null, ziel.category || '', true);
1545        return true;
1546    };
1547
1548    // Globale loadEvents Funktion (wird von Filtern und Pagination genutzt)
1549    var _precLoadEvents = function($wrap, page, perPage, category, scrollAfter) {
1550        if (typeof precFrontend === 'undefined') {
1551            console.error('[EK] precFrontend nicht definiert — AJAX nicht möglich');
1552            return;
1553        }
1554        var layout = $wrap.attr('class').match(/prec-layout-([\w-]+)/);
1555        layout = layout ? layout[1] : 'grid';
1556
1557        var $outer   = $wrap.closest('.prec-shortcode-wrap');
1558
1559        // Seit v6.9.7: Kategorie wird ausschließlich aus dem aktiven Button gelesen.
1560        // Shortcode-Kategorie wird beim Initial-Render als active-Button gesetzt, ist
1561        // aber NICHT mehr hart gelockt — der User kann frei wechseln.
1562        // Das frühere data-prec-locked-category wird ignoriert (Backward-Compat: bleibt
1563        // im DOM, hat aber keine Wirkung mehr).
1564        var cat = category || '';
1565        if (!cat) {
1566            var $activeBtn = $outer.find('.prec-filter-btn.active');
1567            if ($activeBtn.length && $activeBtn.data('filter') !== '*') cat = $activeBtn.data('filter');
1568        }
1569        if (!cat) cat = ($outer.find('[data-prec-category]').val() || $wrap.find('[data-prec-category]').val()) || '';
1570        // Von einem Filter-Button gesetzte Kategorie (seit v10.2.0) - greift
1571        // nur, wenn weder Aufrufer noch Filterleiste eine Kategorie liefern.
1572        if (!cat) cat = ($outer.data('prec-ext-category') || '') + '';
1573
1574        // ── Filter-State-Fallback (seit v7.1.3, erweitert v7.1.6) ─────────
1575        // Drei Quellen werden in Reihenfolge geprüft:
1576        //  (1) Filter-Bar im DOM (User-Interaktion, höchste Priorität)
1577        //  (2) URL-Parameter (?s=…&prec_category=…) — von [prec_event_search]
1578        //  (3) Shortcode-Attribute am Wrapper (data-prec-locked-*) — Shortcode-Defaults
1579        //
1580        // Wichtig: Nur wenn die Filter-Bar GAR NICHT im DOM ist, fällt JS auf
1581        // Shortcode-Defaults zurück. Wenn die Filter-Bar da ist, aber der User
1582        // alle Checkboxen abgehakt hat, soll "Alle" gelten — Shortcode-Default
1583        // greift dann NICHT, weil das User-Intent ist.
1584        function _precUrlParam(name) {
1585            try {
1586                var sp = new URLSearchParams(window.location.search);
1587                return sp.get(name) || '';
1588            } catch (e) { return ''; }
1589        }
1590
1591        var search = ($outer.find('#prec-event-search').val()
1592                   || $outer.find('.prec-search-input').val()
1593                   || $outer.find('[data-prec-search]').val()
1594                   || $wrap.find('[data-prec-search]').val()
1595                   || _precUrlParam('s')) || '';
1596
1597        // ── LOCATION ────────────────────────────────────────────────
1598        var $locChecks       = $outer.find('.prec-loc-check');
1599        var hasLocFilterBar  = $locChecks.length > 0;
1600        var locationFromBar  = (function(){
1601            var slugs = [];
1602            $outer.find('.prec-loc-check:checked').each(function(){ slugs.push($(this).val()); });
1603            return slugs.join(',');
1604        })();
1605
1606        var location;
1607        if (hasLocFilterBar) {
1608            // User hat Hoheit — auch wenn alle abgehakt = "Alle Orte"
1609            location = locationFromBar;
1610        } else {
1611            // Keine Filter-Bar → Shortcode-Default oder URL
1612            location = ($outer.find('select[name="prec_location"]').val()
1613                     || _precUrlParam('prec_location')
1614                     || $outer.data('prec-locked-location')
1615                     || $wrap.data('prec-locked-location')
1616                     || '') + '';
1617        }
1618
1619        // ── KATEGORIE ───────────────────────────────────────────────
1620        var $catBtns = $outer.find('.prec-filter-btn');
1621        if (!cat) {
1622            // Wenn Filter-Bar mit Buttons da ist und ein Button "*" (Alle) aktiv,
1623            // dann hat User explizit "Alle" gewählt → kein Shortcode-Fallback.
1624            // Wenn KEINE Filter-Bar im DOM → Shortcode-Default greift.
1625            if ($catBtns.length === 0) {
1626                cat = ($outer.find('select[name="prec_category"]').val()
1627                    || _precUrlParam('prec_category')
1628                    || $outer.data('prec-locked-category')
1629                    || $wrap.data('prec-locked-category')
1630                    || '') + '';
1631            } else {
1632                // Filter-Bar vorhanden — entweder ein Button (außer "*") ist aktiv
1633                // (haben wir oben schon erfasst), oder "*" / nichts aktiv = "Alle"
1634                cat = ($outer.find('select[name="prec_category"]').val()
1635                    || _precUrlParam('prec_category')
1636                    || '') + '';
1637            }
1638        }
1639        var limit    = perPage || parseInt($outer.find('#prec-per-page').val(), 10) || parseInt($wrap.data('limit'), 10) || 20;
1640        var past     = $wrap.data('past') || '0';
1641        var $list    = $wrap.find('.prec-events-grid, .prec-ic2-grid, .prec-cc-grid, .prec-ig-grid, .prec-cl-list, .prec-horizontal-list, .prec-masonry-grid, .prec-events-list, .prec-timeline, [data-prec-list]'); // [data-prec-list] seit v10.9.0: Editions-Layouts
1642        if (!$list.length) {
1643            // Grid-Container fehlt → erstellen mit korrektem Layout
1644            var containerMap = {
1645                'grid': 'prec-events-grid',
1646                'image-cards': 'prec-ic2-grid',
1647                'color-cards': 'prec-cc-grid',
1648                'image-grid': 'prec-ig-grid',
1649                'color-list': 'prec-cl-list',
1650                'list': 'prec-events-list',
1651                'horizontal': 'prec-horizontal-list',
1652                'masonry': 'prec-masonry-grid',
1653                'timeline': 'prec-timeline'
1654            };
1655            var containerClass = containerMap[layout] || 'prec-events-grid';
1656            $wrap.find('.prec-no-events, .prec-no-events-filtered').remove();
1657            var $newList = $('<div>').addClass(containerClass);
1658            $wrap.prepend($newList);
1659            $list = $newList;
1660        }
1661
1662        // Datumsfilter: Von PREC.filter lesen
1663        var dateFrom = '', dateTo = '';
1664        if (PREC.filter) {
1665            if (PREC.filter.dateFrom) { dateFrom = PREC.filter.dateFrom; }
1666            if (PREC.filter.dateTo) { dateTo = PREC.filter.dateTo; }
1667            if (!dateFrom && PREC.filter.activeDays > 0) {
1668                // precFrontend.today kommt vom Server (WordPress-Zeitzone) — kein UTC-Versatz
1669                var todayStr = (precFrontend && precFrontend.today) ? precFrontend.today : (function(){
1670                    var d = new Date();
1671                    return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
1672                })();
1673                dateFrom = todayStr;
1674                if (PREC.filter.activeDays === 1) {
1675                    dateTo = dateFrom;
1676                } else {
1677                    var parts = todayStr.split('-');
1678                    var end = new Date(+parts[0], +parts[1]-1, +parts[2]);
1679                    end.setDate(end.getDate() + PREC.filter.activeDays - 1);
1680                    dateTo = end.getFullYear() + '-' + String(end.getMonth()+1).padStart(2,'0') + '-' + String(end.getDate()).padStart(2,'0');
1681                }
1682            }
1683        }
1684
1685        // Date-Fallback aus URL (seit v7.1.3): wenn von [prec_event_search]
1686        // kommend, kann prec_date=2026-06-17 in der URL stehen.
1687        if (!dateFrom) {
1688            var urlDate = _precUrlParam('prec_date');
1689            if (urlDate && /^\d{4}-\d{2}-\d{2}$/.test(urlDate)) {
1690                dateFrom = urlDate;
1691                dateTo   = urlDate;
1692            }
1693        }
1694
1695        $wrap.addClass('prec-loading');
1696
1697        // Skeleton-Platzhalter während des Ladens
1698        var skelCount = Math.min(limit, 6);
1699        var skelHtml = '';
1700        for (var s = 0; s < skelCount; s++) skelHtml += '<div class="prec-skeleton"></div>';
1701        $list.html(skelHtml);
1702
1703        // ── Shortcode-Defaults (seit v7.1.7) ────────────────────────────
1704        // Alle weiteren Shortcode-Filter (außer location/category, die schon
1705        // oben behandelt sind) werden vom Wrapper gelesen und im AJAX-POST
1706        // mitgeschickt. Diese können vom User nicht über die Filter-Bar
1707        // verändert werden, sind also immer "locked" — bei jedem Pagination-,
1708        // Layout- und Kategorie-Reset-Klick werden sie respektiert.
1709        var lockedVenue     = ($outer.data('prec-locked-venue')      || '') + '';
1710        var lockedTag       = ($outer.data('prec-locked-tag')        || '') + '';
1711        var lockedOrganizer = ($outer.data('prec-locked-organizer')  || '') + '';
1712        var lockedEventType = ($outer.data('prec-locked-event-type') || '') + '';
1713        var lockedFeatured  = ($outer.data('prec-locked-featured')   || '') + '';
1714        var lockedOrderby   = ($outer.data('prec-locked-orderby')    || '') + '';
1715        var lockedOrder     = ($outer.data('prec-locked-order')      || '') + '';
1716        var lockedIds       = ($outer.data('prec-locked-ids')        || '') + '';
1717        // seit v7.1.8: drei weitere persistente Shortcode-Defaults
1718        var lockedShowOffline   = ($outer.data('prec-locked-show-offline')   || '') + '';
1719        var lockedGroupMultiday = ($outer.data('prec-locked-group-multiday') || '') + '';
1720        var lockedSliderEffect  = ($outer.data('prec-locked-slider-effect')  || '') + '';
1721        // Seit v9.8.3: Datumsbereich, Ausschluss und (fixe) Suche als Shortcode-
1722        // Defaults ebenfalls persistent — überleben Blättern/Filtern/Layoutwechsel.
1723        var lockedDateFrom  = ($outer.data('prec-locked-date-from') || '') + '';
1724        var lockedDateTo    = ($outer.data('prec-locked-date-to')   || '') + '';
1725        var lockedExclude   = ($outer.data('prec-locked-exclude')   || '') + '';
1726        var lockedSearch    = ($outer.data('prec-locked-search')    || '') + '';
1727        // Seit v10.2.0 (Filter-Buttons): Stadt-Filter und Status-Vorgabe.
1728        var lockedCity      = ($outer.data('prec-locked-city')      || '') + '';
1729        var lockedPast      = ($outer.data('prec-locked-past')      || '') + '';
1730        // Seit v10.9.0: Ortsteil + „Nur Non-Profit" — Filterleiste hat Vorrang,
1731        // ohne Bedienelement gilt die Vorgabe aus dem Kurzbefehl (district="…", nonprofit="1").
1732        var $otSel   = $outer.find('.prec-district-select').first();
1733        var district = $otSel.length ? ($otSel.val() || '') : (($outer.data('prec-locked-district') || '') + '');
1734        var $npChk   = $outer.find('.prec-np-check').first();
1735        var nonprofit = $npChk.length ? ($npChk.is(':checked') ? '1' : '') : (($outer.data('prec-locked-nonprofit') || '') + '');
1736
1737        $.post(precFrontend.ajaxUrl, {
1738            action:         'prec_load_events',
1739            nonce:          precFrontend.nonce,
1740            page:           page,
1741            limit:          limit,
1742            layout:         layout,
1743            category:       cat,
1744            location:       location,
1745            search:         search || lockedSearch,
1746            date_from:      dateFrom || lockedDateFrom,
1747            date_to:        dateTo || lockedDateTo,
1748            past:           lockedPast !== '' ? lockedPast : past,
1749            city:           lockedCity,
1750            district:       district,
1751            nonprofit:      nonprofit,
1752            pagination:     (($outer.data('prec-locked-pagination') + '') === '0') ? '0' : '', // seit v10.9.0
1753            venue:          lockedVenue,
1754            tag:            lockedTag,
1755            organizer:      lockedOrganizer,
1756            event_type:     lockedEventType,
1757            featured:       lockedFeatured,
1758            orderby:        lockedOrderby,
1759            order:          lockedOrder,
1760            ids:            lockedIds,
1761            show_offline:   lockedShowOffline,
1762            group_multiday: lockedGroupMultiday,
1763            slider_effect:  lockedSliderEffect,
1764            exclude:        lockedExclude
1765        }, function (res) {
1766            $wrap.removeClass('prec-loading');
1767            if (res.success && res.data) {
1768
1769                // Slider: vollständiges Template (inkl. Wrapper + Inline-JS) ersetzen
1770                if (layout === 'slider') {
1771                    var html = res.data.html || '';
1772                    // Wrapper finden und ersetzen
1773                    var $sliderWrap = $wrap.find('.prec-slider-wrap');
1774                    if ($sliderWrap.length) {
1775                        $sliderWrap.replaceWith(html);
1776                    } else {
1777                        $list.html(html);
1778                    }
1779                    // Inline-Scripts ausführen (jQuery .html() ignoriert sie)
1780                    var $tmp = $('<div>').append($.parseHTML(html, document, true));
1781                    $tmp.find('script').each(function () {
1782                        try { $.globalEval(this.text || this.textContent); } catch (e) {}
1783                    });
1784                    if (res.data.pagination) $wrap.find('.prec-pagination').replaceWith(res.data.pagination);
1785                    else $wrap.find('.prec-pagination').empty();
1786                    if (scrollAfter) $('html,body').animate({ scrollTop: $wrap.offset().top - 80 }, 300);
1787                    return;
1788                }
1789
1790                // IMMER ersetzen - sonst blieben bei leerer Antwort die
1791                // Skeleton-Platzhalter dauerhaft stehen (seit v10.2.4).
1792                $list.html(res.data.html || ('<p class="prec-no-events">'
1793                    + ((precFrontend.i18n && precFrontend.i18n.noEvents) || 'Keine Veranstaltungen gefunden.')
1794                    + '</p>'));
1795                if (res.data.pagination) $wrap.find('.prec-pagination').replaceWith(res.data.pagination);
1796                else $wrap.find('.prec-pagination').empty();
1797
1798                // Spalten nach AJAX wiederherstellen — liest immer direkt vom Picker
1799                var colLayouts = ['grid','color-cards','color-list','image-cards','image-grid','masonry'];
1800                if (colLayouts.indexOf(layout) !== -1) {
1801                    var vw      = window.innerWidth || document.documentElement.clientWidth;
1802                    var $picker = $outer.find('#prec-cols-picker');
1803                    var cols    = $picker.length ? (parseInt($picker.val()) || 3) : 3;
1804
1805                    // Responsive Deckel (verhindert zu viele Spalten auf kleinen Screens)
1806                    if (vw <= 480)      cols = Math.min(cols, 1);
1807                    else if (vw <= 768) cols = Math.min(cols, 2);
1808
1809                    var gridVal = 'repeat(' + cols + ',1fr)';
1810                    $list.css({
1811                        'display':               'grid',
1812                        'grid-template-columns': gridVal,
1813                        'gap':                   '24px',
1814                        'width':                 '100%'
1815                    });
1816                    // CSS-Variable ebenfalls aktualisieren
1817                    $wrap[0].style.setProperty('--prec-grid-cols', String(cols));
1818                    if ($list[0]) $list[0].style.setProperty('--prec-grid-cols', String(cols));
1819                }
1820
1821                if (scrollAfter) $('html,body').animate({ scrollTop: $wrap.offset().top - 80 }, 300);
1822            }
1823        }).fail(function () {
1824            $wrap.removeClass('prec-loading');
1825            console.error('[EK] AJAX load_events fehlgeschlagen');
1826        });
1827    };
1828
1829    function initAjaxPagination() {
1830        // Klick auf Seitenzahl
1831        $(document).on('click', '.prec-page-btn', function (e) {
1832            e.preventDefault();
1833            var $btn  = $(this);
1834            var page  = parseInt($btn.data('page'), 10);
1835            var $wrap = $btn.closest('[class*="prec-layout-"]');
1836            if (!$wrap.length || !page) return;
1837            _precLoadEvents($wrap, page, null, null, true);
1838        });
1839
1840        // Anzahl pro Seite geändert → Seite 1 neu laden
1841        $(document).on('change', '#prec-per-page', function () {
1842            var $outer = $(this).closest('.prec-shortcode-wrap');
1843            var $wrap  = $outer.find('div[class*="prec-layout-"]').first();
1844            if (!$wrap.length) return;
1845            _precLoadEvents($wrap, 1, parseInt($(this).val(), 10), null, true);
1846        });
1847
1848        // Spalten-Picker: setzt grid-template-columns direkt + CSS-Variable
1849        $(document).on('change', '#prec-cols-picker', function () {
1850            var cols   = parseInt($(this).val(), 10);
1851            var $outer = $(this).closest('.prec-shortcode-wrap');
1852            var $wrap  = $outer.find('div[class*="prec-layout-"]').first();
1853            if (!$wrap.length) return;
1854            var gridVal = 'repeat(' + cols + ', 1fr)';
1855            // CSS-Variable auf Wrapper setzen
1856            $wrap[0].style.setProperty('--prec-grid-cols', String(cols));
1857            // Grid-Template direkt auf alle Grid-Container setzen (zuverlässiger)
1858            $wrap.find('.prec-cc-grid,.prec-ic2-grid,.prec-ig-grid,.prec-events-grid').each(function(){
1859                this.style.setProperty('--prec-grid-cols', String(cols));
1860                this.style.gridTemplateColumns = gridVal;
1861            });
1862            // Color-list (CSS-Grid)
1863            $wrap.find('.prec-cl-list').each(function(){
1864                this.style.setProperty('--prec-grid-cols', String(cols));
1865                this.style.gridTemplateColumns = gridVal;
1866            });
1867            // Masonry (columns CSS property)
1868            $wrap.find('.prec-masonry-grid').each(function(){
1869                this.style.columns = String(cols);
1870            });
1871        });
1872
1873        // Spalten-Picker bei Layout-Wechsel aktualisieren
1874        $(document).on('change', '#prec-layout-switcher', function () {
1875            var layout = $(this).val();
1876            var colLayouts = ['grid','color-cards','image-cards','image-grid','color-list','masonry'];
1877            var $picker = $(this).closest('.prec-filter-controls').find('#prec-cols-picker');
1878            if (colLayouts.indexOf(layout) === -1) {
1879                $picker.closest('label').hide();
1880            } else {
1881                $picker.closest('label').show();
1882                $picker.attr('data-layout', layout);
1883
1884                // Spalten aus Responsive-Einstellungen laden
1885                var vw   = window.innerWidth || document.documentElement.clientWidth;
1886                var isPortrait2 = window.matchMedia ? window.matchMedia('(orientation: portrait)').matches : (window.innerHeight > window.innerWidth);
1887                    var dev;
1888                    if      (vw <= 480)  dev = 'mobile_port';
1889                    else if (vw <= 600)  dev = isPortrait2 ? 'mobile_port' : 'mobile_land';
1890                    else if (vw <= 900)  dev = isPortrait2 ? 'tablet_port' : 'mobile_land';
1891                    else if (vw <= 1024) dev = isPortrait2 ? 'tablet_port' : 'tablet_land';
1892                    else                 dev = 'desktop';
1893                var resp = (typeof precFrontend !== 'undefined' && precFrontend.responsive) ? precFrontend.responsive : null;
1894                if (resp && resp[dev] && resp[dev].layoutCols && resp[dev].layoutCols[layout]) {
1895                    var cols = parseInt(resp[dev].layoutCols[layout]);
1896                    if (vw <= 480)  cols = Math.min(cols, 1);
1897                    if (vw <= 1023) cols = Math.min(cols, 2);
1898                    $picker.val(String(cols));
1899                }
1900            }
1901        });
1902    }
1903
1904    // =========================================================
1905    // LIVE FILTER & SEARCH
1906    // =========================================================
1907    function initLiveFilter() {
1908        var filterTimer;
1909
1910        $(document).on('change', '[data-prec-category]', function () {
1911            triggerFilter($(this).closest('[class*="prec-layout-"]'));
1912        });
1913
1914        $(document).on('input', '[data-prec-search]', function () {
1915            clearTimeout(filterTimer);
1916            var $wrap = $(this).closest('[class*="prec-layout-"]');
1917            filterTimer = setTimeout(function () { triggerFilter($wrap); }, 400);
1918        });
1919
1920        function triggerFilter($wrap) {
1921            if (!$wrap.length) return;
1922            var layout   = ($wrap.attr('class').match(/prec-layout-([\w-]+)/) || [])[1] || 'grid';
1923            var category = $wrap.find('[data-prec-category]').val() || '';
1924            var search   = $wrap.find('[data-prec-search]').val() || '';
1925            var $list    = $wrap.find('.prec-events-grid, .prec-ic2-grid, .prec-cc-grid, .prec-ig-grid, .prec-cl-list, .prec-horizontal-list, .prec-masonry-grid, .prec-events-list, .prec-timeline, [data-prec-list]'); // [data-prec-list] seit v10.9.0: Editions-Layouts
1926            if (!$list.length) $list = $wrap;
1927
1928            $wrap.addClass('prec-loading');
1929
1930            $.post(precFrontend.ajaxUrl, {
1931                action:   'prec_load_events',
1932                nonce:    precFrontend.nonce,
1933                page:     1,
1934                layout:   layout,
1935                category: category,
1936                search:   search
1937            }, function (res) {
1938                $wrap.removeClass('prec-loading');
1939                if (res.success && res.data) {
1940                    if (res.data.html)       $list.html(res.data.html);
1941                    if (res.data.pagination) $wrap.find('.prec-pagination').replaceWith(res.data.pagination);
1942                }
1943            });
1944        }
1945    }
1946
1947    // =========================================================
1948    // MASONRY REFLOW (nach Bild-Laden)
1949    // =========================================================
1950    function initMasonry() {
1951        var $grid = $('[data-prec-masonry]');
1952        if (!$grid.length) return;
1953        $grid.find('img').on('load', function () {
1954            // CSS columns reflow via display trick
1955            $grid.css('display','none').height(); // force reflow
1956            $grid.css('display','');
1957        });
1958    }
1959
1960    // =========================================================
1961    // SHARE BUTTON – Link kopieren (Instagram)
1962    // =========================================================
1963    function initCopyShare() {
1964        $(document).on('click', '.prec-share-copy', function (e) {
1965            e.preventDefault();
1966            var url = $(this).data('url');
1967            if (navigator.clipboard && url) {
1968                navigator.clipboard.writeText(url).then(function () {
1969                    showToast(precFrontend.i18n && precFrontend.i18n.copied ? precFrontend.i18n.copied 
1969: 'Link kopiert!');
1970                });
1971            } else if (url) {
1972                // Fallback
1973                var $t = $('<input>').val(url).appendTo('body').select();
1974                document.execCommand('copy');
1975                $t.remove();
1976                showToast('Link kopiert!');
1977            }
1978        });
1979    }
1980
1981    // =========================================================
1982    // TOAST NOTIFICATION
1983    // =========================================================
1984    function showToast(message, type) {
1985        // Vorsorglich maskieren: Heute uebergeben alle Aufrufer feste Texte.
1986        // Kaeme spaeter ein dynamischer Wert dazu, waere die Luecke sonst
1987        // sofort da, ohne dass es jemand bemerkt.
1988        message = precText(message);
1989        var $toast = $('<div class="prec-toast prec-toast-' + (type || 'success') + '">' + message + '</div>');
1990        $('body').append($toast);
1991        setTimeout(function () { $toast.addClass('prec-toast--visible'); }, 10);
1992        setTimeout(function () {
1993            $toast.removeClass('prec-toast--visible');
1994            setTimeout(function () { $toast.remove(); }, 300);
1995        }, 3000);
1996    }
1997
1998    // =========================================================
1999    // SCROLL-TO-TOP nach Formular-Submit
2000    // =========================================================
2001    function initFormFeedback() {
2002        $(document).on('submit', '.prec-reg-form', function (e) {
2003            e.preventDefault();
2004            var $form = $(this);
2005            var $btn  = $form.find('[type=submit]');
2006            var $msg  = $form.find('.prec-form-message');
2007
2008            $btn.prop('disabled', true).text($btn.data('loading') || 'Wird gesendet…');
2009            $msg.text('').removeClass('prec-msg-success prec-msg-error');
2010
2011            $.post(precFrontend.ajaxUrl, $form.serialize(), function (res) {
2012                $btn.prop('disabled', false).text($btn.data('original') || $btn.attr('data-original') || 'Jetzt anmelden');
2013                if (res.success) {
2014                    $msg.addClass('prec-msg-success').text(res.data.message || 'Anmeldung erfolgreich!');
2015                    $form.find('input:not([type=hidden]), textarea, select').val('');
2016                    $('html,body').animate({ scrollTop: $msg.offset().top - 60 }, 400);
2017                } else {
2018                    $msg.addClass('prec-msg-error').text((res.data && res.data.message) || 'Fehler. Bitte versuche es erneut.');
2019                }
2020            }).fail(function () {
2021                $btn.prop('disabled', false);
2022                $msg.addClass('prec-msg-error').text('Verbindungsfehler. Bitte erneut versuchen.');
2023            });
2024        });
2025
2026        // Store original button text
2027        $(document).on('mouseenter', '.prec-reg-form [type=submit]', function () {
2028            if (!$(this).attr('data-original')) $(this).attr('data-original', $(this).text());
2029        });
2030    }
2031
2032    // Init all
2033    initAjaxPagination();
2034    initLiveFilter();
2035    initMasonry();
2036    initCopyShare();
2037
2038    // ── Spalten-Picker + Layout-Switcher: Responsive-Einstellungen anwenden ──
2039    (function applyInitialCols() {
2040        var vw     = window.innerWidth || document.documentElement.clientWidth;
2041        var resp   = (typeof precFrontend !== 'undefined' && precFrontend.responsive) ? precFrontend.responsive : null;
2042        // Gerät + Orientierung bestimmen
2043        var isPortrait = window.matchMedia ? window.matchMedia('(orientation: portrait)').matches
2044                       : (window.innerHeight > window.innerWidth);
2045        var dev;
2046        if      (vw <= 480)  dev = 'mobile_port';
2047        else if (vw <= 600)  dev = isPortrait ? 'mobile_port' : 'mobile_land';
2048        else if (vw <= 900)  dev = isPortrait ? 'tablet_port' : 'mobile_land';
2049        else if (vw <= 1024) dev = isPortrait ? 'tablet_port' : 'tablet_land';
2050        else                 dev = 'desktop';
2051        var devCfg = resp ? resp[dev] : null;
2052
2053        // Layout-Switcher: nur erlaubte Layouts je Gerät anzeigen
2054        if (devCfg && devCfg.layouts && devCfg.layouts.length) {
2055            $('#prec-layout-switcher option').each(function(){
2056                if (devCfg.layouts.indexOf($(this).val()) === -1) $(this).remove();
2057            });
2058        }
2059
2060        // Spalten für alle Shortcode-Wraps setzen
2061        $('.prec-shortcode-wrap').each(function () {
2062            var $outer   = $(this);
2063            var $picker  = $outer.find('#prec-cols-picker');
2064            if (!$picker.length) return;
2065
2066            // Aktuelles Layout ermitteln
2067            var $layoutWrap = $outer.find('div[class*="prec-layout-"]').first();
2068            var currentLayout = 'grid';
2069            if ($layoutWrap.length) {
2070                var cls = $layoutWrap.attr('class') || '';
2071                var m   = cls.match(/prec-layout-([\w-]+)/);
2072                if (m) currentLayout = m[1];
2073            }
2074
2075            // Spalten aus layoutCols holen, Fallback auf defaultCols
2076            var layoutCols = devCfg && devCfg.layoutCols ? devCfg.layoutCols : null;
2077            var cols = layoutCols && layoutCols[currentLayout]
2078                ? parseInt(layoutCols[currentLayout])
2079                : (devCfg ? (parseInt(devCfg.defaultCols) || 1) : (vw <= 480 ? 1 : vw <= 1023 ? 2 : 3));
2080
2081            // Harte Limits
2082            if      (vw <= 480)  cols = Math.min(cols, 1);
2083            else if (vw <= 1023) cols = Math.min(cols, 2);
2084
2085            $picker.val(String(cols));
2086
2087            if ($layoutWrap.length) {
2088                var gridVal = 'repeat(' + cols + ', 1fr)';
2089                $layoutWrap[0].style.setProperty('--prec-grid-cols', String(cols));
2090                $layoutWrap.find('.prec-cc-grid,.prec-ic2-grid,.prec-ig-grid,.prec-cl-list,.prec-events-grid').each(function () {
2091                    this.style.setProperty('--prec-grid-cols', String(cols));
2092                    this.style.gridTemplateColumns = gridVal;
2093                });
2094            }
2095        });
2096
2097        // Resize → neu berechnen
2098        if (!applyInitialCols._bound) {
2099            applyInitialCols._bound = true;
2100            var rt;
2101            window.addEventListener('resize', function () {
2102                clearTimeout(rt);
2103                rt = setTimeout(applyInitialCols, 250);
2104            });
2105        }
2106    })();
2107    // Lightbox wird über PREC.gallery.init() gestartet
2108    initFormFeedback();
2109
2110    // Preis-Text aufklappen/zuklappen
2111    $(document).on('click', '.prec-price-toggle', function(e) {
2112        e.preventDefault();
2113        e.stopPropagation();
2114        var $btn   = $(this);
2115        var $price = $btn.siblings('.prec-price-truncated');
2116        var expanded = $price.hasClass('prec-price-expanded');
2117        $price.toggleClass('prec-price-expanded');
2118        $btn.text(expanded ? '+ Mehr' : '− Weniger');
2119        $btn.attr('aria-expanded', !expanded);
2120    });
2121
2122})(jQuery);
2123
2124/* =========================================================
2125   LIGHTBOX + TOAST CSS (inline für Zero-Dependency)
2126   ========================================================= */
2127(function () {
2128    var css = `
2129    /* ── Lightbox Container ── */
2130    .prec-lb {
2131        position:fixed; inset:0; z-index:100000;
2132        display:flex; align-items:center; justify-content:center;
2133        opacity:0; visibility:hidden;
2134        /* pointer-events:none ist entscheidend: Ohne dies blieb die
2135           geschlossene (unsichtbare) Lightbox als Klickfalle ueber der
2136           ganzen Seite liegen - Links darunter waren tot (bis v10.2.6). */
2137        pointer-events:none !important;
2138        transition: opacity .35s ease, visibility .35s ease;
2139    }
2140    .prec-lb.prec-lb-visible {
2141        opacity:1; visibility:visible;
2142        pointer-events:auto !important;
2143    }
2144
2145    /* ── Backdrop: Blur + dunkle Transparenz ── */
2146    .prec-lb-backdrop {
2147        position:absolute; inset:0;
2148        background: rgba(10,10,10,.75);
2149        backdrop-filter: blur(18px) saturate(120%);
2150        -webkit-backdrop-filter: blur(18px) saturate(120%);
2151        cursor:pointer;
2152    }
2153
2154    /* ── Bühne (Bild-Container) ── */
2155    .prec-lb-stage {
2156        position:relative; z-index:2;
2157        display:flex; align-items:center; justify-content:center;
2158        max-width:92vw; max-height:85vh;
2159    }
2160
2161    /* ── Bild ── */
2162    .prec-lb-img {
2163        max-width:92vw; max-height:85vh;
2164        border-radius:6px;
2165        box-shadow: 0 25px 80px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.08);
2166        object-fit:contain;
2167        transition: opacity .25s ease, transform .25s ease;
2168        user-select:none; -webkit-user-drag:none;
2169    }
2170    .prec-lb-img-fade {
2171        opacity:0; transform:scale(.97);
2172    }
2173
2174    /* ── Spinner ── */
2175    .prec-lb-spinner {
2176        position:absolute; width:36px; height:36px;
2177        border:3px solid rgba(255,255,255,.15);
2178        border-top-color:rgba(255,255,255,.8);
2179        border-radius:50%;
2180        animation: precSpin .7s linear infinite;
2181        opacity:0; transition:opacity .2s;
2182    }
2183    .prec-lb-loading .prec-lb-spinner { opacity:1; }
2184    @keyframes precSpin { to { transform:rotate(360deg); } }
2185
2186    /* ── Close-Button ── */
2187    /* !important + feste SVG-Strichfarbe: Themes ueberschreiben gern
2188       button-Hintergrund und -Farbe (z.B. CD-Rot) - dann war das weisse
2189       Schliessen-X unsichtbar (rot auf rot). Seit v10.2.7 theme-fest. */
2190    .prec-lb-close {
2191        position:absolute; top:16px; right:20px; z-index:10;
2192        width:44px !important; height:44px !important;
2193        min-width:0 !important; min-height:0 !important;
2194        padding:0 !important; margin:0 !important;
2195        display:flex !important; align-items:center; justify-content:center;
2196        background:rgba(0,0,0,.45) !important;
2197        backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
2198        border:1px solid rgba(255,255,255,.35) !important;
2199        border-radius:50% !important; color:#fff !important; cursor:pointer;
2200        box-shadow:none !important; text-indent:0 !important;
2201        transition: background .2s, transform .15s;
2202        -webkit-appearance:none; appearance:none;
2203    }
2204    .prec-lb-close:hover {
2205        background:rgba(0,0,0,.65) !important; transform:scale(1.08);
2206    }
2207    .prec-lb-close svg, .prec-lb-nav svg {
2208        display:block !important; width:24px; height:24px;
2209        stroke:#fff !important; fill:none !important; opacity:1 !important;
2210    }
2211    .prec-lb-nav svg { width:28px; height:28px; }
2212
2213    /* ── Nav-Buttons ── */
2214    .prec-lb-nav {
2215        position:absolute; top:50%; z-index:10;
2216        transform:translateY(-50%);
2217        width:48px; height:48px;
2218        display:flex; align-items:center; justify-content:center;
2219        background:rgba(255,255,255,.08);
2220        backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
2221        border:1px solid rgba(255,255,255,.12);
2222        border-radius:50%; color:#fff; cursor:pointer;
2223        transition: background .2s, transform .15s, opacity .2s;
2224        opacity:.7;
2225    }
2226    .prec-lb-nav:hover { background:rgba(255,255,255,.18); opacity:1; transform:translateY(-50%) scale(1.08); }
2227    .prec-lb-prev { left:20px; }
2228    .prec-lb-next { right:20px; }
2229
2230    /* ── Footer / Counter ── */
2231    .prec-lb-footer {
2232        position:absolute; bottom:20px; left:50%; transform:translateX(-50%);
2233        z-index:10; display:flex; align-items:center; gap:10px; max-width:92vw;
2234    }
2235    /* Bildautor in der Lightbox (seit v10.3.1) */
2236    .prec-lb-caption:empty { display:none; }
2237    .prec-lb-caption {
2238        display:inline-block; padding:6px 14px;
2239        background:rgba(0,0,0,.45); border:1px solid rgba(255,255,255,.12);
2240        border-radius:20px; color:rgba(255,255,255,.9);
2241        font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
2242    }
2243    .prec-lb-counter {
2244        display:inline-block;
2245        padding:6px 16px;
2246        background:rgba(255,255,255,.1);
2247        backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
2248        border:1px solid rgba(255,255,255,.12);
2249        border-radius:20px;
2250        color:rgba(255,255,255,.85);
2251        font-size:13px; font-weight:500; letter-spacing:.5px;
2252        user-select:none;
2253    }
2254
2255    /* ── Responsive ── */
2256    @media (max-width:768px) {
2257        .prec-lb-nav { width:40px; height:40px; }
2258        .prec-lb-prev { left:10px; }
2259        .prec-lb-next { right:10px; }
2260        .prec-lb-close { top:10px; right:12px; width:38px; height:38px; }
2261        .prec-lb-img { max-width:96vw; max-height:80vh; border-radius:4px; }
2262    }
2263
2264    /* ── Toast (unverändert) ── */
2265    .prec-toast {
2266        position:fixed; bottom:24px; left:50%; transform:translateX(-50%) translateY(20px);
2267        background:#1a1a1a; color:#fff; padding:12px 24px; border-radius:8px;
2268        font-size:14px; font-weight:500; z-index:99998; opacity:0;
2269        transition:opacity .3s, transform .3s; pointer-events:none; white-space:nowrap;
2270    }
2271    .prec-toast--visible { opacity:1; transform:translateX(-50%) translateY(0); }
2272    .prec-toast-error { background:#d63638; }
2273    .prec-toast-success { background:#00a32a; }
2274    .prec-loading { opacity:.5; pointer-events:none; transition:opacity .2s; }
2275    .prec-msg-success { color:#00a32a; font-weight:600; margin-top:10px; display:block; }
2276    .prec-msg-error   { color:#d63638; font-weight:600; margin-top:10px; display:block; }
2277    `;
2278    var style = document.createElement('style');
2279    style.textContent = css;
2280    document.head.appendChild(style);
2281})();
2282
2283/* ── Ganze Event-Karte anklickbar (alle Layouts) ── */
2284(function(){
2285    var selectors = [
2286        '.prec-event-card[data-href]',
2287        '.prec-hcard[data-href]',
2288        '.prec-mcard[data-href]',
2289        '.prec-timeline-event[data-href]',
2290        '.prec-sb-item[data-href]'
2291    ].join(',');
2292
2293    document.addEventListener('click', function(e) {
2294        // Nicht wenn auf echten Link/Button geklickt
2295        if (e.target.closest('a, button, input, select, textarea')) return;
2296        var card = e.target.closest(selectors);
2297        if (!card) return;
2298        window.location.href = card.dataset.href;
2299    });
2300})();
2301
2302/* ── Date-Picker: Klick überall im Feld öffnet Kalender ── */
2303(function(){
2304    document.addEventListener('click', function(e) {
2305        var input = e.target.closest('input[type="date"]');
2306        if (!input) return;
2307        if (typeof input.showPicker === 'function') {
2308            try { input.showPicker(); } catch(ex) {}
2309        }
2310    });
2311})();
2312
2313/* =====================================================================
2314   PREC EVENT-SLIDER (seit v5.22.15)
2315   Initialisiert alle .prec-slider-Elemente auf der Seite mit:
2316   - Prev/Next-Navigation
2317   - Dots-Indikator (klickbar)
2318   - Optional Autoplay (data-autoplay="1", data-interval="5000")
2319   - Touch-Swipe-Support für Mobile
2320   - Responsive: 3/2/1 Cards je nach Viewport
2321   ===================================================================== */
2322(function () {
2323    function visibleCount() {
2324        var w = window.innerWidth || document.documentElement.clientWidth;
2325        if (w <= 680) return 1;
2326        if (w <= 1024) return 2;
2327        return 3;
2328    }
2329
2330    function initSlider(root) {
2331        if (root.dataset.precSliderInit === '1') return; // Idempotenz
2332        root.dataset.precSliderInit = '1';
2333
2334        var track = root.querySelector('.prec-slider-track');
2335        var prev  = root.querySelector('.prec-slider-prev');
2336        var next  = root.querySelector('.prec-slider-next');
2337        var dots  = root.querySelector('.prec-slider-dots');
2338        if (!track) return;
2339
2340        var slides = Array.prototype.slice.call(track.children);
2341        var total  = slides.length;
2342        if (total === 0) return;
2343
2344        // Container-Position absichern (für absolute Pfeil-Buttons)
2345        if (getComputedStyle(root).position === 'static') {
2346            root.style.position = 'relative';
2347        }
2348
2349        // Card-Breiten als Inline-Style setzen — robust gegen
2350        // konkurrierende CSS-Regeln und Cache-Probleme. (seit v5.22.16)
2351        function applyCardWidths() {
2352            var pv = visibleCount();
2353            var gap = pv === 1 ? 16 : 20;
2354            // Track-Gap synchronisieren
2355            track.style.gap = gap + 'px';
2356            track.style.display = 'flex';
2357            track.style.flexWrap = 'nowrap';
2358            track.style.alignItems = 'stretch';
2359            // Card-Breite berechnen: ((100% - (n-1)*gap) / n)
2360            for (var i = 0; i < slides.length; i++) {
2361                var s = slides[i];
2362                s.style.flex = '0 0 calc((100% - ' + ((pv - 1) * gap) + 'px) / ' + pv + ')';
2363                s.style.maxWidth = 'calc((100% - ' + ((pv - 1) * gap) + 'px) / ' + pv + ')';
2364                s.style.minWidth = '0';
2365                s.style.boxSizing = 'border-box';
2366            }
2367        }
2368        applyCardWidths();
2369
2370        var index = 0;
2371        var perView = visibleCount();
2372        var maxIndex = Math.max(0, total - perView);
2373
2374        function goTo(i, animate) {
2375            index = Math.max(0, Math.min(i, maxIndex));
2376            if (!slides[index]) return;
2377            // Position des aktuellen Slides relativ zum Track-Anfang
2378            var offset = slides[index].offsetLeft;
2379            track.style.transition = animate === false ? 'none' : '';
2380            track.style.transform = 'translateX(-' + offset + 'px)';
2381            updateDots();
2382            updateButtons();
2383        }
2384
2385        function updateButtons() {
2386            if (prev) prev.disabled = index <= 0;
2387            if (next) next.disabled = index >= maxIndex;
2388        }
2389
2390        function buildDots() {
2391            if (!dots) return;
2392            dots.innerHTML = '';
2393            var pages = maxIndex + 1;
2394            for (var i = 0; i < pages; i++) {
2395                var b = document.createElement('button');
2396                b.type = 'button';
2397                b.dataset.idx = i;
2398                b.setAttribute('aria-label', 'Zu Slide ' + (i + 1));
2399                if (i === 0) b.className = 'is-active';
2400                b.addEventListener('click', function () {
2401                    goTo(parseInt(this.dataset.idx, 10), true);
2402                    resetAutoplay();
2403                });
2404                dots.appendChild(b);
2405            }
2406        }
2407
2408        function updateDots() {
2409            if (!dots) return;
2410            var children = dots.children;
2411            for (var i = 0; i < children.length; i++) {
2412                children[i].classList.toggle('is-active', i === index);
2413            }
2414        }
2415
2416        // Autoplay
2417        var autoplay = root.dataset.autoplay === '1';
2418        var interval = parseInt(root.dataset.interval, 10) || 5000;
2419        var timer = null;
2420        function startAutoplay() {
2421            if (!autoplay || maxIndex === 0) return;
2422            stopAutoplay();
2423            timer = setInterval(function () {
2424                goTo(index >= maxIndex ? 0 : index + 1, true);
2425            }, interval);
2426        }
2427        function stopAutoplay() {
2428            if (timer) { clearInterval(timer); timer = null; }
2429        }
2430        function resetAutoplay() { stopAutoplay(); startAutoplay(); }
2431
2432        // Pfeil-Buttons
2433        if (prev) prev.addEventListener('click', function () {
2434            goTo(index - 1, true); resetAutoplay();
2435        });
2436        if (next) next.addEventListener('click', function () {
2437            goTo(index + 1, true); resetAutoplay();
2438        });
2439
2440        // Touch-Swipe für Mobile
2441        var touchStartX = 0;
2442        var touchEndX = 0;
2443        track.addEventListener('touchstart', function (e) {
2444            touchStartX = e.changedTouches[0].screenX;
2445            stopAutoplay();
2446        }, { passive: true });
2447        track.addEventListener('touchend', function (e) {
2448            touchEndX = e.changedTouches[0].screenX;
2449            var diff = touchEndX - touchStartX;
2450            if (Math.abs(diff) > 40) {
2451                goTo(index + (diff < 0 ? 1 : -1), true);
2452            }
2453            startAutoplay();
2454        }, { passive: true });
2455
2456        // Hover stoppt Autoplay (Desktop)
2457        root.addEventListener('mouseenter', stopAutoplay);
2458        root.addEventListener('mouseleave', startAutoplay);
2459
2460        // Resize: perView neu berechnen + Card-Breiten neu setzen
2461        var resizeTimeout = null;
2462        window.addEventListener('resize', function () {
2463            clearTimeout(resizeTimeout);
2464            resizeTimeout = setTimeout(function () {
2465                var newPerView = visibleCount();
2466                applyCardWidths(); // Inline-Breiten neu berechnen
2467                if (newPerView !== perView) {
2468                    perView = newPerView;
2469                    maxIndex = Math.max(0, total - perView);
2470                    buildDots();
2471                    if (index > maxIndex) index = maxIndex;
2472                    goTo(index, false);
2473                } else {
2474                    goTo(index, false); // bei reiner Größenänderung Position justieren
2475                }
2476            }, 150);
2477        });
2478
2479        // Init
2480        buildDots();
2481        goTo(0, false);
2482        startAutoplay();
2483    }
2484
2485    function initAll() {
2486        var sliders = document.querySelectorAll('.prec-slider:not([data-prec-slider-init="1"])');
2487        for (var i = 0; i < sliders.length; i++) {
2488            initSlider(sliders[i]);
2489        }
2490    }
2491
2492    if (document.readyState === 'loading') {
2493        document.addEventListener('DOMContentLoaded', initAll);
2494    } else {
2495        initAll();
2496    }
2497
2498    // Falls der Slider via AJAX nachgeladen wird (Filter/Pagination):
2499    // erneut initialisieren — Idempotenz-Flag verhindert Doppel-Init.
2500    if (typeof jQuery !== 'undefined') {
2501        jQuery(document).on('prec:events-loaded', initAll);
2502    }
2503})();

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.