'use strict';
const CFG = JSON.parse(document.getElementById('engineConfig').textContent);
const CSRF_TOKEN = CFG.csrf || '';
const DAY_NAMES = ['DOM','SEG','TER','QUA','QUI','SEX','SÁB'];
const MONTH_NAMES = ['JAN','FEV','MAR','ABR','MAI','JUN','JUL','AGO','SET','OUT','NOV','DEZ'];
/* ── State ─────────────────────────────────────────────────── */
let items = []; // [{id, title, subtitle, date}]
let nextItemId = 1;
let slides = []; // arrays of items (chunked by template max)
let currentSlide = 0;
let currentBg = null; // gradient/solid CSS or image URL
let bgIsImage = false;
let blurAmount = 0;
let overlaySource = 'none';
let overlayValue = '';
let overlayOpacity = 50;
function esc(str) {
return String(str ?? '')
.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
/* ── Dynamic items (platform content endpoint) ─────────────── */
let dynSelected = new Set((CFG.presel || []).filter(id => (CFG.dynamic || []).some(d => d.id === id)));
let dynCatValue = ''; // active category filter ('' = all)
// Items visible under the current category filter (selection persists across
// filter changes — hidden-but-selected items stay in the post).
function dynVisible() {
return (CFG.dynamic || []).filter(d => !dynCatValue || (d.category || '') === dynCatValue);
}
// Build the category dropdown from the distinct categories present in the
// dynamic items (first-seen order = the platform's order). Hidden when <2.
function buildCatFilter() {
const sel = document.getElementById('dynCatFilter');
const bar = document.getElementById('dynFilterBar');
if (!sel || !bar) return;
const seen = new Map();
(CFG.dynamic || []).forEach(d => {
const k = d.category || '';
if (k && !seen.has(k)) seen.set(k, d.category_label || k);
});
if (seen.size < 2) { bar.style.display = 'none'; return; }
let html = ``;
seen.forEach((lbl, k) => { html += ``; });
sel.innerHTML = html;
sel.value = dynCatValue;
bar.style.display = '';
}
function onCatFilter(v) { dynCatValue = v; renderDynList(); }
function renderDynList() {
const list = document.getElementById('dynList');
if (!list) return;
list.innerHTML = dynVisible().map(d => {
const dp = d.date_start ? dateParts(d.date_start.slice(0, 10)) : null;
return ``;
}).join('');
list.querySelectorAll('input[data-dyn-id]').forEach(cb => {
cb.addEventListener('change', () => {
cb.checked ? dynSelected.add(cb.dataset.dynId) : dynSelected.delete(cb.dataset.dynId);
rebuildSlides();
});
});
}
function dynToggleAll() {
// Operates on the currently-visible (filtered) items only.
const visible = dynVisible();
const everyOn = visible.length > 0 && visible.every(d => dynSelected.has(d.id));
visible.forEach(d => everyOn ? dynSelected.delete(d.id) : dynSelected.add(d.id));
document.getElementById('dynSelectAllBtn').textContent = everyOn ? 'Selecionar todos' : 'Limpar';
renderDynList();
rebuildSlides();
}
// Selected dynamic items, normalized to the same shape as manual items.
function dynamicItems() {
return (CFG.dynamic || [])
.filter(d => dynSelected.has(d.id))
.map(d => ({
id: 'dyn:' + d.id,
title: d.title,
subtitle: d.subtitle || '',
date: d.date_start ? d.date_start.slice(0, 10) : '',
}));
}
/* ── Lazy gallery browser (photo_session content, Fase 4) ──── */
// Sessions render collapsed (title + count only); the thumbnail images are
// only attached on first expand, so a platform with many galleries doesn't
// fetch hundreds of photos on page load. The same accordion is used in two
// places: the Fundo card (mode 'bg': click = use photo as background) and
// the photo-templates picker (mode 'multi': numbered multi-select).
let selectedPhotos = []; // [{url, title, subtitle, date}], in selection order
function initGalleryAccordion(containerId, mode) {
const box = document.getElementById(containerId);
if (!box) return;
box.innerHTML = (CFG.dynamicPhoto || []).map(s => `
`).join('');
box.querySelectorAll('.ig-gal-head').forEach(head => {
head.addEventListener('click', () => {
const body = head.nextElementSibling;
const opening = body.classList.contains('d-none');
if (opening && body.dataset.loaded !== '1') {
const s = (CFG.dynamicPhoto || []).find(x => String(x.id) === head.dataset.session);
body.innerHTML = (s ? s.media : []).map(m => `
`).join('');
body.dataset.loaded = '1';
body.querySelectorAll('.ig-photo-pick').forEach(el => {
el.addEventListener('click', () =>
mode === 'multi' ? togglePhoto(el) : pickPhotoAsBg(el));
});
if (mode === 'multi') syncPhotoPicker();
}
body.classList.toggle('d-none', !opening);
const icon = head.querySelector('.bi');
if (icon) icon.className = opening ? 'bi bi-chevron-up' : 'bi bi-chevron-down';
});
});
}
// Fundo card: a session photo as the post background (full-res via proxy).
function pickPhotoAsBg(el) {
clearBgActive();
el.classList.add('active');
currentBg = el.dataset.url;
bgIsImage = true;
applyStyle();
}
function togglePhoto(el) {
const url = el.dataset.url;
const idx = selectedPhotos.findIndex(p => p.url === url);
if (idx >= 0) {
selectedPhotos.splice(idx, 1);
} else {
const session = (CFG.dynamicPhoto || []).find(s => String(s.id) === el.dataset.session);
selectedPhotos.push({
url,
title: session?.title || '',
subtitle: session?.subtitle || '',
date: session?.date || '',
});
}
syncPhotoPicker();
rebuildSlides();
}
function syncPhotoPicker() {
document.querySelectorAll('#photoPicker .ig-photo-pick').forEach(el => {
const idx = selectedPhotos.findIndex(p => p.url === el.dataset.url);
el.classList.toggle('active', idx >= 0);
el.querySelector('.ig-photo-pick-num').textContent = idx >= 0 ? String(idx + 1) : '';
});
const cnt = document.getElementById('photoSelCount');
if (cnt) cnt.textContent = selectedPhotos.length + ' selecionada' + (selectedPhotos.length === 1 ? '' : 's');
}
/* ── Manual items ──────────────────────────────────────────── */
function addItem(data) {
items.push(Object.assign({ id: nextItemId++, title: '', subtitle: '', date: '' }, data || {}));
renderItemList();
rebuildSlides();
}
function removeItem(id) {
items = items.filter(i => i.id !== id);
renderItemList();
rebuildSlides();
}
function onItemField(id, field, value) {
const it = items.find(i => i.id === id);
if (it) { it[field] = value; rebuildSlides(); }
}
function renderItemList() {
const box = document.getElementById('itemList');
document.getElementById('itemsEmpty').style.display = items.length ? 'none' : '';
box.innerHTML = items.map(it => `
`).join('');
}
function dateParts(iso) {
if (!iso) return null;
const d = new Date(iso + 'T12:00:00');
if (isNaN(d)) return null;
return {
dayName: DAY_NAMES[d.getDay()],
dayNum: String(d.getDate()),
month: MONTH_NAMES[d.getMonth()],
year: String(d.getFullYear()),
};
}
/* ── Template / format ─────────────────────────────────────── */
function currentTemplate() {
const slug = document.querySelector('input[name="tplPick"]:checked')?.value;
return CFG.templates.find(t => t.slug === slug) || CFG.templates[0];
}
function currentFormatId() {
return document.querySelector('input[name="postFormat"]:checked').value;
}
// Export dimensions for the active format, honoring the paper orientation swap.
function formatDims() {
const id = currentFormatId();
const f = CFG.formats[id];
let w = f.w, h = f.h;
if (f.group === 'paper' && document.getElementById('orientSwap').checked) {
[w, h] = [h, w];
}
return { id, w, h, group: f.group };
}
function maxItemsPerSlide() {
const tpl = currentTemplate();
const id = currentFormatId();
return tpl.maxItems[id] || tpl.maxItems['default'] || 1;
}
function isPhotoTemplate(tpl) {
return (tpl.itemTypes || []).includes('photo_session');
}
function toggleCard(id, show) {
const el = document.getElementById(id);
if (el) el.style.display = show ? '' : 'none';
}
function onTemplateChange() {
const tpl = currentTemplate();
const photo = isPhotoTemplate(tpl);
const isGrid = tpl.slug === 'photo-grid';
// The heading/tagline inputs only apply to the list template; the single
// announcement and photo templates take their caption from the item itself.
const single = !tpl.multi || photo;
document.getElementById('headingWrap').style.display = single ? 'none' : '';
document.getElementById('taglineWrap').style.display = single ? 'none' : '';
toggleCard('dynContentCard', !photo);
toggleCard('manualContentCard', !photo);
toggleCard('photoCard', photo);
toggleCard('fundoCard', !isGrid);
toggleCard('estiloCard', !isGrid);
toggleCard('textoCard', !isGrid);
rebuildSlides();
}
function onFormatChange() {
const { w, h, group } = formatDims();
document.getElementById('orientWrap').style.display = group === 'paper' ? '' : 'none';
const post = document.getElementById('postPreview');
post.style.aspectRatio = w + ' / ' + h;
document.getElementById('previewWrapper').classList.toggle('is-wide', w > h);
rebuildSlides();
}
/* ── Slides (overflow pagination) ──────────────────────────── */
function rebuildSlides() {
const tpl = currentTemplate();
const usable = isPhotoTemplate(tpl)
? selectedPhotos
: dynamicItems().concat(items.filter(i => i.title.trim() !== ''));
slides = SocialEngine.chunk(usable, maxItemsPerSlide());
if (!slides.length) slides = [[]];
if (currentSlide >= slides.length) currentSlide = slides.length - 1;
renderSlideNav();
updateSlideVisuals();
}
function renderSlideNav() {
const bar = document.getElementById('slideToolbar');
const dots = document.getElementById('slideDots');
bar.style.display = slides.length > 1 ? '' : 'none';
dots.innerHTML = '';
slides.forEach((_, i) => {
const dot = document.createElement('button');
dot.type = 'button';
dot.className = 'ig-slide-dot' + (i === currentSlide ? ' active' : '');
dot.addEventListener('click', () => { currentSlide = i; renderSlideNav(); updateSlideVisuals(); });
dots.appendChild(dot);
});
}
function slidePrev() { if (currentSlide > 0) { currentSlide--; renderSlideNav(); updateSlideVisuals(); } }
function slideNext() { if (currentSlide < slides.length - 1) { currentSlide++; renderSlideNav(); updateSlideVisuals(); } }
/* ── Slide rendering (templates) ───────────────────────────── */
function logoRowHtml() {
if (!CFG.logo || !document.getElementById('showLogo')?.checked) return '';
return ``;
}
function itemCardHtml(it) {
const dp = dateParts(it.date);
return `
${dp ? `
${dp.dayName}
${dp.dayNum}
${dp.month}
` : ''}
${esc(it.title)}
${it.subtitle ? `
${esc(it.subtitle)}
` : ''}
`;
}
function renderSlide() {
const content = document.getElementById('postContent');
const tpl = currentTemplate();
const slide = slides[currentSlide] || [];
const cta = document.getElementById('ctaText').value.trim();
let html = logoRowHtml();
if (tpl.slug === 'photo-grid') {
content.innerHTML = '';
return;
}
if (tpl.slug === 'photo-highlight') {
const it = slide[0];
if (!it) {
html += 'Seleciona uma foto à esquerda
';
} else {
const dp = dateParts(it.date);
html += `
${dp ? `
${dp.dayName} · ${dp.dayNum} ${dp.month} ${dp.year}
` : ''}
${esc(it.title)}
${it.subtitle ? `
${esc(it.subtitle)}
` : ''}
`;
}
} else if (tpl.slug === 'single-announcement') {
const it = slide[0];
if (!it) {
html += 'Adiciona um item à esquerda
';
} else {
const dp = dateParts(it.date);
html += `
${dp ? `
${dp.dayName} · ${dp.dayNum} ${dp.month} ${dp.year}
` : ''}
${esc(it.title)}
${it.subtitle ? `
${esc(it.subtitle)}
` : ''}
`;
}
} else { // event-list
const tagline = document.getElementById('tagline').value.trim();
const heading = document.getElementById('headingText').value.trim();
html += `
${tagline ? `
${esc(tagline)}
` : ''}
${heading ? `
${esc(heading)}
` : ''}
${slide.length ? slide.map(itemCardHtml).join('') : '
Adiciona itens à esquerda
'}
`;
}
if (cta) html += `${esc(cta)}
`;
content.innerHTML = html;
}
/* ── Background / style ────────────────────────────────────── */
function luminance(hex) {
const m = /^#([0-9a-f]{6})$/i.exec(hex || '');
if (!m) return 0;
const n = parseInt(m[1], 16);
return (0.2126 * (n >> 16) + 0.7152 * ((n >> 8) & 255) + 0.0722 * (n & 255)) / 255;
}
// The displayed background: the user's pick, except for photo-highlight where
// the current slide's selected photo always wins.
function effectiveBg() {
const tpl = currentTemplate();
if (tpl.slug === 'photo-highlight') {
const it = (slides[currentSlide] || [])[0];
return { bg: it ? it.url : null, isImage: !!it };
}
return { bg: currentBg, isImage: bgIsImage };
}
function applyStyle() {
const post = document.getElementById('postPreview');
const bg = document.getElementById('photoBg');
const { bg: effBg, isImage: effIsImage } = effectiveBg();
if (effIsImage && effBg) {
bg.style.display = '';
bg.style.backgroundImage = "url('" + effBg.replace(/'/g, "\\'") + "')";
bg.style.filter = blurAmount > 0 ? `blur(${blurAmount}px) brightness(.85)` : '';
bg.style.transform = blurAmount > 0 ? `scale(${1 + blurAmount / 280})` : '';
post.style.background = '';
post.classList.add('is-photo');
post.classList.remove('is-light');
} else {
bg.style.display = 'none';
bg.style.backgroundImage = '';
bg.style.filter = '';
bg.style.transform = '';
post.style.background = effBg || '';
post.classList.remove('is-photo');
// Solid light colors flip the text palette + drop the dark wash.
post.classList.toggle('is-light', !!effBg && luminance(effBg) > 0.72);
}
document.getElementById('blurWrap').style.display = effIsImage ? '' : 'none';
const ov = document.getElementById('customOverlay');
if ((overlaySource === 'gradient' || overlaySource === 'solid') && overlayValue) {
ov.style.background = overlayValue;
ov.style.opacity = (overlayOpacity / 100).toString();
} else {
ov.style.background = '';
ov.style.opacity = '0';
}
post.classList.toggle('has-watermark', !!document.getElementById('showWatermark')?.checked);
const content = document.getElementById('postContent');
content.dataset.alignH = document.querySelector('input[name="alignH"]:checked').value;
content.dataset.alignV = document.querySelector('input[name="alignV"]:checked').value;
renderSlide(); // logo toggle lives inside slide markup
}
// Switches the canvas between the photo-grid layout and the normal
// background+content layout, then applies the current style.
function updateSlideVisuals() {
const tpl = currentTemplate();
const grid = document.getElementById('photoGrid');
const content = document.getElementById('postContent');
const overlay = document.querySelector('.ig-overlay');
if (tpl.slug === 'photo-grid') {
const slide = slides[currentSlide] || [];
grid.style.display = 'grid';
content.style.display = 'none';
overlay.style.display = 'none';
document.getElementById('photoBg').style.display = 'none';
document.getElementById('customOverlay').style.opacity = '0';
const cols = slide.length <= 1 ? 1 : (slide.length <= 4 ? 2 : 3);
grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
grid.innerHTML = slide.length
? slide.map(p => ``).join('')
: 'Seleciona fotos à esquerda
';
return;
}
grid.style.display = 'none';
content.style.display = '';
overlay.style.display = '';
applyStyle();
}
function onBlurChange() {
blurAmount = parseInt(document.getElementById('blurRange').value, 10) || 0;
document.getElementById('blurValue').textContent = blurAmount + ' px';
applyStyle();
}
function onAlignChange() { applyStyle(); }
function onOverlayChange() {
overlaySource = document.querySelector('input[name="overlaySource"]:checked').value;
const cur = overlayValue || '';
if (overlaySource === 'gradient' && !cur.startsWith('linear-gradient') && !cur.startsWith('radial-gradient')) {
overlayValue = CFG.presets.gradients[0].css;
} else if (overlaySource === 'solid' && (cur === '' || cur.includes('gradient'))) {
overlayValue = CFG.presets.solids[0].css;
}
const hasPreset = overlaySource !== 'none';
document.getElementById('ovGradientWrap').style.display = overlaySource === 'gradient' ? '' : 'none';
document.getElementById('ovSolidWrap').style.display = overlaySource === 'solid' ? '' : 'none';
document.getElementById('ovOpacityWrap').style.display = hasPreset ? '' : 'none';
markActive('#ovGradientList .ig-preset, #ovSolidList .ig-preset', overlayValue);
applyStyle();
}
function onOverlayOpacityChange() {
overlayOpacity = parseInt(document.getElementById('ovOpacity').value, 10) || 0;
document.getElementById('ovOpacityValue').textContent = overlayOpacity + ' %';
applyStyle();
}
function markActive(selector, css) {
document.querySelectorAll(selector).forEach(b => b.classList.toggle('active', b.dataset.bg === css));
}
function clearBgActive() {
document.querySelectorAll('#gradientList .ig-preset, #solidList .ig-preset, .ig-img-preset, #bgGalleryList .ig-photo-pick')
.forEach(b => b.classList.remove('active'));
}
function renderPresets(containerId, presets, onPick) {
const el = document.getElementById(containerId);
el.innerHTML = presets.map(p => `
`).join('');
el.querySelectorAll('.ig-preset').forEach(b => b.addEventListener('click', () => onPick(b)));
}
function pickBgPreset(btn) {
clearBgActive();
btn.classList.add('active');
currentBg = btn.dataset.bg;
bgIsImage = false;
applyStyle();
}
function pickOverlayPreset(btn) {
overlayValue = btn.dataset.bg;
markActive('#ovGradientList .ig-preset, #ovSolidList .ig-preset', overlayValue);
applyStyle();
}
/* Image backgrounds (gallery + local file) */
function bindImgPreset(btn) {
btn.addEventListener('click', () => {
clearBgActive();
btn.classList.add('active');
currentBg = btn.dataset.img;
bgIsImage = true;
applyStyle();
});
}
document.getElementById('imageInput').addEventListener('change', function () {
if (this.files[0]) readLocalImage(this.files[0]);
});
const dropZone = document.getElementById('dropZone');
['dragenter','dragover'].forEach(ev => dropZone.addEventListener(ev, e => {
e.preventDefault(); dropZone.classList.add('dragging');
}));
['dragleave','drop'].forEach(ev => dropZone.addEventListener(ev, () => dropZone.classList.remove('dragging')));
dropZone.addEventListener('drop', e => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) readLocalImage(file);
});
function readLocalImage(file) {
const reader = new FileReader();
reader.onload = e => {
clearBgActive();
currentBg = e.target.result;
bgIsImage = true;
applyStyle();
};
reader.readAsDataURL(file);
}
/* Gallery upload / delete */
document.getElementById('bgUploadInput').addEventListener('change', function () {
const file = this.files[0];
if (!file) return;
const icon = document.getElementById('bgUploadIcon');
icon.className = 'bi bi-arrow-clockwise ig-spin';
const fd = new FormData();
fd.append('action', 'upload_bg');
fd.append('app', CFG.app);
fd.append('bg_image', file);
fetch(window.location.pathname + '?app=' + encodeURIComponent(CFG.app), {
method: 'POST', headers: { 'X-CSRF-Token': CSRF_TOKEN }, body: fd,
})
.then(r => r.json())
.then(data => {
if (data.status !== 'success') { alert(data.message); return; }
document.getElementById('noPresetsHint')?.remove();
const list = document.getElementById('bgPresetList');
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ig-img-preset';
btn.dataset.img = data.url;
btn.dataset.id = data.id;
btn.style.backgroundImage = "url('" + data.url + "')";
btn.innerHTML = '×';
bindImgPreset(btn);
list.appendChild(btn);
const count = document.getElementById('bgCount');
count.textContent = (parseInt(count.textContent, 10) || 0) + 1;
btn.click();
})
.catch(() => alert('Erro ao guardar a imagem.'))
.finally(() => { icon.className = 'bi bi-plus-circle'; this.value = ''; });
});
function deleteBg(ev, id) {
ev.stopPropagation();
if (!confirm('Eliminar este fundo da galeria?')) return;
const fd = new FormData();
fd.append('action', 'delete_bg');
fd.append('app', CFG.app);
fd.append('bg_id', id);
fetch(window.location.pathname + '?app=' + encodeURIComponent(CFG.app), {
method: 'POST', headers: { 'X-CSRF-Token': CSRF_TOKEN }, body: fd,
}).then(() => {
document.querySelector('.ig-img-preset[data-id="' + id + '"]')?.remove();
const count = document.getElementById('bgCount');
count.textContent = Math.max(0, (parseInt(count.textContent, 10) || 1) - 1);
});
}
/* ── Export ────────────────────────────────────────────────── */
function exportFileName(n) {
const tpl = currentTemplate().slug;
const date = new Date().toISOString().slice(0, 10);
return `${CFG.app}_${tpl}_${date}_${n}.jpg`;
}
function blurOpts() {
const { bg, isImage } = effectiveBg();
if (!isImage || !(blurAmount > 0)) return null;
return { bgEl: document.getElementById('photoBg'), photoUrl: bg, amount: blurAmount };
}
async function exportCurrent(btn) {
const { w } = formatDims();
btn.disabled = true;
try {
const blob = await SocialEngine.renderBlob(document.getElementById('postPreview'), w, { blur: blurOpts() });
SocialEngine.saveBlob(blob, exportFileName(currentSlide + 1));
} catch (err) {
console.error(err);
alert('Erro ao gerar a imagem: ' + (err?.message || err));
} finally {
btn.disabled = false;
}
}
async function exportAll(btn) {
const { w } = formatDims();
const original = currentSlide;
btn.disabled = true;
try {
const files = [];
for (let i = 0; i < slides.length; i++) {
currentSlide = i;
renderSlideNav();
updateSlideVisuals();
// Give the DOM a frame to settle before capturing.
await new Promise(r => requestAnimationFrame(() => setTimeout(r, 60)));
const blob = await SocialEngine.renderBlob(document.getElementById('postPreview'), w, { blur: blurOpts() });
files.push({ blob, name: exportFileName(i + 1) });
}
const date = new Date().toISOString().slice(0, 10);
await SocialEngine.saveAll(files, `${CFG.app}_${currentTemplate().slug}_${date}.zip`);
} catch (err) {
console.error(err);
alert('Erro ao exportar: ' + (err?.message || err));
} finally {
currentSlide = original;
renderSlideNav();
updateSlideVisuals();
btn.disabled = false;
}
}
/* ── Screenshot mode ───────────────────────────────────────── */
function enterScreenshotMode() {
const { w, h } = formatDims();
const dest = document.getElementById('screenshotPost');
const vw = window.innerWidth, vh = window.innerHeight;
if (vh * w / h <= vw) {
dest.style.height = '100dvh';
dest.style.width = `calc(100dvh * ${w} / ${h})`;
} else {
dest.style.width = '100vw';
dest.style.height = `calc(100vw * ${h} / ${w})`;
}
dest.innerHTML = '';
const clone = document.getElementById('postPreview').cloneNode(true);
clone.removeAttribute('id');
clone.style.aspectRatio = w + ' / ' + h;
dest.appendChild(clone);
const overlay = document.getElementById('screenshotOverlay');
const closeBtn = document.getElementById('screenshotCloseBtn');
overlay.style.display = 'flex';
document.body.style.overflow = 'hidden';
closeBtn.style.opacity = '1';
clearTimeout(window._scTimer);
window._scTimer = setTimeout(() => { closeBtn.style.opacity = '0'; }, 2000);
}
function exitScreenshotMode() {
document.getElementById('screenshotOverlay').style.display = 'none';
document.body.style.overflow = '';
}
document.getElementById('screenshotOverlay').addEventListener('click', function (e) {
if (e.target === this) exitScreenshotMode();
});
/* ── Init ──────────────────────────────────────────────────── */
renderPresets('gradientList', CFG.presets.gradients, pickBgPreset);
renderPresets('solidList', CFG.presets.solids, pickBgPreset);
renderPresets('ovGradientList', CFG.presets.gradients, pickOverlayPreset);
renderPresets('ovSolidList', CFG.presets.solids, pickOverlayPreset);
document.querySelectorAll('#bgPresetList .ig-img-preset').forEach(bindImgPreset);
// Default background: first brand gradient.
const firstGrad = document.querySelector('#gradientList .ig-preset');
if (firstGrad) { firstGrad.classList.add('active'); currentBg = firstGrad.dataset.bg; }
buildCatFilter();
renderDynList();
initGalleryAccordion('bgGalleryList', 'bg');
initGalleryAccordion('photoPicker', 'multi');
if (!(CFG.dynamic || []).length) addItem(); // empty manual row only without dynamic source
onTemplateChange(); // sets heading visibility + builds slides
onFormatChange(); // sets aspect ratio
applyStyle();