PageSourceSearch

https://ottawawestfourrivers.ca/wp-content/plugins/owfr-news-hub/assets/owfr-news.js?ver=2.7.1

js ottawawestfourrivers.ca collected 2026-10-04 22:49:37 UTC 76,113 bytes, 1,463 lines download raw bytes

1/* ═══════════════════════════════════════════════════════
2   OWFR News Hub – JavaScript
3   Plugin: owfr-news-hub  v2.2
4   Front-end rendering + admin inline CRUD
5   ═══════════════════════════════════════════════════════ */
6document.addEventListener('DOMContentLoaded', function () {
7  'use strict';
8
9  /* ══════════════════════════════════════ */
10  /*  DISMISS LOADING OVERLAY              */
11  /* ══════════════════════════════════════ */
12  var _loader = document.getElementById('owfr-news-loading');
13  if (_loader) {
14    _loader.classList.add('owfr-news-loading-done');
15    setTimeout(function () { _loader.remove(); }, 400);
16  }
17
18  /* ══════════════════════════════════════ */
19  /*  READ-MORE TOGGLE (delegated)         */
20  /* ══════════════════════════════════════ */
21  function initReadMoreBtns(container) {
22    container.querySelectorAll('.owfr-news-read-more').forEach(function (btn) {
23      if (btn.dataset.rmInit) return;
24      btn.dataset.rmInit = '1';
25      var summary = btn.previousElementSibling;
26      if (!summary || !summary.classList.contains('owfr-news-card-summary')) return;
27      if (summary.scrollHeight > summary.clientHeight) {
28        btn.style.display = 'block';
29      }
30    });
31  }
32
33  document.addEventListener('click', function (e) {
34    var btn = e.target.closest('.owfr-news-read-more');
35    if (!btn) return;
36    var summary = btn.previousElementSibling;
37    if (!summary || !summary.classList.contains('owfr-news-card-summary')) return;
38    var expanded = btn.getAttribute('aria-expanded') === 'true';
39    summary.classList.toggle('is-expanded', !expanded);
40    btn.setAttribute('aria-expanded', String(!expanded));
41    btn.textContent = expanded ? 'Read more' : 'Read less';
42  });
43
44  /* ══════════════════════════════════════ */
45  /*  AUTO-ACTIVATE FIRST TAB ON LOAD      */
46  /* ══════════════════════════════════════ */
47  (function () {
48    var firstTab = document.querySelector('#owfr-news-tabs .owfr-news-tab-section');
49    if (!firstTab) return;
50    var firstSection = firstTab.dataset.section;
51    firstTab.classList.add('active');
52    var firstPanel = document.getElementById('owfr-news-panel-' + firstSection);
53    if (firstPanel) {
54      firstPanel.classList.add('active');
55      requestAnimationFrame(function () { initReadMoreBtns(firstPanel); });
56    }
57  }());
58
59  /* ══════════════════════════════════════ */
60  /*  TAB NAVIGATION                       */
61  /* ══════════════════════════════════════ */
62  var _tabList = document.getElementById('owfr-news-tabs');
63  if (_tabList) {
64    _tabList.addEventListener('click', function (e) {
65      var tab = e.target.closest('.owfr-news-tab');
66      if (!tab || tab.classList.contains('owfr-news-admin-add-section')) return;
67      var section = tab.dataset.section;
68      var panelId = 'owfr-news-panel-' + section;
69
70      _tabList.querySelectorAll('.owfr-news-tab').forEach(function (t) { t.classList.remove('active'); });
71      document.querySelectorAll('.owfr-news-panel').forEach(function (p) { p.classList.remove('active'); });
72
73      tab.classList.add('active');
74      var target = document.getElementById(panelId);
75      if (target) {
76        target.classList.add('active');
77        requestAnimationFrame(function () { initReadMoreBtns(target); });
78      }
79
80      /* scroll tab into view */
81      var linkLeft = tab.offsetLeft - _tabList.offsetLeft;
82      var linkCenter = linkLeft - (_tabList.clientWidth / 2) + (tab.offsetWidth / 2);
83      _tabList.scrollTo({ left: linkCenter, behavior: 'smooth' });
84
85      /* reset search when switching tabs */
86      var searchInput = document.getElementById('owfr-news-search');
87      if (searchInput && searchInput.value) {
88        filterCards(searchInput.value);
89      }
90    });
91  }
92
93  /* ══════════════════════════════════════ */
94  /*  SEARCH / FILTER                      */
95  /* ══════════════════════════════════════ */
96  var searchInput = document.getElementById('owfr-news-search');
97  var searchCount = document.getElementById('owfr-news-search-count');
98  if (searchInput) {
99    var debounceTimer;
100    searchInput.addEventListener('input', function () {
101      clearTimeout(debounceTimer);
102      debounceTimer = setTimeout(function () { filterCards(searchInput.value); }, 200);
103    });
104  }
105
106  function filterCards(query) {
107    var q = (query || '').toLowerCase().trim();
108    var activePanel = document.querySelector('.owfr-news-panel.active');
109    if (!activePanel) return;
110    var cards = activePanel.querySelectorAll('.owfr-news-card[data-item-id]');
111    var visible = 0;
112
113    cards.forEach(function (card) {
114      if (!q) {
115        card.classList.remove('owfr-news-hidden');
116        visible++;
117        return;
118      }
119      var text = card.textContent.toLowerCase();
120      var section = (card.dataset.section || '').toLowerCase();
121      if (text.indexOf(q) !== -1 || section.indexOf(q) !== -1) {
122        card.classList.remove('owfr-news-hidden');
123        visible++;
124      } else {
125        card.classList.add('owfr-news-hidden');
126      }
127    });
128
129    if (searchCount) {
130      searchCount.textContent = q ? (visible + ' result' + (visible !== 1 ? 's' : '')) : '';
131    }
132  }
133
134  /* ══════════════════════════════════════ */
135  /*  ADMIN GATE — exit early if not admin */
136  /* ══════════════════════════════════════ */
137  var isAdmin = document.body.classList.contains('admin-bar') || !!document.getElementById('wpadminbar');
138  if (!isAdmin) return;
139
140  /* ══════════════════════════════════════ */
141  /*  PARSE ADMIN CONFIG FROM wp_footer    */
142  /* ══════════════════════════════════════ */
143  function parseConfig(id) {
144    var el = document.getElementById(id);
145    if (!el) return null;
146    try { return JSON.parse(el.textContent); } catch (e) { return null; }
147  }
148
149  var config         = parseConfig('owfr-news-admin-config');
150  var orderConfig    = parseConfig('owfr-news-order-config');
151  var sectionsConfig = parseConfig('owfr-news-sections-config');
152  var settingsConfig = parseConfig('owfr-news-settings-config');
153
154  function esc(s) { return (s || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
155
156  /* ══════════════════════════════════════ */
157  /*  INJECT EDIT/DELETE BUTTONS ON CARDS  */
158  /* ══════════════════════════════════════ */
159  document.querySelectorAll('.owfr-news-card[data-item-id]').forEach(function (card) {
160    injectCardAdminButtons(card);
161  });
162
163  function injectCardAdminButtons(card) {
164    card.style.position = 'relative';
165
166    var editBtn = document.createElement('button');
167    editBtn.className = 'owfr-news-admin-edit-btn';
168    editBtn.type = 'button';
169    editBtn.textContent = '✎ Edit';
170    editBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); openItemEditor(card); });
171    card.appendChild(editBtn);
172
173    var delBtn = document.createElement('button');
174    delBtn.className = 'owfr-news-admin-delete-btn';
175    delBtn.type = 'button';
176    delBtn.textContent = '✕ Delete';
177    delBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); deleteItem(card); });
178    card.appendChild(delBtn);
179
180    injectDragHandle(card);
181  }
182
183  /* ══════════════════════════════════════ */
184  /*  INJECT "ADD CARD" IN EACH GRID       */
185  /* ══════════════════════════════════════ */
186  document.querySelectorAll('.owfr-news-panel').forEach(function (panel) {
187    var grid = panel.querySelector('.owfr-news-grid');
188    if (!grid) return;
189    var sectionId = panel.id.replace('owfr-news-panel-', '');
190    /* skip the "all" panel — items are added per section */
191    if (sectionId === 'all') return;
192    var addBtn = document.createElement('button');
193    addBtn.className = 'owfr-news-admin-add-card';
194    addBtn.type = 'button';
195    addBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span>Add New Item</span>';
196    addBtn.addEventListener('click', function () { openNewItemEditor(sectionId, grid); });
197    grid.appendChild(addBtn);
198  });
199
200  /* ══════════════════════════════════════ */
201  /*  HERO EDIT BUTTON                     */
202  /* ══════════════════════════════════════ */
203  var heroEl = document.querySelector('.owfr-news-hero[data-hero-id]');
204  if (heroEl) {
205    var heroEditBtn = document.createElement('button');
206    heroEditBtn.className = 'owfr-news-admin-hero-edit-btn';
207    heroEditBtn.type = 'button';
208    heroEditBtn.textContent = '✎ Edit Hero';
209    heroEditBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); openHeroEditor(heroEl); });
210    heroEl.appendChild(heroEditBtn);
211  }
212
213  /* ══════════════════════════════════════ */
214  /*  WELCOME BANNER EDIT BUTTON           */
215  /* ══════════════════════════════════════ */
216  var welcomeEl = document.getElementById('owfr-news-welcome');
217  if (welcomeEl) {
218    welcomeEl.style.position = 'relative';
219    var welcomeEditBtn = document.createElement('button');
220    welcomeEditBtn.className = 'owfr-news-admin-hero-edit-btn';
221    welcomeEditBtn.type = 'button';
222    welcomeEditBtn.textContent = '✎ Edit Banner';
223    welcomeEditBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); openWelcomeEditor(welcomeEl); });
224    welcomeEl.appendChild(welcomeEditBtn);
225  }
226
227  /* ══════════════════════════════════════ */
228  /*  SECTION HEADER EDIT BUTTONS          */
229  /* ══════════════════════════════════════ */
230  document.querySelectorAll('.owfr-news-section-header[data-section-slug]').forEach(function (hdr) {
231    hdr.style.position = 'relative';
232
233    var editBtn = document.createElement('button');
234    editBtn.className = 'owfr-news-admin-section-edit-btn';
235    editBtn.type = 'button';
236    editBtn.textContent = '✎ Edit';
237    editBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); openSectionEditor(hdr); });
238    hdr.appendChild(editBtn);
239
240    var delBtn = document.createElement('button');
241    delBtn.className = 'owfr-news-admin-section-delete-btn';
242    delBtn.type = 'button';
243    delBtn.textContent = '✕ Delete';
244    delBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); deleteSection(hdr.dataset.sectionSlug); });
245    hdr.appendChild(delBtn);
246  });
247
248  /* ══════════════════════════════════════ */
249  /*  ADD SECTION "+" BUTTON IN TAB BAR    */
250  /* ══════════════════════════════════════ */
251  (function () {
252    var tl = document.getElementById('owfr-news-tabs');
253    if (!tl) return;
254    var li = document.createElement('li');
255    li.className = 'owfr-news-add-section-li';
256    var btn = document.createElement('button');
257    btn.className = 'owfr-news-admin-add-section';
258    btn.type = 'button';
259    btn.title = 'Add New Section';
260    btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
261    btn.addEventListener('click', openNewSectionEditor);
262    li.appendChild(btn);
263    tl.appendChild(li);
264
265    /* Delete "×" on each section tab */
266    tl.querySelectorAll('.owfr-news-tab-section').forEach(function (tab) {
267      injectTabDeleteButton(tab);
268    });
269  })();
270
271  function injectTabDeleteButton(tabBtn) {
272    if (tabBtn.querySelector('.owfr-news-admin-delete-section-tab')) return;
273    var del = document.createElement('span');
274    del.className = 'owfr-news-admin-delete-section-tab';
275    del.title = 'Delete section';
276    del.innerHTML = '&times;';
277    del.addEventListener('click', function (e) {
278      e.preventDefault(); e.stopPropagation();
279      deleteSection(tabBtn.dataset.section);
280    });
281    tabBtn.appendChild(del);
282  }
283
284  /* ══════════════════════════════════════ */
285  /*  MODAL HELPERS                        */
286  /* ══════════════════════════════════════ */
287  function closeModal() {
288    var m = document.getElementById('owfr-news-edit-modal');
289    if (m) m.remove();
290    document.body.style.overflow = '';
291  }
292
293  function createModal(title, formHTML) {
294    closeModal();
295    var m = document.createElement('div');
296    m.id = 'owfr-news-edit-modal';
297    m.innerHTML = '<div class="owfr-news-edit-overlay"></div>'
298      + '<div class="owfr-news-edit-panel">'
299      + '<div class="owfr-news-edit-panel-header"><h3>' + esc(title) + '</h3><button class="owfr-news-edit-close" aria-label="Close">&times;</button></div>'
300      + '<form class="owfr-news-edit-form">'
301      + formHTML
302      + '<div class="owfr-news-edit-actions">'
303      + '<button type="submit" class="owfr-news-btn owfr-news-btn-primary" style="flex:1">Save</button>'
304      + '<button type="button" class="owfr-news-edit-cancel owfr-news-btn owfr-news-btn-secondary" style="flex:1">Cancel</button>'
305      + '</div>'
306      + '<div class="owfr-news-edit-status"></div>'
307      + '</form></div>';
308
309    document.body.appendChild(m);
310    document.body.style.overflow = 'hidden';
311
312    m.querySelector('.owfr-news-edit-close').addEventListener('click', closeModal);
313    m.querySelector('.owfr-news-edit-cancel').addEventListener('click', closeModal);
314
315    attachMediaPickers(m);
316    return m;
317  }
318
319  function fieldHTML(name, label, value, type, hint) {
320    var isFile = (type === 'url' && (name === 'image' || name === 'file_url' || name === 'bottom_image'));
321    var html = '<div class="owfr-news-edit-field"><label>' + label;
322    if (hint) html += ' <span class="owfr-news-label-hint">(' + hint + ')</span>';
323    html += '</label>';
324    if (isFile) {
325      html += '<div style="display:flex;gap:0.5rem;align-items:center">';
326      html += '<input type="url" name="' + name + '" value="' + esc(value) + '" placeholder="Paste URL or choose from Media Library" style="flex:1" />';
327      html += '<button type="button" class="owfr-news-btn owfr-news-btn-secondary owfr-news-media-pick" data-target="' + name + '" style="white-space:nowrap;padding:0.4rem 0.7rem;font-size:0.82rem">📁 Media</button>';
328      html += '</div>';
329      if (value) html += '<img class="owfr-news-img-preview-' + name + '" src="' + esc(value) + '" style="max-height:50px;margin-top:0.4rem;border-radius:4px;border:1px solid #eee" />';
330    } else if (type === 'textarea') {
331      html += '<textarea name="' + name + '" rows="3">' + esc(value) + '</textarea>';
332    } else if (type === 'select') {
333      /* value = JSON string of options array [{value, label}] */
334      /* actual selected value passed via hint overload – re-parse */
335      html += value; /* pre-built <select> HTML */
336    } else {
337      html += '<input type="' + type + '" name="' + name + '" value="' + esc(value) + '" />';
338    }
339    html += '</div>';
340    return html;
341  }
342
343  function buildSectionSelect(name, selectedSlug) {
344    var sections = document.querySelectorAll('.owfr-news-tab-section');
345    var html = '<select name="' + name + '">';
346    html += '<option value="">— Select section —</option>';
347    sections.forEach(function (tab) {
348      var slug = tab.dataset.section;
349      var label = tab.textContent.replace(/×$/, '').trim();
350      html += '<option value="' + esc(slug) + '"' + (slug === selectedSlug ? ' selected' : '') + '>' + esc(label) + '</option>';
351    });
352    html += '</select>';
353    return html;
354  }
355
356  /* ══════════════════════════════════════ */
357  /*  CARD LAYOUT HELPER                   */
358  /* ══════════════════════════════════════ */
359  function getCardLayout(card) {
360    if (card.classList.contains('owfr-news-card-wide')) return 'owfr-news-card-wide';
361    if (card.classList.contains('owfr-news-card-half')) return 'owfr-news-card-half';
362    return '';
363  }
364
365  function buildLayoutPicker(currentLayout) {
366    var html = '<div class="owfr-news-edit-field"><label>Card Size</label>';
367    html += '<div class="owfr-news-layout-picker">';
368    html += '<button type="button" class="owfr-news-layout-opt' + (currentLayout === '' ? ' selected' : '') + '" data-layout="">Standard</button>';
369    html += '<button type="button" class="owfr-news-layout-opt' + (currentLayout === 'owfr-news-card-half' ? ' selected' : '') + '" data-layout="owfr-news-card-half">Half Width</button>';
370    html += '<button type="button" class="owfr-news-layout-opt' + (currentLayout === 'owfr-news-card-wide' ? ' selected' : '') + '" data-layout="owfr-news-card-wide">Full Width</button>';
371    html += '</div><input type="hidden" name="layout" value="' + esc(currentLayout) + '" /></div>';
372    return html;
373  }
374
375  function initLayoutPicker(modal) {
376    modal.querySelectorAll('.owfr-news-layout-opt').forEach(function (opt) {
377      opt.addEventListener('click', function () {
378        modal.querySelectorAll('.owfr-news-layout-opt').forEach(function (o) { o.classList.remove('selected'); });
379        this.classList.add('selected');
380        modal.querySelector('[name="layout"]').value = this.dataset.layout;
381      });
382    });
383  }
384
385  /* ══════════════════════════════════════ */
386  /*  FILES REPEATER (EN / FR tabs)        */
387  /* ══════════════════════════════════════ */
388  function buildFilesRepeater(filesEn, filesFr) {
389    var html = '<div class="owfr-news-edit-field">';
390    html += '<label>Attached Files / Documents <span class="owfr-news-label-hint">(by language)</span></label>';
391    html += '<div class="owfr-news-files-tab-bar">';
392    html += '<button type="button" class="owfr-news-files-tab active" data-files-tab="en">🇬🇧 English</button>';
393    html += '<button type="button" class="owfr-news-files-tab" data-files-tab="fr">🇫🇷 Français</button>';
394    html += '</div>';
395    /* EN panel */
396    html += '<div class="owfr-news-files-panel active" data-files-panel="en">';
397    html += '<div class="owfr-news-files-list" data-lang="en">';
398    if (filesEn && filesEn.length) {
399      filesEn.forEach(function (fi, i) { html += fileRowHTML(fi.url || '', fi.label || '', i); });
400    }
401    html += '</div>';
402    html += '<button type="button" class="owfr-news-btn owfr-news-btn-secondary owfr-news-add-file-btn" data-lang="en" style="margin-top:0.5rem;padding:0.45rem 1rem;font-size:0.82rem">+ Add English File</button>';
403    html += '</div>';
404    /* FR panel */
405    html += '<div class="owfr-news-files-panel" data-files-panel="fr">';
406    html += '<div class="owfr-news-files-list" data-lang="fr">';
407    if (filesFr && filesFr.length) {
408      filesFr.forEach(function (fi, i) { html += fileRowHTML(fi.url || '', fi.label || '', i); });
409    }
410    html += '</div>';
411    html += '<button type="button" class="owfr-news-btn owfr-news-btn-secondary owfr-news-add-file-btn" data-lang="fr" style="margin-top:0.5rem;padding:0.45rem 1rem;font-size:0.82rem">+ Add French File</button>';
412    html += '</div>';
413    html += '</div>';
414    return html;
415  }
416
417  function fileRowHTML(url, label, idx) {
418    return '<div class="owfr-news-file-row" data-idx="' + idx + '" style="display:flex;gap:0.4rem;align-items:center;margin-bottom:0.4rem">'
419      + '<input type="text" class="owfr-news-file-row-label" value="' + esc(label) + '" placeholder="Label (e.g. Budget Report)" style="flex:1;min-width:0;padding:0.4rem 0.6rem;border:1px solid #ccc;border-radius:5px;font-size:0.85rem" />'
420      + '<input type="url" class="owfr-news-file-row-url" value="' + esc(url) + '" placeholder="File URL" style="flex:1.5;min-width:0;padding:0.4rem 0.6rem;border:1px solid #ccc;border-radius:5px;font-size:0.85rem" />'
421      + '<button type="button" class="owfr-news-btn owfr-news-btn-secondary owfr-news-file-row-media" style="white-space:nowrap;padding:0.35rem 0.55rem;
421font-size:0.78rem" title="Choose from Media Library">📁</button>'
422      + '<button type="button" class="owfr-news-file-row-remove" style="background:#c00;color:#fff;border:none;border-radius:5px;width:28px;height:28px;cursor:pointer;font-size:14px;line-height:1;flex-shrink:0" title="Remove">&times;</button>'
423      + '</div>';
424  }
425
426  function initFilesRepeater(modal) {
427    /* Tab switching */
428    modal.querySelectorAll('.owfr-news-files-tab').forEach(function (tab) {
429      tab.addEventListener('click', function () {
430        modal.querySelectorAll('.owfr-news-files-tab').forEach(function (t) { t.classList.remove('active'); });
431        modal.querySelectorAll('.owfr-news-files-panel').forEach(function (p) { p.classList.remove('active'); });
432        this.classList.add('active');
433        var panel = modal.querySelector('[data-files-panel="' + this.dataset.filesTab + '"]');
434        if (panel) panel.classList.add('active');
435      });
436    });
437
438    /* Add buttons for each language */
439    modal.querySelectorAll('.owfr-news-add-file-btn').forEach(function (addBtn) {
440      var lang = addBtn.dataset.lang;
441      var list = modal.querySelector('.owfr-news-files-list[data-lang="' + lang + '"]');
442      if (!list) return;
443      addBtn.addEventListener('click', function () {
444        var idx = list.querySelectorAll('.owfr-news-file-row').length;
445        var tmp = document.createElement('div');
446        tmp.innerHTML = fileRowHTML('', '', idx);
447        var row = tmp.firstChild;
448        list.appendChild(row);
449        attachFileRowEvents(row, modal);
450        row.querySelector('.owfr-news-file-row-label').focus();
451      });
452    });
453
454    modal.querySelectorAll('.owfr-news-file-row').forEach(function (row) {
455      attachFileRowEvents(row, modal);
456    });
457  }
458
459  function attachFileRowEvents(row, modal) {
460    row.querySelector('.owfr-news-file-row-remove').addEventListener('click', function () { row.remove(); });
461    var mediaBtn = row.querySelector('.owfr-news-file-row-media');
462    if (mediaBtn) {
463      mediaBtn.addEventListener('click', function () {
464        if (typeof wp === 'undefined' || !wp.media) { alert('WordPress Media Library not available.'); return; }
465        var frame = wp.media({ title: 'Choose File', button: { text: 'Use this file' }, multiple: false });
466        frame.on('select', function () {
467          var att = frame.state().get('selection').first().toJSON();
468          row.querySelector('.owfr-news-file-row-url').value = att.url;
469          if (!row.querySelector('.owfr-news-file-row-label').value) {
470            row.querySelector('.owfr-news-file-row-label').value = att.title || att.filename || '';
471          }
472        });
473        frame.open();
474      });
475    }
476  }
477
478  /* ══════════════════════════════════════ */
479  /*  PARTNER LOGOS REPEATER               */
480  /* ══════════════════════════════════════ */
481  function frontLogosRepeaterHTML(logos) {
482    var html = '<div class="owfr-news-edit-field">';
483    html += '<label>Partner Logos <span class="owfr-news-label-hint">(display multiple logos side-by-side at top of card — overrides single image)</span></label>';
484    html += '<div class="owfr-news-logos-list">';
485    if (logos && logos.length) {
486      logos.forEach(function (url) { html += frontLogoRowHTML(url); });
487    }
488    html += '</div>';
489    html += '<button type="button" class="owfr-news-btn owfr-news-btn-secondary owfr-news-add-logo-btn" style="margin-top:0.5rem;padding:0.45rem 1rem;font-size:0.82rem">+ Add Logo</button>';
490    html += '</div>';
491    return html;
492  }
493
494  function frontLogoRowHTML(url) {
495    return '<div class="owfr-news-logo-row" style="display:flex;gap:0.4rem;align-items:center;margin-bottom:0.4rem">'
496      + '<input type="url" name="logos[]" value="' + esc(url || '') + '" placeholder="Logo image URL" style="flex:1;min-width:0;padding:0.4rem 0.6rem;border:1px solid #ccc;border-radius:5px;font-size:0.85rem" />'
497      + '<button type="button" class="owfr-news-btn owfr-news-btn-secondary owfr-news-logo-media-btn" style="white-space:nowrap;padding:0.35rem 0.55rem;
497font-size:0.78rem" title="Choose from Media Library">&#128193;</button>'
498      + '<button type="button" class="owfr-news-logo-row-remove" style="background:#c00;color:#fff;border:none;border-radius:5px;width:28px;height:28px;cursor:pointer;font-size:14px;line-height:1;flex-shrink:0" title="Remove">&times;</button>'
499      + '</div>';
500  }
501
502  function initFrontLogosRepeater(modal) {
503    modal.querySelectorAll('.owfr-news-logo-row').forEach(function (row) {
504      attachFrontLogoRowEvents(row);
505    });
506    var addBtn = modal.querySelector('.owfr-news-add-logo-btn');
507    if (addBtn) {
508      addBtn.addEventListener('click', function () {
509        var list = modal.querySelector('.owfr-news-logos-list');
510        var tmp = document.createElement('div');
511        tmp.innerHTML = frontLogoRowHTML('');
512        var row = tmp.firstChild;
513        list.appendChild(row);
514        attachFrontLogoRowEvents(row);
515        row.querySelector('[name="logos[]"]').focus();
516      });
517    }
518  }
519
520  function attachFrontLogoRowEvents(row) {
521    var removeBtn = row.querySelector('.owfr-news-logo-row-remove');
522    if (removeBtn) removeBtn.addEventListener('click', function () { row.remove(); });
523    var mediaBtn = row.querySelector('.owfr-news-logo-media-btn');
524    if (mediaBtn) {
525      mediaBtn.addEventListener('click', function () {
526        if (typeof wp === 'undefined' || !wp.media) { alert('WordPress Media Library not available.'); return; }
527        var input = row.querySelector('[name="logos[]"]');
528        var frame = wp.media({ title: 'Choose Logo', button: { text: 'Use this image' }, library: { type: 'image' }, multiple: false });
529        frame.on('select', function () { input.value = frame.state().get('selection').first().toJSON().url; });
530        frame.open();
531      });
532    }
533  }
534
535  /* ══════════════════════════════════════ */
536  /*  MEDIA LIBRARY PICKER                 */
537  /* ══════════════════════════════════════ */
538  function attachMediaPickers(modal) {
539    modal.querySelectorAll('.owfr-news-media-pick').forEach(function (btn) {
540      btn.addEventListener('click', function () {
541        if (typeof wp === 'undefined' || !wp.media) { alert('WordPress Media Library not available.'); return; }
542        var targetName = btn.dataset.target;
543        var fileType = (targetName === 'file_url') ? '' : 'image';
544        var frame = wp.media({
545          title: targetName === 'file_url' ? 'Choose File' : 'Choose Image',
546          button: { text: 'Use this file' },
547          multiple: false,
548          library: fileType ? { type: fileType } : {}
549        });
550        frame.on('select', function () {
551          var att = frame.state().get('selection').first().toJSON();
552          var input = modal.querySelector('[name="' + targetName + '"]');
553          if (input) input.value = att.url;
554          var preview = modal.querySelector('.owfr-news-img-preview-' + targetName);
555          if (preview) {
556            preview.src = att.url;
557          } else if (targetName === 'image') {
558            var img = document.createElement('img');
559            img.className = 'owfr-news-img-preview-' + targetName;
560            img.src = att.url;
561            img.style.cssText = 'max-height:50px;margin-top:0.4rem;border-radius:4px;border:1px solid #eee';
562            if (input) input.closest('.owfr-news-edit-field').appendChild(img);
563          }
564        });
565        frame.open();
566      });
567    });
568  }
569
570  /* ══════════════════════════════════════ */
571  /*  MODAL: EDIT ITEM                     */
572  /* ══════════════════════════════════════ */
573  function getCardData(card) {
574    var d = {};
575    var el;
576    el = card.querySelector('.owfr-news-card-image'); if (el) d.image = el.getAttribute('src');
577    el = card.querySelector('h3'); if (el) d.title = el.textContent;
578    el = card.querySelector('.owfr-news-card-summary'); if (el) d.summary = el.textContent.trim();
579    d.section = card.dataset.section || '';
580    d.featured = card.classList.contains('owfr-news-card-featured');
581    el = card.querySelector('.owfr-news-date'); if (el) d.event_date = el.textContent.trim();
582    el = card.querySelector('.owfr-news-source'); if (el) d.source_label = el.textContent.trim();
583    el = card.querySelector('.owfr-news-lang'); if (el) d.language = el.textContent.trim();
584    /* extract link URLs */
585    el = card.querySelector('.owfr-news-btn-primary'); if (el) { d.external_link = el.getAttribute('href'); d.button_text = el.textContent.trim(); }
586    el = card.querySelector('.owfr-news-btn-secondary'); if (el) d.file_url = el.getAttribute('href');
587    /* extract bottom image */
588    el = card.querySelector('.owfr-news-card-bottom-image'); if (el) d.bottom_image = el.getAttribute('src');
589    /* extract layout */
590    d._layout = getCardLayout(card);
591    /* extract EN/FR files */
592    d.files_en = []; d.files_fr = [];
593    card.querySelectorAll('.owfr-news-file-link[data-lang="en"]').forEach(function (a) {
594      d.files_en.push({ url: a.getAttribute('href'), label: a.textContent.trim() });
595    });
596    card.querySelectorAll('.owfr-news-file-link[data-lang="fr"]').forEach(function (a) {
597      d.files_fr.push({ url: a.getAttribute('href'), label: a.textContent.trim() });
598    });
599    /* extract partner logos */
600    d.logos = [];
601    card.querySelectorAll('.owfr-news-logo-img').forEach(function (img) {
602      d.logos.push(img.getAttribute('src'));
603    });
604    return d;
605  }
606
607  function openItemEditor(card) {
608    var itemId = card.dataset.itemId;
609    var cur = getCardData(card);
610
611    var f = '';
612    f += fieldHTML('image', 'Single Image URL (fallback)', cur.image || '', 'url');
613    f += frontLogosRepeaterHTML(cur.logos || []);
614    f += fieldHTML('title', 'Title', cur.title || '', 'text');
615    f += '<div class="owfr-news-edit-field"><label>Summary <span class="owfr-news-label-hint">(displayed on card)</span></label>'
616       + '<textarea name="summary" rows="3">' + esc(cur.summary || '') + '</textarea></div>';
617    f += '<div class="owfr-news-edit-field"><label>Section</label>' + buildSectionSelect('section', cur.section) + '</div>';
618    f += fieldHTML('event_date', 'Date', cur.event_date || '', 'date');
619    f += fieldHTML('source_label', 'Source Label', cur.source_label || '', 'text', 'e.g. OWFR, Ministry of Health');
620    f += fieldHTML('language', 'Language', cur.language || '', 'text', 'e.g. EN, FR, Bilingual');
621    f += fieldHTML('external_link', 'External Link URL', cur.external_link || '', 'url');
622    f += fieldHTML('button_text', 'Button Text', cur.button_text || 'Learn More', 'text');
623    f += fieldHTML('file_url', 'File / Document URL', cur.file_url || '', 'url');
624    f += buildLayoutPicker(cur._layout || '');
625    f += buildFilesRepeater(cur.files_en || [], cur.files_fr || []);
626    f += fieldHTML('bottom_image', 'Bottom Image URL', cur.bottom_image || '', 'url');
627    f += '<div class="owfr-news-edit-toggle">'
628       + '<input type="checkbox" name="featured" id="owfr-news-feat-chk"' + (cur.featured ? ' checked' : '') + ' />'
629       + '<label for="owfr-news-feat-chk">★ Featured Item</label></div>';
630
631    var m = createModal('Edit Item', f);
632    initLayoutPicker(m);
633    initFilesRepeater(m);
634    initFrontLogosRepeater(m);
635
636    m.querySelector('.owfr-news-edit-form').addEventListener('submit', function (e) {
637      e.preventDefault();
638      handleItemSave(m, itemId, card);
639    });
640  }
641
642  function openNewItemEditor(sectionSlug, grid) {
643    var itemId = 'news-' + Date.now();
644
645    var f = '';
646    f += fieldHTML('image', 'Single Image URL (fallback)', '', 'url');
647    f += frontLogosRepeaterHTML([]);
648    f += fieldHTML('title', 'Title', '', 'text');
649    f += '<div class="owfr-news-edit-field"><label>Summary</label><textarea name="summary" rows="3" placeholder="Brief description of this update"></textarea></div>';
650    f += '<div class="owfr-news-edit-field"><label>Section</label>' + buildSectionSelect('section', sectionSlug) + '</div>';
651    f += fieldHTML('event_date', 'Date', '', 'date');
652    f += fieldHTML('source_label', 'Source Label', '', 'text', 'e.g. OWFR, Ministry of Health');
653    f += fieldHTML('language', 'Language', '', 'text', 'e.g. EN, FR, Bilingual');
654    f += fieldHTML('external_link', 'External Link URL', '', 'url');
655    f += fieldHTML('button_text', 'Button Text', 'Learn More', 'text');
656    f += fieldHTML('file_url', 'File / Document URL', '', 'url');
657    f += buildLayoutPicker('');
658    f += buildFilesRepeater([], []);
659    f += fieldHTML('bottom_image', 'Bottom Image URL', '', 'url');
660    f += '<div class="owfr-news-edit-toggle">'
661       + '<input type="checkbox" name="featured" id="owfr-news-feat-chk" />'
662       + '<label for="owfr-news-feat-chk">★ Featured Item</label></div>';
663
664    var m = createModal('Add New Item', f);
665    initLayoutPicker(m);
666    initFilesRepeater(m);
667    initFrontLogosRepeater(m);
668
669    m.querySelector('.owfr-news-edit-form').addEventListener('submit', function (e) {
670      e.preventDefault();
671      handleNewItemSave(m, itemId, sectionSlug, grid);
672    });
673  }
674
675  /* ══════════════════════════════════════ */
676  /*  SAVE / DELETE ITEMS                  */
677  /* ══════════════════════════════════════ */
678  function gatherItemFormData(modal) {
679    var fd = new FormData(modal.querySelector('.owfr-news-edit-form'));
680    var data = {};
681    data.title         = fd.get('title') || '';
682    data.summary       = fd.get('summary') || '';
683    data.section       = fd.get('section') || '';
684    data.image         = fd.get('image') || '';
685    data.event_date    = fd.get('event_date') || '';
686    data.source_label  = fd.get('source_label') || '';
687    data.language      = fd.get('language') || '';
688    data.external_link = fd.get('external_link') || '';
689    data.button_text   = fd.get('button_text') || 'Learn More';
690    data.file_url      = fd.get('file_url') || '';
691    data.bottom_image  = fd.get('bottom_image') || '';
692    data.featured      = !!fd.get('featured');
693    data.status        = 'active';
694    /* Layout */
695    var layout = fd.get('layout');
696    data._layout = (layout !== null) ? layout : '';
697    /* Gather EN files */
698    data.files_en = [];
699    var enList = modal.querySelector('.owfr-news-files-list[data-lang="en"]');
700    if (enList) {
701      enList.querySelectorAll('.owfr-news-file-row').forEach(function (row) {
702        var url   = row.querySelector('.owfr-news-file-row-url').value.trim();
703        var label = row.querySelector('.owfr-news-file-row-label').value.trim();
704        if (url) data.files_en.push({ url: url, label: label });
705      });
706    }
707    /* Gather FR files */
708    data.files_fr = [];
709    var frList = modal.querySelector('.owfr-news-files-list[data-lang="fr"]');
710    if (frList) {
711      frList.querySelectorAll('.owfr-news-file-row').forEach(function (row) {
712        var url   = row.querySelector('.owfr-news-file-row-url').value.trim();
713        var label = row.querySelector('.owfr-news-file-row-label').value.trim();
714        if (url) data.files_fr.push({ url: url, label: label });
715      });
716    }
717    /* Gather partner logos */
718    data.logos = [];
719    modal.querySelectorAll('.owfr-news-logos-list [name="logos[]"]').forEach(function (inp) {
720      if (inp.value.trim()) data.logos.push(inp.value.trim());
721    });
722    return data;
723  }
724
725  function handleItemSave(modal, itemId, card) {
726    var status = modal.querySelector('.owfr-news-edit-status');
727    if (!config) { status.textContent = '⚠ Cannot save.'; status.style.color = '#c00'; return; }
728
729    var data = gatherItemFormData(modal);
730    if (!data.title) { status.textContent = 'Title is required.'; status.style.color = '#c00'; return; }
731
732    status.textContent = 'Saving…'; status.style.color = '#666';
733    fetch(config.restUrl, {
734      method: 'POST', credentials: 'same-origin',
735      headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce },
736      body: JSON.stringify({ itemId: itemId, data: data })
737    })
738    .then(function (r) { if (!r.ok) throw new Error(); return r.json(); })
739    .then(function () {
740      applyItemOverrides(card, data);
741      status.textContent = '✓ Saved!'; status.style.color = '#006B3F';
742      /* Also update matching card in the other panel */
743      syncDuplicateCards(itemId, data, card);
744      setTimeout(closeModal, 700);
745    })
746    .catch(function () { status.textContent = '✗ Save failed.'; status.style.color = '#c00'; });
747  }
748
749  function handleNewItemSave(modal, itemId, sectionSlug, grid) {
750    var status = modal.querySelector('.owfr-news-edit-status');
751    if (!config) { status.textContent = '⚠ Cannot save.'; status.style.color = '#c00'; return; }
752
753    var data = gatherItemFormData(modal);
754    if (!data.title) { status.textContent = 'Title is required.'; status.style.color = '#c00'; return; }
755
756    status.textContent = 'Saving…'; status.style.color = '#666';
757    fetch(config.restUrl, {
758      method: 'POST', credentials: 'same-origin',
759      headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce },
760      body: JSON.stringify({ itemId: itemId, data: data })
761    })
762    .then(function (r) { if (!r.ok) throw new Error(); return r.json(); })
763    .then(function () {
764      renderNewCard(itemId, data, grid);
765      status.textContent = '✓ Created!'; status.style.color = '#006B3F';
766      setTimeout(closeModal, 600);
767    })
768    .catch(function () { status.textContent = '✗ Save failed.'; status.style.color = '#c00'; });
769  }
770
771  function deleteItem(card) {
772    if (!confirm('Permanently delete this item? This cannot be undone.')) return;
773    var itemId = card.dataset.itemId;
774    if (config) {
775      fetch(config.restUrl + '/' + encodeURIComponent(itemId), {
776        method: 'DELETE', credentials: 'same-origin',
777        headers: { 'X-WP-Nonce': config.nonce }
778      }).catch(function () {});
779    }
780    /* Remove from all panels */
781    document.querySelectorAll('.owfr-news-card[data-item-id="' + itemId + '"]').forEach(function (c) { c.remove(); });
782  }
783
784  /* ══════════════════════════════════════ */
785  /*  APPLY OVERRIDES TO EXISTING CARD     */
786  /* ══════════════════════════════════════ */
787  function applyItemOverrides(card, data) {
788    var el;
789    /* Partner logos take priority over single image */
790    var hasLogos = data.logos && data.logos.length > 0;
791    var existingLogosDiv = card.querySelector('.owfr-news-card-logos');
792    var existingImg = card.querySelector('.owfr-news-card-image');
793    if (hasLogos) {
794      /* Build / rebuild logos div */
795      var logosDiv = existingLogosDiv || document.createElement('div');
796      logosDiv.className = 'owfr-news-card-logos';
797      logosDiv.innerHTML = '';
798      data.logos.forEach(function (url) {
799        var li = document.createElement('img');
800        li.className = 'owfr-news-logo-img'; li.src = url; li.alt = ''; li.loading = 'lazy';
801        logosDiv.appendChild(li);
802      });
803      if (!existingLogosDiv) card.insertBefore(logosDiv, card.firstChild);
804      /* Remove single image if logos are present */
805      if (existingImg) existingImg.remove();
806    } else {
807      /* No logos — remove logos div if present, handle single image */
808      if (existingLogosDiv) existingLogosDiv.remove();
809      if (data.image) {
810        if (existingImg) { existingImg.src = data.image; }
811        else {
812          var img = document.createElement('img');
813          img.className = 'owfr-news-card-image'; img.src = data.image; img.alt = ''; img.loading = 'lazy';
814          card.insertBefore(img, card.firstChild);
815        }
816      }
817    }
818    el = card.querySelector('h3'); if (el && data.title) el.textContent = data.title;
819    el = card.querySelector('.owfr-news-card-summary');
820    if (el && data.summary !== undefined) el.textContent = data.summary;
821
822    /* featured */
823    if (data.featured) { card.classList.add('owfr-news-card-featured'); }
824    else { card.classList.remove('owfr-news-card-featured'); }
825
826    /* date */
827    el = card.queryS
827elector('.owfr-news-date');
828    if (data.event_date && el) el.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> ' + esc(data.event_date);
829
830    /* source */
831    el = card.querySelector('.owfr-news-source');
832    if (el && data.source_label !== undefined) el.textContent = data.source_label;
833
834    /* language */
835    el = card.querySelector('.owfr-news-lang');
836    if (el && data.language !== undefined) el.textContent = data.language;
837
838    /* buttons */
839    el = card.querySelector('.owfr-news-btn-primary');
840    if (el) { if (data.external_link) { el.href = data.external_link; el.textContent = data.button_text || 'Learn More'; } }
841    el = card.querySelector('.owfr-news-btn-secondary');
842    if (el) { if (data.file_url) el.href = data.file_url; }
843
844    /* multiple files — EN/FR groups */
845    var filesWrap = card.querySelector('.owfr-news-card-files');
846    var hasEn = data.files_en && data.files_en.length;
847    var hasFr = data.files_fr && data.files_fr.length;
848    if (hasEn || hasFr) {
849      if (!filesWrap) {
850        filesWrap = document.createElement('div');
851        filesWrap.className = 'owfr-news-card-files';
852        var actions = card.querySelector('.owfr-news-card-actions');
853        if (actions) actions.appendChild(filesWrap);
854      }
855      filesWrap.innerHTML = '';
856      if (hasEn) {
857        var enGroup = document.createElement('div');
858        enGroup.className = 'owfr-news-files-lang-group';
859        enGroup.innerHTML = '<div class="owfr-news-files-lang-header"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg> English</div>';
860        data.files_en.forEach(function (fi) {
861          if (!fi.url) return;
862          var a = document.createElement('a');
863          a.className = 'owfr-news-file-link'; a.href = fi.url; a.target = '_blank'; a.rel = 'noopener'; a.dataset.lang = 'en';
864          a.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="15" height="15"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg> ' + esc(fi.label || fi.url.split('/').pop());
865          enGroup.appendChild(a);
866        });
867        filesWrap.appendChild(enGroup);
868      }
869      if (hasFr) {
870        var frGroup = document.createElement('div');
871        frGroup.className = 'owfr-news-files-lang-group';
872        frGroup.innerHTML = '<div class="owfr-news-files-lang-header"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg> Français</div>';
873        data.files_fr.forEach(function (fi) {
874          if (!fi.url) return;
875          var a = document.createElement('a');
876          a.className = 'owfr-news-file-link'; a.href = fi.url; a.target = '_blank'; a.rel = 'noopener'; a.dataset.lang = 'fr';
877          a.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="15" height="15"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg> ' + esc(fi.label || fi.url.split('/').pop());
878          frGroup.appendChild(a);
879        });
880        filesWrap.appendChild(frGroup);
881      }
882    } else if (filesWrap) { filesWrap.remove(); }
883
884    /* bottom image */
885    var bottomImg = card.querySelector('.owfr-news-card-bottom-image');
886    if (data.bottom_image) {
887      if (bottomImg) { bottomImg.src = data.bottom_image; }
888      else {
889        bottomImg = document.createElement('img');
890        bottomImg.className = 'owfr-news-card-bottom-image'; bottomImg.src = data.bottom_image; bottomImg.alt = ''; bottomImg.loading = 'lazy';
891        var actionsEl = card.querySelector('.owfr-news-card-actions');
892        if (actionsEl) actionsEl.appendChild(bottomImg);
893      }
894    } else if (bottomImg) { bottomImg.remove(); }
895
896    /* layout */
897    if (typeof data._layout === 'string') {
898      card.classList.remove('owfr-news-card-wide', 'owfr-news-card-half');
899      if (data._layout) card.classList.add(data._layout);
900    }
901
902    /* section data attribute */
903    if (data.section) card.dataset.section = data.section;
904
905    /* badges */
906    var badgeWrap = card.querySelector('.owfr-news-card-meta-top');
907    if (badgeWrap) {
908      var sectionBadge = badgeWrap.querySelector('.owfr-news-badge:not(.owfr-news-badge-featured)');
909      if (sectionBadge && data.section) {
910        var tab = document.querySelector('.owfr-news-tab-section[data-section="' + data.section + '"]');
911        if (tab) sectionBadge.textContent = tab.textContent.replace(/×$/, '').trim();
912      }
913      var featBadge = badgeWrap.querySelector('.owfr-news-badge-featured');
914      if (data.featured && !featBadge) {
915        var fb = document.createElement('span');
916        fb.className = 'owfr-news-badge owfr-news-badge-featured';
917        fb.textContent = '★ Featured';
918        badgeWrap.appendChild(fb);
919      } else if (!data.featured && featBadge) {
920        featBadge.remove();
921      }
922    }
923  }
924
925  function syncDuplicateCards(itemId, data, sourceCard) {
926    /* Update any matching cards in other panels */
927    document.querySelectorAll('.owfr-news-card[data-item-id="' + itemId + '"]').forEach(function (c) {
928      if (c !== sourceCard) applyItemOverrides(c, data);
929    });
930  }
931
932  /* ══════════════════════════════════════ */
933  /*  RENDER NEW CARD IN DOM               */
934  /* ══════════════════════════════════════ */
935  function renderNewCard(itemId, data, grid) {
936    if (grid.querySelector('[data-item-id="' + itemId + '"]')) return;
937
938    var sectionName = '';
939    var tab = document.querySelector('.owfr-news-tab-section[data-section="' + (data.section || '') + '"]');
940    if (tab) sectionName = tab.textContent.replace(/×$/, '').trim();
941
942    var card = document.createElement('article');
943    card.className = 'owfr-news-card' + (data.featured ? ' owfr-news-card-featured' : '') + (data._layout ? ' ' + data._layout : '');
944    card.dataset.itemId = itemId;
945    card.dataset.section = data.section || '';
946    card.style.position = 'relative';
947
948    var html = '';
949    var hasLogos2 = data.logos && data.logos.length > 0;
950    if (hasLogos2) {
951      html += '<div class="owfr-news-card-logos">';
952      data.logos.forEach(function (url) {
953        html += '<img src="' + esc(url) + '" alt="" class="owfr-news-logo-img" loading="lazy" />';
954      });
955      html += '</div>';
956    } else if (data.image) {
957      html += '<img src="' + esc(data.image) + '" alt="" class="owfr-news-card-image" loading="lazy" />';
958    }
959    html += '<div class="owfr-news-card-content">';
960    html += '<div class="owfr-news-card-meta-top">';
961    if (sectionName) html += '<span class="owfr-news-badge">' + esc(sectionName) + '</span>';
962    if (data.featured) html += '<span class="owfr-news-badge owfr-news-badge-featured">★ Featured</span>';
963    html += '</div>';
964    html += '<h3>' + esc(data.title) + '</h3>';
965    if (data.summary) html += '<p class="owfr-news-card-summary">' + esc(data.summary) + '</p><button class="owfr-news-read-more" aria-expanded="false">Read more</button>';
966    html += '<div class="owfr-news-card-meta-bottom">';
967    if (data.event_date) html += '<span class="owfr-news-date"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> ' + esc(data.event_date) + '</span>';
968    if (data.source_label) html += '<span class="owfr-news-source">' + esc(data.source_label) + '</span>';
969    if (data.language) html += '<span class="owfr-news-lang">' + esc(data.language) + '</span>';
970    html += '</div>';
971    html += '<div class="owfr-news-card-actions">';
972    if (data.external_link) html += '<a class="owfr-news-btn owfr-news-btn-primary" href="' + esc(data.external_link) + '" target="_blank" rel="noopener">' + esc(data.button_text || 'Learn More') + '</a>';
973    if (data.file_url) html += '<a class="owfr-news-btn owfr-news-btn-secondary" href="' + esc(data.file_url) + '" target="_blank" rel="noopener">📄 Download</a>';
974    var hasEn2 = data.files_en && data.files_en.length;
975    var hasFr2 = data.files_fr && data.files_fr.length;
976    if (hasEn2 || hasFr2) {
977      html += '<div class="owfr-news-card-files">';
978      if (hasEn2) {
979        html += '<div class="owfr-news-files-lang-group"><div class="owfr-news-files-lang-header">'
980          + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>'
981          + ' English</div>';
982        data.files_en.forEach(function (fi) {
983          if (!fi.url) return;
984          html += '<a class="owfr-news-file-link" href="' + esc(fi.url) + '" target="_blank" rel="noopener" data-lang="en">'
985            + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="15" height="15"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg> '
986            + esc(fi.label || fi.url.split('/').pop()) + '</a>';
987        });
988        html += '</div>';
989      }
990      if (hasFr2) {
991        html += '<div class="owfr-news-files-lang-group"><div class="owfr-news-files-lang-header">'
992          + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>'
993          + ' Fran\u00e7ais</div>';
994        data.files_fr.forEach(function (fi) {
995          if (!fi.url) return;
996          html += '<a class="owfr-news-file-link" href="' + esc(fi.url) + '" target="_blank" rel="noopener" data-lang="fr">'
997            + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="15" height="15"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg> '
998            + esc(fi.label || fi.url.split('/').pop()) + '</a>';
999        });
1000        html += '</div>';
1001      }
1002      html += '</div>';
1003    }
1004    if (data.bottom_image) html += '<img src="' + esc(data.bottom_image) + '" alt="" class="owfr-news-card-bottom-image" loading="lazy" />';
1005    html += '</div></div>';
1006
1007    card.innerHTML = html;
1008
1009    var addBtn = grid.querySelector('.owfr-news-admin-add-card');
1010    if (addBtn) grid.insertBefore(card, addBtn);
1011    else grid.appendChild(card);
1012
1013    requestAnimationFrame(function () { initReadMoreBtns(card); });
1014    injectCardAdminButtons(card);
1015  }
1016
1017  /* ══════════════════════════════════════ */
1018  /*  MODAL: EDIT HERO                     */
1019  /* ══════════════════════════════════════ */
1020  function openHeroEditor(heroEl) {
1021    var h2 = heroEl.querySelector('h2');
1022    var p = heroEl.querySelector('p');
1023    var curTitle = h2 ? h2.textContent : '';
1024    var curIntro = p ? p.innerHTML : '';
1025
1026    var f = '';
1027    f += fieldHTML('hero_title', 'Hero Title', curTitle, 'text');
1028    f += '<div class="owfr-news-edit-field"><label>Intro Text <span class="owfr-news-label-hint">(HTML ok)</span></label>'
1029       + '<textarea name="hero_intro" rows="3">' + esc(curIntro) + '</textarea></div>';
1030
1031    var m = createModal('Edit Hero Section', f);
1032
1033    m.querySelector('.owfr-news-edit-form').addEventListener('submit', function (e) {
1034      e.preventDefault();
1035      var status = m.querySelector('.owfr-news-edit-status');
1036      if (!settingsConfig) { status.textContent = '⚠ Cannot save.'; status.style.color = '#c00'; return; }
1037
1038      var fd = new FormData(m.querySelector('.owfr-news-edit-form'));
1039      var payload = {
1040        hero_title: fd.get('hero_title') || '',
1041        hero_intro: fd.get('hero_intro') || ''
1042      };
1043
1044      status.textContent = 'Saving…'; status.style.color = '#666';
1045      fetch(settingsConfig.restUrl, {
1046        method: 'POST', credentials: 'same-origin',
1047        headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsConfig.nonce },
1048        body: JSON.stringify(payload)
1049      })
1050      .then(function (r) { if (!r.ok) throw new Error(); return r.json(); })
1051      .then(function () {
1052        if (h2) h2.textContent = payload.hero_title;
1053        if (p) p.innerHTML = payload.hero_intro;
1054        status.textContent = '✓ Saved!'; status.style.color = '#006B3F';
1055        setTimeout(closeModal, 700);
1056      })
1057      .catch(function () { status.textContent = '✗ Save failed.'; status.style.color = '#c00'; });
1058    });
1059  }
1060
1061  /* ══════════════════════════════════════ */
1062  /*  MODAL: EDIT WELCOME BANNER           */
1063  /* ══════════════════════════════════════ */
1064  function openWelcomeEditor(welEl) {
1065    var titleEl = welEl.querySelector('.owfr-news-welcome-title');
1066    var textEl  = welEl.querySelector('.owfr-news-welcome-text');
1067    var steps   = welEl.querySelectorAll('.owfr-news-welcome-step-label');
1068
1069    var curTitle = titleEl ? titleEl.textContent : '';
1070    var curText  = textEl  ? textEl.innerHTML    : '';
1071    var curStep1 = steps[0] ? steps[0].innerHTML : '';
1072    var curStep2 = steps[1] ? steps[1].innerHTML : '';
1073    var curStep3 = steps[2] ? steps[2].innerHTML : '';
1074
1075    var f = '';
1076    f += fieldHTML('welcome_title', 'Banner Title', curTitle, 'text');
1077    f += '<div class="owfr-news-edit-field"><label>Description <span class="owfr-news-label-hint">(HTML ok)</span></label>'
1078       + '<textarea name="welcome_text" rows="3">' + esc(curText) + '</textarea></div>';
1079    f += '<div class="owfr-news-edit-field"><label>Step 1 <span class="owfr-news-label-hint">(HTML ok)</span></label>'
1080       + '<input type="text" name="welcome_step_1" value="' + esc(curStep1) + '" /></div>';
1081    f += '<div class="owfr-news-edit-field"><label>Step 2 <span class="owfr-news-label-hint">(HTML ok)</span></label>'
1082       + '<input type="text" name="welcome_step_2" value="' + esc(curStep2) + '" /></div>';
1083    f += '<div class="owfr-news-edit-field"><label>Step 3 <span class="owfr-news-label-hint">(HTML ok)</span></label>'
1084       + '<input type="text" name="welcome_step_3" value="' + esc(curStep3) + '" /></div>';
1085
1086    var m = createModal('Edit Welcome Banner', f);
1087
1088    m.querySelector('.owfr-news-edit-form').addEventListener('submit', function (e) {
1089      e.preventDefault();
1090      var status = m.querySelector('.owfr-news-edit-status');
1091      if (!settingsConfig) { status.textContent = '⚠ Cannot save.'; status.style.color = '#c00'; return; }
1092
1093      var fd = new FormData(m.querySelector('.owfr-news-edit-form'));
1094      var payload = {
1095        welcome_title:  fd.get('welcome_title')  || '',
1096        welcome_text:   fd.get('welcome_text')   || '',
1097        welcome_step_1: fd.get('welcome_step_1') || '',
1098        welcome_step_2: fd.get('welcome_step_2') || '',
1099        welcome_step_3: fd.get('welcome_step_3') || ''
1100      };
1101
1102      status.textContent = 'Saving…'; status.style.color = '#666';
1103      fetch(settingsConfig.restUrl, {
1104        method: 'POST', credentials: 'same-origin',
1105        headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': settingsConfig.nonce },
1106        body: JSON.stringify(payload)
1107      })
1108      .then(function (r) { if (!r.ok) throw new Error(); return r.json(); })
1109      .then(function () {
1110        if (titleEl) titleEl.textContent = payload.welcome_title;
1111        if (textEl)  textEl.innerHTML    = payload.welcome_text;
1112        if (steps[0]) steps[0].innerHTML = payload.welcome_step_1;
1113        if (steps[1]) steps[1].innerHTML = payload.welcome_step_2;
1114        if (steps[2]) steps[2].innerHTML = payload.welcome_step_3;
1115        status.textContent = '✓ Saved!'; status.style.color = '#006B3F';
1116        setTimeout(closeModal, 700);
1117      })
1118      .catch(function () { status.textContent = '✗ Save failed.'; status.style.color = '#c00'; });
1119    });
1120  }
1121
1122  /* ══════════════════════════════════════ */
1123  /*  MODAL: EDIT / ADD SECTION            */
1124  /* ══════════════════════════════════════ */
1125  function openSectionEditor(hdr) {
1126    var slug = hdr.dataset.sectionSlug;
1127    var titleEl = hdr.querySelector('.owfr-news-section-title');
1128    var curName = titleEl ? titleEl.textContent.trim() : '';
1129
1130    var f = '';
1131    f += fieldHTML('name', 'Section Name', curName, 'text');
1132    f += '<input type="hidden" name="slug" value="' + esc(slug) + '" />';
1133
1134    var m = createModal('Edit Section', f);
1135    m.querySelector('.owfr-news-edit-form').addEventListener('submit', function (e) {
1136      e.preventDefault();
1137      var status = m.querySelector('.owfr-news-edit-status');
1138      if (!sectionsConfig) { status.textContent = '⚠ Cannot save.'; status.style.color = '#c00'; return; }
1139
1140      var fd = new FormData(m.querySelector('.owfr-news-edit-form'));
1141      var name = (fd.get('name') || '').trim();
1142      if (!name) { status.textContent = 'Section name is required.'; status.style.color = '#c00'; return; }
1143
1144      status.textContent = 'Saving…'; status.style.color = '#666';
1145      fetch(sectionsConfig.restUrl, {
1146        method: 'POST', credentials: 'same-origin',
1147        headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': sectionsConfig.nonce },
1148        body: JSON.stringify({ slug: slug, name: name, icon: '' })
1149      })
1150      .then(function (r) { if (!r.ok) throw new Error(); return r.json(); })
1151      .then(function () {
1152        if (titleEl) titleEl.textContent = name;
1153        /* Update the tab button text */
1154        var tab = document.querySelector('.owfr-news-tab-section[data-section="' + slug + '"]');
1155        if (tab) {
1156          var deleteSpan = tab.querySelector('.owfr-news-admin-delete-section-tab');
1157          tab.textContent = name;
1158          if (deleteSpan) tab.appendChild(deleteSpan);
1159        }
1160        status.textContent = '✓ Saved!'; status.style.color = '#006B3F';
1161        setTimeout(closeModal, 600);
1162      })
1163      .catch(function () { status.textContent = '✗ Save failed.'; status.style.color = '#c00'; });
1164    });
1165  }
1166
1167  function openNewSectionEditor() {
1168    var f = '';
1169    f += fieldHTML('name', 'Section Name', '', 'text');
1170
1171    var m = createModal('Add New Section', f);
1172    m.querySelector('.owfr-news-edit-form').addEventListener('submit', function (e) {
1173      e.preventDefault();
1174      var status = m.querySelector('.owfr-news-edit-status');
1175      if (!sectionsConfig) { status.textContent = '⚠ Cannot save.'; status.style.color = '#c00'; return; }
1176
1177      var fd = new FormData(m.querySelector('.owfr-news-edit-form'));
1178      var name = (fd.get('name') || '').trim();
1179      if (!name) { status.textContent = 'Section name is required.'; status.style.color = '#c00'; return; }
1180
1181      var slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
1182      if (!slug) { status.textContent = 'Invalid name.'; status.style.color = '#c00'; return; }
1183
1184      /* check for duplicate */
1185      if (document.querySelector('.owfr-news-tab-section[data-section="' + slug + '"]')) {
1186        status.textContent = 'A section with this name already exists.'; status.style.color = '#c00'; return;
1187      }
1188
1189      status.textContent = 'Saving…'; status.style.color = '#666';
1190      fetch(sectionsConfig.restUrl, {
1191        method: 'POST', credentials: 'same-origin',
1192        headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': sectionsConfig.nonce },
1193        body: JSON.stringify({ slug: slug, name: name, icon: '' })
1194      })
1195      .then(function (r) { if (!r.ok) throw new Error(); return r.json(); })
1196      .then(function () {
1197        renderNewSection(slug, name);
1198        status.textContent = '✓ Section created!'; status.style.color = '#006B3F';
1199        setTimeout(closeModal, 600);
1200      })
1201      .catch(function () { status.textContent = '✗ Save failed.'; status.style.color = '#c00'; });
1202    });
1203  }
1204
1205  function renderNewSection(slug, name) {
1206    /* Add tab */
1207    var tl = document.getElementById('owfr-news-tabs');
1208    if (!tl) return;
1209    var li = document.createElement('li');
1210    var btn = document.createElement('button');
1211    btn.className = 'owfr-news-tab owfr-news-tab-section';
1212    btn.dataset.section = slug;
1213    btn.setAttribute('role', 'tab');
1214    btn.type = 'button';
1215    btn.textContent = name;
1216    li.appendChild(btn);
1217    var addLi = tl.querySelector('.owfr-news-add-section-li');
1218    if (addLi) tl.insertBefore(li, addLi);
1219    else tl.appendChild(li);
1220    injectTabDeleteButton(btn);
1221
1222    /* Add panel */
1223    var inner = document.querySelector('.owfr-news-inner');
1224    if (!inner) return;
1225    var panel = document.createElement('div');
1226    panel.className = 'owfr-news-panel';
1227    panel.id = 'owfr-news-panel-' + slug;
1228    panel.setAttribute('role', 'tabpanel');
1229    panel.innerHTML = '<div class="owfr-news-section-header" data-section-slug="' + esc(slug) + '">'
1230      + '<span class="owfr-news-section-title">' + esc(name) + '</span>'
1231      + '<span class="owfr-news-section-count">0 items</span>'
1232      + '</div><div class="owfr-news-grid"></div>';
1233    inner.appendChild(panel);
1234
1235    /* Section header admin buttons */
1236    var hdr = panel.querySelector('.owfr-news-section-header');
1237    if (hdr) {
1238      hdr.style.position = 'relative';
1239      var editBtn = document.createElement('button');
1240      editBtn.className = 'owfr-news-admin-section-edit-btn';
1241      editBtn.type = 'button';
1242      editBtn.textContent = '✎ Edit';
1243      editBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); openSectionEditor(hdr); });
1244      hdr.appendChild(editBtn);
1245
1246      var delBtn = document.createElement('button');
1247      delBtn.className = 'owfr-news-admin-section-delete-btn';
1248      delBtn.type = 'button';
1249      delBtn.textContent = '✕ Delete';
1250      delBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); deleteSection(slug); });
1251      hdr.appendChild(delBtn);
1252    }
1253
1254    /* "Add New" button in the grid */
1255    var grid = panel.querySelector('.owfr-news-grid');
1256    var addCard = document.createElement('button');
1257    addCard.className = 'owfr-news-admin-add-card';
1258    addCard.type = 'button';
1259    addCard.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span>Add New Item</span>';
1260    addCard.addEventListener('click', function () { openNewItemEditor(slug, grid); });
1261    grid.appendChild(addCard);
1262
1263    initGridDragDrop(grid, slug);
1264
1265    /* Switch to the new tab */
1266    btn.click();
1267  }
1268
1269  function deleteSection(slug) {
1270    if (!confirm('Delete this section? Items in this section will NOT be deleted, but they will become uncategorized.')) return;
1271    if (sectionsConfig) {
1272      fetch(sectionsConfig.restUrl + '/' + encodeURIComponent(slug), {
1273        method: 'DELETE', credentials: 'same-origin',
1274        headers: { 'X-WP-Nonce': sectionsConfig.nonce }
1275      }).catch(function () {});
1276    }
1277    /* Remove tab */
1278    var tab = document.querySelector('.owfr-news-tab-section[data-section="' + slug + '"]');
1279    if (tab && tab.parentElement) tab.parentElement.remove();
1280    /* Remove panel */
1281    var panel = document.getElementById('owfr-news-panel-' + slug);
1282    if (panel) panel.remove();
1283    /* Switch to first tab */
1284    var first = document.querySelector('.owfr-news-tab');
1285    if (first) first.click();
1286  }
1287
1288  /* ══════════════════════════════════════ */
1289  /*  DRAG-AND-DROP REORDER                */
1290  /* ══════════════════════════════════════ */
1291  var draggedCard = null;
1292  var placeholder = null;
1293  var dirtyPanels = {};
1294
1295  function injectDragHandle(card) {
1296    if (card.querySelector('.owfr-news-drag-handle')) return;
1297    card.setAttribute('draggable', 'true');
1298    card.style.position = 'relative';
1299    var handle = document.createElement('button');
1300    handle.className = 'owfr-news-drag-handle';
1301    handle.type = 'button';
1302    handle.title = 'Drag to reorder';
1303    handle.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
1304      + '<circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/>'
1305      + '<circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/>'
1306      + '<circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>';
1307    card.insertBefore(handle, card.firstChild);
1308    card.addEventListener('dragstart', onDragStart);
1309    card.addEventListener('dragend', onDragEnd);
1310  }
1311
1312  function onDragStart(e) {
1313    draggedCard = this;
1314    this.classList.add('owfr-news-dragging');
1315    placeholder = document.createElement('div');
1316    placeholder.className = 'owfr-news-drop-indicator';
1317    e.dataTransfer.effectAllowed = 'move';
1318    e.dataTransfer.setData('text/plain', this.dataset.itemId);
1319    var grid = this.closest('.owfr-news-grid');
1320    if (grid) grid.classList.add('owfr-news-drag-active');
1321  }
1322
1323  function onDragEnd() {
1324    if (draggedCard) draggedCard.classList.remove('owfr-news-dragging');
1325    if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
1326    document.querySelectorAll('.owfr-news-grid.owfr-news-drag-active').forEach(function (g) { g.classList.remove('owfr-news-drag-active'); });
1327    draggedCard = null;
1328    placeholder = null;
1329  }
1330
1331  function initGridDragDrop(grid, sectionId) {
1332    grid.addEventListener('dragover', function (e) {
1333      e.preventDefault(); e.dataTransfer.dropEffect = 'move';
1334      if (!draggedCard) return;
1335      var t = getDropTarget(e, grid);
1336      if (t && t !== draggedCard && t !== placeholder) {
1337        var r = t.getBoundingClientRect();
1338        if (e.clientY < r.top + r.height / 2) grid.insertBefore(placeholder, t);
1339        else grid.insertBefore(placeholder, t.nextSibling);
1340      } else if (!grid.contains(placeholder)) {
1341        var ab = grid.querySelector('.owfr-news-admin-add-card');
1342        if (ab) grid.insertBefore(placeholder, ab);
1343        else grid.appendChild(placeholder);
1344      }
1345    });
1346    grid.addEventListener('drop', function (e) {
1347      e.preventDefault();
1348      if (!draggedCard || !placeholder || !placeholder.parentNode) return;
1349      grid.insertBefore(draggedCard, placeholder);
1350      placeholder.parentNode.removeChild(placeholder);
1351      draggedCard.classList.remove('owfr-news-dragging');
1352      dirtyPanels[sectionId] = true;
1353      showSaveOrderBtn();
1354      draggedCard = null;
1355      placeholder = null;
1356      grid.classList.remove('owfr-news-drag-active');
1357    });
1358  }
1359
1360  function getDropTarget(e, grid) {
1361    var cards = grid.querySelectorAll('.owfr-news-card[data-item-id]');
1362    var closest = null, closestDist = Infinity;
1363    cards.forEach(function (card) {
1364      if (card === draggedCard) return;
1365      var rect = card.getBoundingClientRect();
1366      var midY = rect.top + rect.height / 2;
1367      var dist = Math.abs(e.clientY - midY);
1368      if (dist < closestDist) { closestDist = dist; closest = card; }
1369    });
1370    return closest;
1371  }
1372
1373  /* Init drag-drop on existing section grids (skip "all" panel) */
1374  document.querySelectorAll('.owfr-news-panel').forEach(function (panel) {
1375    var sectionId = panel.id.replace('owfr-news-panel-', '');
1376    if (sectionId === 'all') return;
1377    var grid = panel.querySelector('.owfr-news-grid');
1378    if (grid) initGridDragDrop(grid, sectionId);
1379  });
1380
1381  /* Save Order floating button */
1382  var saveOrderBtn = document.createElement('button');
1383  saveOrderBtn.className = 'owfr-news-save-order-btn';
1384  saveOrderBtn.type = 'button';
1385  saveOrderBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg> Save Order';
1386  document.body.appendChild(saveOrderBtn);
1387
1388  function showSaveOrderBtn() { saveOrderBtn.classList.add('visible'); }
1389
1390  saveOrderBtn.addEventListener('click', function () {
1391    if (!orderConfig) { alert('Order endpoint not configured.'); return; }
1392    saveOrderBtn.textContent = 'Saving…';
1393    saveOrderBtn.disabled = true;
1394
1395    var sections = Object.keys(dirtyPanels);
1396    var promises = sections.map(function (sectionId) {
1397      var panel = document.getElementById('owfr-news-panel-' + sectionId);
1398      if (!panel) return Promise.resolve();
1399      var grid = panel.querySelector('.owfr-news-grid');
1400      if (!grid) return Promise.resolve();
1401      var order = [];
1402      grid.querySelectorAll('.owfr-news-card[data-item-id]').forEach(function (c) { order.push(c.dataset.itemId); });
1403      return fetch(orderConfig.reorderUrl, {
1404        method: 'POST', credentials: 'same-origin',
1405        headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': orderConfig.nonce },
1406        body: JSON.stringify({ section: sectionId, order: order })
1407      });
1408    });
1409
1410    Promise.all(promises).then(function () {
1411      saveOrderBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> Saved!';
1412      dirtyPanels = {};
1413      setTimeout(function () {
1414        saveOrderBtn.classList.remove('visible');
1415        saveOrderBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg> Save Order';
1416        saveOrderBtn.disabled = false;
1417      }, 1500);
1418    }).catch(function () {
1419      saveOrderBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg> Save Order';
1420      saveOrderBtn.disabled = false;
1421      alert('Failed to save order.');
1422    });
1423  });
1424
1425  /* ══════════════════════════════════════ */
1426  /*  IMAGE LIGHTBOX                       */
1427  /* ══════════════════════════════════════ */
1428  function openLightbox(src) {
1429    closeLightbox();
1430    var lb = document.createElement('div');
1431    lb.className = 'owfr-news-lightbox';
1432    lb.innerHTML = '<button class="owfr-news-lightbox-close" aria-label="Close">&times;</button>'
1433                 + '<img src="' + esc(src) + '" alt="" />';
1434    document.body.appendChild(lb);
1435    document.body.style.overflow = 'hidden';
1436    lb.addEventListener('click', function (e) {
1437      if (e.target === lb) closeLightbox();
1438    });
1439    lb.querySelector('.owfr-news-lightbox-close').addEventListener('click', closeLightbox);
1440  }
1441
1442  function closeLightbox() {
1443    var lb = document.querySelector('.owfr-news-lightbox');
1444    if (lb) lb.remove();
1445    if (!document.getElementById('owfr-news-edit-modal')) {
1446      document.body.style.overflow = '';
1447    }
1448  }
1449
1450  document.addEventListener('keydown', function (e) {
1451    if (e.key === 'Escape') closeLightbox();
1452  });
1453
1454  /* Delegate click on card images */
1455  document.addEventListener('click', function (e) {
1456    var img = e.target;
1457    if (img.classList.contains('owfr-news-card-image') || img.classList.contains('owfr-news-card-bottom-image')) {
1458      e.preventDefault();
1459      openLightbox(img.src);
1460    }
1461  });
1462
1463});

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.