/**
 * Kramar — Email Builder
 *
 * 3-panel drag-and-drop email template editor.
 * Palette (left) | Canvas (center) | Settings (right)
 *
 * @package Kramar
 * @since   1.0.0
 */

(function () {
	'use strict';

	var cfg = window.kramarEmailBuilder || {};
	var ajaxUrl     = cfg.ajaxUrl    || '';
	var nonce       = cfg.nonce      || '';
	var emailId     = cfg.emailId    || '';
	var isTemplate  = !!cfg.isTemplate;
	var blockTypes  = cfg.blockTypes || {};
	var mergeFields = cfg.mergeFields || {};
	var i18n        = cfg.i18n       || {};
	var globalSettings = cfg.globalSettings || {};

	function getAccent() { return globalSettings.accent_color || '#7f54b3'; }
	function getBodyBg() { return globalSettings.body_bg || '#f7f7f7'; }
	function getContentBg() { return globalSettings.content_bg || '#ffffff'; }
	function getTextColor() { return globalSettings.text_color || '#525252'; }

	// ─── Hex colour helpers ───────────────────────────────────────────────

	// Accepts #rgb / #rrggbb with or without the leading #, any case.
	function normalizeHex(raw) {
		var v = String(raw == null ? '' : raw).trim().replace(/^#/, '').toLowerCase();
		if (!/^([0-9a-f]{3}|[0-9a-f]{6})$/.test(v)) return null;
		if (v.length === 3) v = v[0] + v[0] + v[1] + v[1] + v[2] + v[2];
		return '#' + v;
	}

	// Wire a hex text input + its swatch so either one drives the other.
	// onCommit(hex) fires only for a valid colour; onLive(hex) fires while typing.
	function bindHexControl(wrap, onLive, onCommit) {
		if (!wrap) return;
		var hex = wrap.querySelector('.kramar-eb-hex__input');
		var swatch = wrap.querySelector('.kramar-eb-hex__swatch');
		if (!hex) return;

		function markValid(ok) { wrap.classList.toggle('kramar-eb-hex--invalid', !ok); }

		hex.addEventListener('input', function () {
			var v = normalizeHex(this.value);
			markValid(!!v || this.value.trim() === '');
			if (!v) return;
			if (swatch) swatch.value = v;
			if (onLive) onLive(v);
		});

		// On blur/Enter: snap the field to the canonical form, or restore the
		// last good value so the input can never be left holding junk.
		function settle() {
			var v = normalizeHex(hex.value);
			if (!v) {
				hex.value = swatch ? swatch.value : '';
				markValid(true);
				if (onLive) onLive(hex.value);
				return;
			}
			hex.value = v;
			markValid(true);
			if (swatch) swatch.value = v;
			if (onCommit) onCommit(v);
		}
		hex.addEventListener('blur', settle);
		hex.addEventListener('keydown', function (e) {
			if (e.key === 'Enter') { e.preventDefault(); settle(); }
		});

		if (swatch) {
			swatch.addEventListener('input', function () {
				hex.value = this.value;
				markValid(true);
				if (onLive) onLive(this.value);
			});
			swatch.addEventListener('change', function () {
				hex.value = this.value;
				if (onCommit) onCommit(this.value);
			});
		}
	}

	// ─── State ────────────────────────────────────────────────────────────

	var state = {
		blocks: [],
		selectedBlockId: null,
		dirty: false,
		undoStack: [],
		redoStack: [],
		rendered: {},      // blockId -> server-rendered HTML (real email markup)
		// Colours the user explicitly clicked "Override" on: "blockId:key".
		// Without this, overriding to a value that happens to equal the global
		// default is instantly collapsed back to "using global" below, so the
		// Override button appears to do nothing.
		forcedColorOverrides: {},
		renderTimer: null,
		renderSeq: 0,
	};

	// ─── Init ──────────────────────────────────────────────────────────────

	function init() {
		var raw = cfg.blocks;
		state.blocks = Array.isArray(raw) ? raw.slice() : [];

		renderCanvas();
		bindToolbar();
		bindPalette();
		bindCanvasDelegated();
		bindKeyboard();
		bindBeforeUnload();
		bindPaletteSearch();

		if (isTemplate) {
			var subjectRow = document.querySelector('.kramar-eb-canvas__subject');
			if (subjectRow) subjectRow.style.display = 'none';
			bindGlobalColors();
		}

		var firstEditable = null;
		for (var bi = 0; bi < state.blocks.length; bi++) {
			if (!isLockedBlock(state.blocks[bi])) { firstEditable = state.blocks[bi]; break; }
		}
		if (firstEditable) {
			selectBlock(firstEditable.id);
		}
	}

	// ─── ID Generation ────────────────────────────────────────────────────

	function uid() {
		return 'b' + Math.random().toString(36).slice(2, 9) + Date.now().toString(36);
	}

	// ─── Snapshot / Undo ─────────────────────────────────────────────────

	function snapshot() {
		state.undoStack.push(JSON.stringify(state.blocks));
		if (state.undoStack.length > 50) {
			state.undoStack.shift();
		}
		state.redoStack = [];
	}

	function undo() {
		if (!state.undoStack.length) return;
		state.redoStack.push(JSON.stringify(state.blocks));
		state.blocks = JSON.parse(state.undoStack.pop());
		state.selectedBlockId = null;
		renderCanvas();
		renderSettings(null);
		markDirty();
	}

	function redo() {
		if (!state.redoStack.length) return;
		state.undoStack.push(JSON.stringify(state.blocks));
		state.blocks = JSON.parse(state.redoStack.pop());
		state.selectedBlockId = null;
		renderCanvas();
		renderSettings(null);
		markDirty();
	}

	// ─── Dirty flag ───────────────────────────────────────────────────────

	function markDirty() {
		state.dirty = true;
		var btn = document.querySelector('.kramar-eb-toolbar__save');
		if (btn) btn.classList.add('kramar-eb-toolbar__save--dirty');
	}

	function clearDirty() {
		state.dirty = false;
		var btn = document.querySelector('.kramar-eb-toolbar__save');
		if (btn) btn.classList.remove('kramar-eb-toolbar__save--dirty');
	}

	// ─── Block helpers ────────────────────────────────────────────────────

	function findBlock(id) {
		for (var i = 0; i < state.blocks.length; i++) {
			if (state.blocks[i].id === id) return state.blocks[i];
		}
		return null;
	}

	function findBlockIndex(id) {
		for (var i = 0; i < state.blocks.length; i++) {
			if (state.blocks[i].id === id) return i;
		}
		return -1;
	}

	function createBlock(type) {
		var def = blockTypes[type] || {};
		var defaults = def.defaults || {};
		var settings = {};
		for (var k in defaults) {
			if (Object.prototype.hasOwnProperty.call(defaults, k)) {
				settings[k] = defaults[k];
			}
		}
		return {
			id: uid(),
			type: type,
			settings: settings,
			visibility: { type: 'always', field: '', value: '' },
		};
	}

	// ─── Canvas Render ────────────────────────────────────────────────────

	// Paint the canvas from current state + the server-rendered HTML cache,
	// then (debounced) ask the server to refresh that cache so the canvas is a
	// faithful WYSIWYG of the sent email — one render path, no JS reimplementation.
	function renderCanvas() {
		paintCanvas();
		scheduleRender();
	}

	function paintCanvas() {
		var container = document.getElementById('kramar-eb-canvas-container');
		var empty     = document.getElementById('kramar-eb-canvas-empty');
		if (!container) return;

		if (!state.blocks.length) {
			container.innerHTML = '';
			if (empty) empty.style.display = '';
			return;
		}

		if (empty) empty.style.display = 'none';

		var html = '';
		for (var i = 0; i < state.blocks.length; i++) {
			html += renderBlockHTML(state.blocks[i]);
		}
		container.innerHTML = html;

		// Apply global colors to canvas.
		var wrapper = container.closest('.kramar-eb-canvas__email-wrapper');
		if (wrapper) wrapper.style.background = getBodyBg();
		var card = container.closest('.kramar-eb-canvas__email-card');
		if (card) card.style.background = getContentBg();
		// Body text colour cascades to any block that doesn't set its own.
		container.style.color = getTextColor();

		// Re-apply selected state without triggering re-render.
		if (state.selectedBlockId) {
			var el = container.querySelector('[data-block-id="' + state.selectedBlockId + '"]');
			if (el) el.classList.add('kramar-eb-block--selected');
		}
	}

	// ─── Server Render (single source of truth) ───────────────────────────

	function scheduleRender() {
		if (state.renderTimer) { clearTimeout(state.renderTimer); }
		state.renderTimer = setTimeout(doRender, 220);
	}

	function doRender() {
		state.renderTimer = null;
		var seq = ++state.renderSeq;

		var body = new FormData();
		body.append('action', 'kramar_email_render');
		body.append('_ajax_nonce', nonce);
		body.append('blocks', JSON.stringify(state.blocks));
		// Lets the canvas resolve {email_heading} the way Preview and the sent
		// email already do — without it the canvas showed the literal tag.
		body.append('email_id', emailId);

		fetch(ajaxUrl, { method: 'POST', body: body })
			.then(function (r) { return r.json(); })
			.then(function (r) {
				if (seq !== state.renderSeq) return; // superseded by a newer render
				if (r && r.success && r.data && r.data.blocks) {
					var map = {};
					for (var i = 0; i < r.data.blocks.length; i++) {
						map[r.data.blocks[i].id] = r.data.blocks[i].html;
					}
					state.rendered = map;
					paintCanvas();
				}
			})
			.catch(function () { /* keep last good render */ });
	}

	function isLockedBlock(block) {
		if (block.type === 'content-placeholder') return true;
		if (isTemplate) return false;
		return !!block._tpl;
	}

	function getPlaceholderIndex() {
		for (var i = 0; i < state.blocks.length; i++) {
			if (state.blocks[i].type === 'content-placeholder') return i;
		}
		return -1;
	}

	function renderBlockHTML(block) {
		var locked = isLockedBlock(block);
		var selectedClass = (!locked && block.id === state.selectedBlockId) ? ' kramar-eb-block--selected' : '';
		var lockedClass = locked ? ' kramar-eb-block--locked' : '';

		if (locked) {
			return (
				'<div class="kramar-eb-block kramar-eb-block--' + esc(block.type) + lockedClass + '" data-block-id="' + esc(block.id) + '">' +
					'<div class="kramar-eb-block__content">' +
						renderBlockContent(block) +
					'</div>' +
				'</div>'
			);
		}

		return (
			'<div class="kramar-eb-block kramar-eb-block--' + esc(block.type) + selectedClass + '" data-block-id="' + esc(block.id) + '">' +
				'<div class="kramar-eb-block__actions">' +
					'<span class="dashicons dashicons-move kramar-eb-block__handle" role="button" tabindex="0" aria-label="' + esc(i18n.dragToReorder || 'Drag to Reorder') + '" title="' + esc(i18n.dragToReorder || 'Drag to Reorder') + '"></span>' +
					'<span class="dashicons dashicons-admin-page kramar-eb-block__duplicate" role="button" tabindex="0" aria-label="' + esc(i18n.duplicate || 'Duplicate') + '" title="' + esc(i18n.duplicate || 'Duplicate') + '"></span>' +
					'<span class="dashicons dashicons-trash kramar-eb-block__delete" role="button" tabindex="0" aria-label="' + esc(i18n.deleteBlock || 'Delete') + '" title="' + esc(i18n.deleteBlock || 'Delete') + '"></span>' +
				'</div>' +
				'<div class="kramar-eb-block__content">' +
					renderBlockContent(block) +
				'</div>' +
				'<div class="kramar-eb-block__hint">' + esc(i18n.clickToEdit || 'Click to Edit') + ' &middot; ' + esc(i18n.dragToReorder || 'Drag to Reorder') + '</div>' +
			'</div>'
		);
	}

	// Block content is the real email HTML, rendered server-side (see doRender)
	// so the canvas is a faithful WYSIWYG of the sent email. content-placeholder
	// is a builder-only marker with no server output.
	function renderBlockContent(block) {
		if (block.type === 'content-placeholder') {
			return '<div class="kramar-eb-content-placeholder">' +
				'<span class="kramar-eb-content-placeholder__line"></span>' +
				'<span class="kramar-eb-content-placeholder__label">' + esc(i18n.emailContent || 'Email content') + '</span>' +
				'<span class="kramar-eb-content-placeholder__line"></span>' +
				'</div>';
		}

		if (Object.prototype.hasOwnProperty.call(state.rendered, block.id)) {
			var html = state.rendered[block.id];
			if (html && html.replace(/\s/g, '') !== '') {
				return html;
			}
			return '<div class="kramar-eb-block__placeholder">' + esc(blockLabel(block.type)) +
				' <span style="opacity:.6;">(' + esc(i18n.emptyBlock || 'nothing to show for this order') + ')</span></div>';
		}

		return '<div class="kramar-eb-block__placeholder">' + esc(blockLabel(block.type)) + ' — ' + esc(i18n.rendering || 'Rendering…') + '</div>';
	}

	function blockLabel(type) {
		var def = blockTypes[type] || {};
		return def.label || type;
	}

	// ─── Settings Panel ───────────────────────────────────────────────────

	function selectBlock(id) {
		state.selectedBlockId = id;
		var container = document.getElementById('kramar-eb-canvas-container');
		if (container) {
			var all = container.querySelectorAll('.kramar-eb-block');
			for (var i = 0; i < all.length; i++) {
				all[i].classList.remove('kramar-eb-block--selected');
			}
			if (id) {
				var el = container.querySelector('[data-block-id="' + id + '"]');
				if (el) el.classList.add('kramar-eb-block--selected');
			}
		}
		renderSettings(id ? findBlock(id) : null);
	}

	function renderSettings(block) {
		var title = document.getElementById('kramar-eb-settings-title');
		var body  = document.getElementById('kramar-eb-settings-body');
		if (!body) return;

		if (!block) {
			if (title) title.textContent = 'Block Settings';
			body.innerHTML = '<div class="kramar-eb-settings__empty"><span class="dashicons dashicons-admin-generic"></span><p>' + esc(i18n.noBlockSelected || 'Select a block to edit its settings.') + '</p></div>';
			return;
		}

		var def = blockTypes[block.type] || {};
		if (title) title.textContent = (def.label || block.type) + ' Settings';

		body.innerHTML = renderSettingsForm(block);
		bindSettingsFields(block.id);
	}

	function renderSettingsForm(block) {
		var s = block.settings || {};
		var html = '<div class="kramar-eb-settings-form" data-block-id="' + esc(block.id) + '">';

		switch (block.type) {
			case 'header':
				var brandLogo = (window.kramarEmailBuilder && window.kramarEmailBuilder.brandLogo) || '';
				var settingsUrl = (window.kramarEmailBuilder && window.kramarEmailBuilder.settingsUrl) || '';
				html += '<div class="kramar-eb-field">' +
					'<label class="kramar-eb-field__label">Logo</label>' +
					(brandLogo
						? '<div style="margin:0 0 6px;"><img src="' + esc(brandLogo) + '" style="max-width:120px;max-height:50px;" /></div>'
						: '') +
					'<p class="kramar-eb-field__hint" style="margin:0;">Set globally in <a href="' + esc(settingsUrl) + '">Settings → Business Details</a>. Used on all emails &amp; invoices.</p>' +
				'</div>';
				html += field('text', 'store_name', 'Store Name (shown if no logo)', s.store_name || '', block.id);
				html += fieldSelect('align', 'Alignment', s.align || 'center', [['left','Left'],['center','Center'],['right','Right']], block.id);
				break;

			case 'heading':
				html += field('text', 'text', 'Heading Text', s.text || '', block.id);
				html += fieldSelect('level', 'Heading Level', s.level || 'h2', [['h2','H2'],['h3','H3'],['h4','H4']], block.id);
				html += fieldSelect('align', 'Alignment', s.align || 'left', [['left','Left'],['center','Center'],['right','Right']], block.id);
				html += fieldColor('color', 'Text Color', s.color || '', block.id);
				break;

			case 'text':
				html += '<div class="kramar-eb-field">' +
					'<label class="kramar-eb-field__label">Content</label>' +
					'<textarea class="kramar-eb-field__input kramar-eb-field__textarea" data-field="content" rows="5">' + esc(s.content || '') + '</textarea>' +
				'</div>';
				html += mergeFieldDropdown(block.id);
				html += fieldSelect('size', 'Size', s.size || 'normal', [['small','Small (12px)'],['normal','Normal (14px)'],['large','Large (16px)']], block.id);
				html += fieldColor('color', 'Text Color', s.color || '', block.id);
				html += fieldColor('bg', 'Background Color', s.bg || '', block.id);
				break;

			case 'image':
				html += '<div class="kramar-eb-field">' +
					'<label class="kramar-eb-field__label">Image</label>' +
					'<input type="hidden" class="kramar-eb-field__input" data-field="url" value="' + esc(s.url || '') + '" />';
				if (s.url) {
					html += '<div class="kramar-eb-image-preview">' +
						'<img src="' + esc(s.url) + '" />' +
						'<div class="kramar-eb-image-preview__actions">' +
							'<button type="button" class="button kramar-eb-upload-image">Replace</button>' +
							'<button type="button" class="kramar-eb-image-remove">Remove</button>' +
						'</div>' +
					'</div>';
				} else {
					html += '<div class="kramar-eb-image-empty">' +
						'<span class="dashicons dashicons-format-image"></span>' +
						'<button type="button" class="button kramar-eb-upload-image">Select Image</button>' +
					'</div>';
				}
				html += '</div>';
				html += field('text', 'alt', 'Alt Text', s.alt || '', block.id);
				html += field('text', 'link', 'Link URL', s.link || '', block.id);
				html += field('text', 'width', 'Width', s.width || '100%', block.id);
				html += fieldSelect('align', 'Alignment', s.align || 'center', [['left','Left'],['center','Center'],['right','Right']], block.id);
				break;

			case 'button':
				html += field('text', 'text', 'Button Label', s.text || 'Click here', block.id);
				html += field('url', 'url', 'URL', s.url || '', block.id);
				html += fieldColor('bg', 'Background Color', s.bg || '', block.id);
				html += fieldColor('color', 'Text Color', s.color || '', block.id);
				html += fieldSelect('align', 'Alignment', s.align || 'center', [['left','Left'],['center','Center'],['right','Right']], block.id);
				html += field('number', 'radius', 'Border Radius (px)', s.radius !== undefined ? s.radius : '4', block.id);
				break;

			case 'notice':
				html += '<div class="kramar-eb-field">' +
					'<label class="kramar-eb-field__label">Content</label>' +
					'<textarea class="kramar-eb-field__input kramar-eb-field__textarea" data-field="content" rows="3">' + esc(s.content || '') + '</textarea>' +
				'</div>';
				html += fieldSelect('style', 'Style', s.style || 'info', [['info','Info'],['warning','Warning'],['error','Error'],['success','Success']], block.id);
				break;

			case 'divider':
				html += fieldColor('color', 'Line Color', s.color || '', block.id);
				html += field('text', 'width', 'Width', s.width || '100%', block.id);
				html += field('number', 'margin', 'Margin (px)', s.margin || '16', block.id);
				break;

			case 'spacer':
				html += field('number', 'height', 'Height (px)', s.height !== undefined ? s.height : '24', block.id);
				html += fieldColor('bg_color', 'Background Color', s.bg_color || '', block.id);
				break;

			case 'raw-html':
				html += '<div class="kramar-eb-field">' +
					'<label class="kramar-eb-field__label">HTML Content</label>' +
					'<textarea class="kramar-eb-field__input kramar-eb-field__textarea" data-field="content" rows="8" style="font-family:monospace;font-size:12px;">' + esc(s.content || '') + '</textarea>' +
					'<p class="kramar-eb-field__hint" style="margin:4px 0 0;font-size:11px;color:#a0aec0;">HTML is sanitised on output. Scripts and iframes are stripped.</p>' +
				'</div>';
				break;

			case 'order-table':
				html += fieldToggle('show_image', 'Show Product Images', s.show_image !== false, block.id);
				html += field('number', 'image_size', 'Image Size (px)', s.image_size || '32', block.id);
				html += fieldToggle('show_sku', 'Show SKU Column', !!s.show_sku, block.id);
				html += fieldToggle('show_totals', 'Show Totals Section', s.show_totals !== false, block.id);
				break;

			case 'items-table':
				html += '<p style="margin:4px 0;font-size:12px;color:#718096;">This block automatically lists the cart / product items (name, quantity, total) when the email is sent. No settings needed.</p>';
				break;

			case 'addresses':
				html += fieldSelect('show', 'Show Addresses', s.show || 'both', [['both','Both'],['billing','Billing only'],['shipping','Shipping only']], block.id);
				break;

			case 'data-field':
				html += field('text', 'label', 'Label', s.label || '', block.id);
				html += mergeFieldSelect('field', 'Merge Field', s.field || '{order_number}');
				break;

			case 'social':
				var socialPlatforms = ['facebook','instagram','linkedin','x','youtube','tiktok'];
				var socialLabels    = { facebook:'Facebook', instagram:'Instagram', linkedin:'LinkedIn', x:'X (Twitter)', youtube:'YouTube', tiktok:'TikTok' };
				for (var pi = 0; pi < socialPlatforms.length; pi++) {
					var pk = socialPlatforms[pi];
					html += field('url', pk, socialLabels[pk] + ' URL', s[pk] || '', block.id);
				}
				break;

			case 'footer':
				var defaultFooter = 'Company Name · KVK: 00000000 · BTW: NL000000000B01\nStreet 1, 1000 AA City\ninfo@example.com · +31 00 0000000';
				html += '<div class="kramar-eb-field">' +
					'<label class="kramar-eb-field__label">Footer Content</label>' +
					'<textarea class="kramar-eb-field__input kramar-eb-field__textarea" data-field="content" rows="5" placeholder="' + esc(defaultFooter) + '">' + esc(s.content || '') + '</textarea>' +
					'<p style="margin:4px 0 0;font-size:11px;color:#a0aec0;">Leave blank to auto-generate from Business Details. Each line is a separate row.</p>' +
				'</div>';
				break;
		}

		html += renderVisibilitySettings(block);
		html += '</div>';
		return html;
	}

	function field(type, key, label, value, blockId) {
		if (type === 'color') return fieldColor(key, label, value, blockId);
		var inputType = type === 'number' ? 'number' : (type === 'url' ? 'url' : 'text');
		return '<div class="kramar-eb-field">' +
			'<label class="kramar-eb-field__label">' + esc(label) + '</label>' +
			'<input type="' + inputType + '" class="kramar-eb-field__input" data-field="' + esc(key) + '" value="' + esc(value) + '" />' +
		'</div>';
	}

	var COLOR_DEFAULTS = {
		'button.bg':    { get: getAccent,                          source: 'Accent' },
		'button.color': { get: function () { return '#ffffff'; },  source: 'White' },
		'heading.color':{ get: function () { return '#1a1a1a'; },  source: 'Default' },
		'text.color':   { get: getTextColor,                       source: 'Text' },
		'divider.color':{ get: function () { return '#e4e4e7'; },  source: 'Default' },
	};

	function fieldColor(key, label, rawValue, blockId) {
		var block = findBlock(blockId);
		var blockType = block ? block.type : '';
		var def = COLOR_DEFAULTS[blockType + '.' + key] || null;
		var hasGlobal = !!def;
		var forced = !!state.forcedColorOverrides[blockId + ':' + key];
		// Treat stored value that matches global default as "global" — unless the
		// user explicitly chose to override it.
		if (!forced && rawValue && hasGlobal && rawValue.toLowerCase() === def.get().toLowerCase()) {
			rawValue = '';
			if (block) block.settings[key] = '';
		}
		var isUsingGlobal = !rawValue && hasGlobal;
		var effectiveColor = rawValue || (def ? def.get() : '');
		var sourceLabel = def ? def.source : 'None';

		// No value + no global default = optional color (e.g. text bg). Show "None / Add".
		if (!rawValue && !hasGlobal) {
			return '<div class="kramar-eb-field kramar-eb-color-field" data-color-key="' + esc(key) + '">' +
				'<label class="kramar-eb-field__label">' + esc(label) + '</label>' +
				'<div class="kramar-eb-color-global">' +
					'<span class="kramar-eb-color-source">None</span>' +
					'<button type="button" class="kramar-eb-color-override">Add</button>' +
				'</div>' +
				'<input type="hidden" class="kramar-eb-field__input" data-field="' + esc(key) + '" value="" />' +
			'</div>';
		}

		// Using global default — show swatch + source label + Override button.
		if (isUsingGlobal) {
			return '<div class="kramar-eb-field kramar-eb-color-field" data-color-key="' + esc(key) + '">' +
				'<label class="kramar-eb-field__label">' + esc(label) + '</label>' +
				'<div class="kramar-eb-color-global">' +
					'<span class="kramar-eb-color-swatch" style="background:' + esc(effectiveColor) + ';"></span>' +
					'<span class="kramar-eb-color-source">' + esc(sourceLabel) + '</span>' +
					'<button type="button" class="kramar-eb-color-override">Override</button>' +
				'</div>' +
				'<input type="hidden" class="kramar-eb-field__input" data-field="' + esc(key) + '" value="" />' +
			'</div>';
		}

		// Custom value — show picker + Reset (if there's a global to reset to) or Remove.
		var resetLabel = hasGlobal ? 'Reset' : 'Remove';
		return '<div class="kramar-eb-field kramar-eb-color-field kramar-eb-color-field--custom" data-color-key="' + esc(key) + '">' +
			'<label class="kramar-eb-field__label">' + esc(label) + '</label>' +
			'<div class="kramar-eb-color-custom">' +
				'<span class="kramar-eb-hex">' +
					'<input type="color" class="kramar-eb-hex__swatch" tabindex="-1" aria-hidden="true" value="' + esc(effectiveColor || '#000000') + '" />' +
					'<input type="text" class="kramar-eb-field__input kramar-eb-hex__input" data-field="' + esc(key) + '" value="' + esc(effectiveColor || '#000000') + '" spellcheck="false" autocomplete="off" maxlength="7" aria-label="' + esc(label) + '" />' +
				'</span>' +
				'<button type="button" class="kramar-eb-color-reset">' + esc(resetLabel) + '</button>' +
			'</div>' +
		'</div>';
	}

	function fieldSelect(key, label, value, options, blockId) {
		var opts = '';
		for (var i = 0; i < options.length; i++) {
			var selected = (options[i][0] === value) ? ' selected' : '';
			opts += '<option value="' + esc(options[i][0]) + '"' + selected + '>' + esc(options[i][1]) + '</option>';
		}
		return '<div class="kramar-eb-field">' +
			'<label class="kramar-eb-field__label">' + esc(label) + '</label>' +
			'<select class="kramar-eb-field__input kramar-eb-field__select" data-field="' + esc(key) + '">' + opts + '</select>' +
		'</div>';
	}

	function fieldToggle(key, label, checked, blockId) {
		return '<div class="kramar-eb-field kramar-eb-field--toggle">' +
			'<label class="kramar-eb-field__toggle-label">' +
				'<input type="checkbox" class="kramar-eb-field__toggle" data-field="' + esc(key) + '"' + (checked ? ' checked' : '') + ' />' +
				'<span>' + esc(label) + '</span>' +
			'</label>' +
		'</div>';
	}

	function mergeFieldSelect(key, label, currentValue) {
		var items = [];
		for (var cat in mergeFields) {
			if (!Object.prototype.hasOwnProperty.call(mergeFields, cat)) continue;
			var group = mergeFields[cat];
			for (var tag in group.fields) {
				if (!Object.prototype.hasOwnProperty.call(group.fields, tag)) continue;
				items.push({ tag: tag, label: group.fields[tag], category: group.label || cat });
			}
		}

		var currentLabel = currentValue;
		for (var ci = 0; ci < items.length; ci++) {
			if (items[ci].tag === currentValue) { currentLabel = items[ci].label; break; }
		}

		var listHtml = '';
		var lastCat = '';
		for (var idx = 0; idx < items.length; idx++) {
			var item = items[idx];
			if (item.category !== lastCat) {
				lastCat = item.category;
				listHtml += '<div class="kramar-eb-merge-category">' + esc(item.category) + '</div>';
			}
			var activeClass = item.tag === currentValue ? ' kramar-eb-merge-option--active' : '';
			listHtml += '<div class="kramar-eb-merge-option' + activeClass + '" data-tag="' + esc(item.tag) + '" data-search="' + esc((item.label + ' ' + item.tag).toLowerCase()) + '">' +
				'<span class="kramar-eb-merge-option__label">' + esc(item.label) + '</span>' +
				'<code class="kramar-eb-merge-option__tag">' + esc(item.tag) + '</code>' +
			'</div>';
		}

		return '<div class="kramar-eb-field kramar-eb-merge-select-field">' +
			'<label class="kramar-eb-field__label">' + esc(label) + '</label>' +
			'<div class="kramar-eb-merge-dropdown-wrap">' +
				'<input type="text" class="kramar-eb-field__input kramar-eb-merge-select-search" data-field="' + esc(key) + '" value="' + esc(currentLabel) + '" data-tag-value="' + esc(currentValue) + '" autocomplete="off" placeholder="Search fields..." />' +
				'<div class="kramar-eb-merge-dropdown" style="display:none;">' + listHtml + '</div>' +
			'</div>' +
		'</div>';
	}

	function mergeFieldDropdown(blockId) {
		var items = [];
		for (var cat in mergeFields) {
			if (!Object.prototype.hasOwnProperty.call(mergeFields, cat)) continue;
			var group = mergeFields[cat];
			for (var tag in group.fields) {
				if (!Object.prototype.hasOwnProperty.call(group.fields, tag)) continue;
				items.push({ tag: tag, label: group.fields[tag], category: group.label || cat });
			}
		}

		var listHtml = '';
		var lastCat = '';
		for (var idx = 0; idx < items.length; idx++) {
			var item = items[idx];
			if (item.category !== lastCat) {
				lastCat = item.category;
				listHtml += '<div class="kramar-eb-merge-category">' + esc(item.category) + '</div>';
			}
			listHtml += '<div class="kramar-eb-merge-option" data-tag="' + esc(item.tag) + '" data-search="' + esc((item.label + ' ' + item.tag).toLowerCase()) + '">' +
				'<span class="kramar-eb-merge-option__label">' + esc(item.label) + '</span>' +
				'<code class="kramar-eb-merge-option__tag">' + esc(item.tag) + '</code>' +
			'</div>';
		}

		return '<div class="kramar-eb-field kramar-eb-merge-insert">' +
			'<label class="kramar-eb-field__label">' + esc(i18n.insertMergeField || 'Insert merge field') + '</label>' +
			'<div class="kramar-eb-merge-dropdown-wrap">' +
				'<input type="text" class="kramar-eb-field__input kramar-eb-merge-search" placeholder="Search fields..." autocomplete="off" />' +
				'<div class="kramar-eb-merge-dropdown" style="display:none;">' + listHtml + '</div>' +
			'</div>' +
		'</div>';
	}

	function renderVisibilitySettings(block) {
		var v = block.visibility || { type: 'always', field: '', value: '' };
		var allFields = '<option value="">-- Select field --</option>';
		for (var cat in mergeFields) {
			if (!Object.prototype.hasOwnProperty.call(mergeFields, cat)) continue;
			var group = mergeFields[cat];
			for (var tag in group.fields) {
				if (!Object.prototype.hasOwnProperty.call(group.fields, tag)) continue;
				allFields += '<option value="' + esc(tag) + '"' + (v.field === tag ? ' selected' : '') + '>' + esc(group.fields[tag]) + '</option>';
			}
		}

		var showField = (v.type === 'show_if' || v.type === 'hide_if') ? '' : ' style="display:none;"';

		return '<div class="kramar-eb-visibility">' +
			'<h4 class="kramar-eb-visibility__title">Visibility</h4>' +
			'<div class="kramar-eb-field">' +
				'<select class="kramar-eb-field__input kramar-eb-visibility__type" data-visibility-field="type">' +
					'<option value="always"' + (v.type === 'always' ? ' selected' : '') + '>Always show</option>' +
					'<option value="show_if"' + (v.type === 'show_if' ? ' selected' : '') + '>Show if field has value</option>' +
					'<option value="hide_if"' + (v.type === 'hide_if' ? ' selected' : '') + '>Hide if field has value</option>' +
				'</select>' +
			'</div>' +
			'<div class="kramar-eb-visibility__field-wrap"' + showField + '>' +
				'<div class="kramar-eb-field">' +
					'<label class="kramar-eb-field__label">Field</label>' +
					'<select class="kramar-eb-field__input" data-visibility-field="field">' + allFields + '</select>' +
				'</div>' +
			'</div>' +
		'</div>';
	}

	// ─── Settings Binding ─────────────────────────────────────────────────

	function bindSettingsFields(blockId) {
		var body = document.getElementById('kramar-eb-settings-body');
		if (!body) return;

		// Regular inputs and selects.
		var inputs = body.querySelectorAll('.kramar-eb-field__input, .kramar-eb-field__textarea, .kramar-eb-field__toggle');
		for (var i = 0; i < inputs.length; i++) {
			(function (input) {
				var fieldKey = input.getAttribute('data-field');
				if (!fieldKey) return;
				var isHex = input.classList.contains('kramar-eb-hex__input');
				var evtName = (input.type === 'checkbox') ? 'change' : 'input';
				input.addEventListener(evtName, function () {
					var block = findBlock(blockId);
					if (!block) return;
					var value;
					if (input.type === 'checkbox') {
						value = input.checked;
					} else if (isHex) {
						// Half-typed hex ("#ab") must never reach block settings —
						// commit only once it parses, and mirror it to the swatch.
						var hexWrap = input.closest('.kramar-eb-hex');
						var parsed = normalizeHex(input.value);
						if (hexWrap) hexWrap.classList.toggle('kramar-eb-hex--invalid', !parsed);
						if (!parsed) return;
						var sw = hexWrap && hexWrap.querySelector('.kramar-eb-hex__swatch');
						if (sw) sw.value = parsed;
						value = parsed;
					} else {
						value = input.value;
					}
					snapshot();
					block.settings[fieldKey] = value;
					// Re-render just this block's content preview.
					refreshBlockPreview(blockId);
					markDirty();
				});
				// The swatch has no data-field of its own; it drives the hex input.
				if (isHex) {
					var wrapEl = input.closest('.kramar-eb-hex');
					var swatchEl = wrapEl && wrapEl.querySelector('.kramar-eb-hex__swatch');
					if (swatchEl) {
						swatchEl.addEventListener('input', function () {
							input.value = this.value;
							input.dispatchEvent(new Event('input', { bubbles: true }));
						});
					}
					// Restore the canonical form when focus leaves a partial entry.
					input.addEventListener('blur', function () {
						var parsed = normalizeHex(input.value);
						var wrap2 = input.closest('.kramar-eb-hex');
						var block2 = findBlock(blockId);
						if (!parsed) {
							input.value = (block2 && block2.settings[fieldKey]) || (swatchEl ? swatchEl.value : '');
						} else {
							input.value = parsed;
						}
						if (wrap2) wrap2.classList.remove('kramar-eb-hex--invalid');
					});
				}
			})(inputs[i]);
		}

		// Media library upload buttons (image blocks; logo is now global in Settings).
		var mediaButtons = body.querySelectorAll('.kramar-eb-upload-image');
		for (var mi = 0; mi < mediaButtons.length; mi++) {
			(function (btn) {
				if (typeof wp === 'undefined' || !wp.media) return;
				btn.addEventListener('click', function () {
					var isLogo = btn.classList.contains('kramar-eb-upload-logo');
					var frame = wp.media({
						title: isLogo ? 'Select Logo' : 'Select Image',
						button: { text: isLogo ? 'Use as Logo' : 'Use Image' },
						multiple: false,
						library: { type: 'image' },
					});
					frame.on('select', function () {
						var attachment = frame.state().get('selection').first().toJSON();
						var block = findBlock(blockId);
						if (!block) return;
						snapshot();
						if (isLogo) {
							block.settings.logo_url = attachment.url;
						} else {
							block.settings.url = attachment.url;
							if (!block.settings.alt && (attachment.alt || attachment.title)) {
								block.settings.alt = attachment.alt || attachment.title;
							}
						}
						renderSettings(block);
						refreshBlockPreview(blockId);
						markDirty();
					});
					frame.open();
				});
			})(mediaButtons[mi]);
		}

		// Image remove button.
		var removeBtn = body.querySelector('.kramar-eb-image-remove');
		if (removeBtn) {
			removeBtn.addEventListener('click', function () {
				var block = findBlock(blockId);
				if (!block) return;
				snapshot();
				block.settings.url = '';
				block.settings.alt = '';
				renderSettings(block);
				refreshBlockPreview(blockId);
				markDirty();
			});
		}

		// Merge field select (for data-field blocks).
		var mergeSelectSearch = body.querySelector('.kramar-eb-merge-select-search');
		var mergeSelectDropdown = mergeSelectSearch ? mergeSelectSearch.parentNode.querySelector('.kramar-eb-merge-dropdown') : null;
		if (mergeSelectSearch && mergeSelectDropdown) {
			mergeSelectSearch.addEventListener('focus', function () {
				mergeSelectDropdown.style.display = 'block';
				filterMergeOptions(mergeSelectDropdown, '');
			});
			mergeSelectSearch.addEventListener('input', function () {
				filterMergeOptions(mergeSelectDropdown, mergeSelectSearch.value.toLowerCase());
			});
			mergeSelectDropdown.addEventListener('click', function (ev) {
				var opt = closest(ev.target, '.kramar-eb-merge-option');
				if (!opt) return;
				var tag = opt.getAttribute('data-tag');
				if (!tag) return;
				var block = findBlock(blockId);
				if (block) {
					snapshot();
					block.settings.field = tag;
					var optLabel = opt.querySelector('.kramar-eb-merge-option__label');
					mergeSelectSearch.value = optLabel ? optLabel.textContent : tag;
					mergeSelectSearch.setAttribute('data-tag-value', tag);
					refreshBlockPreview(blockId);
					markDirty();
				}
				mergeSelectDropdown.style.display = 'none';
			});
			document.addEventListener('click', function (ev) {
				if (!mergeSelectSearch.contains(ev.target) && !mergeSelectDropdown.contains(ev.target)) {
					mergeSelectDropdown.style.display = 'none';
				}
			});
		}

		// Searchable merge field dropdown (for text block insert).
		var mergeSearch = body.querySelector('.kramar-eb-merge-search');
		var mergeDropdown = body.querySelector('.kramar-eb-merge-dropdown');
		if (mergeSearch && mergeDropdown) {
			mergeSearch.addEventListener('focus', function () {
				mergeDropdown.style.display = 'block';
				filterMergeOptions(mergeDropdown, '');
			});
			mergeSearch.addEventListener('input', function () {
				filterMergeOptions(mergeDropdown, mergeSearch.value.toLowerCase());
			});
			mergeDropdown.addEventListener('click', function (ev) {
				var opt = closest(ev.target, '.kramar-eb-merge-option');
				if (!opt) return;
				var tag = opt.getAttribute('data-tag');
				if (!tag) return;
				var textarea = body.querySelector('textarea[data-field="content"]');
				if (textarea) {
					insertAtCursor(textarea, tag);
					var block = findBlock(blockId);
					if (block) {
						snapshot();
						block.settings.content = textarea.value;
						refreshBlockPreview(blockId);
						markDirty();
					}
				}
				mergeSearch.value = '';
				mergeDropdown.style.display = 'none';
			});
			document.addEventListener('click', function closeDropdown(ev) {
				if (!mergeSearch.contains(ev.target) && !mergeDropdown.contains(ev.target)) {
					mergeDropdown.style.display = 'none';
				}
			});
		}

		// Visibility type select.
		var visType = body.querySelector('.kramar-eb-visibility__type');
		if (visType) {
			visType.addEventListener('change', function () {
				var block = findBlock(blockId);
				if (!block) return;
				snapshot();
				if (!block.visibility) block.visibility = { type: 'always', field: '', value: '' };
				block.visibility.type = visType.value;
				markDirty();
				var wrap = body.querySelector('.kramar-eb-visibility__field-wrap');
				if (wrap) {
					wrap.style.display = (visType.value === 'show_if' || visType.value === 'hide_if') ? '' : 'none';
				}
			});
		}

		// Color Override / Reset buttons.
		body.querySelectorAll('.kramar-eb-color-override').forEach(function (btn) {
			btn.addEventListener('click', function () {
				var wrapper = closest(btn, '.kramar-eb-color-field');
				if (!wrapper) return;
				var key = wrapper.getAttribute('data-color-key');
				var block = findBlock(blockId);
				if (!block) return;
				var def = COLOR_DEFAULTS[block.type + '.' + key];
				snapshot();
				state.forcedColorOverrides[blockId + ':' + key] = true;
				block.settings[key] = def ? def.get() : (key === 'bg' ? '#f0f0f0' : '#333333');
				renderSettings(block);
				refreshBlockPreview(blockId);
				markDirty();
			});
		});
		body.querySelectorAll('.kramar-eb-color-reset').forEach(function (btn) {
			btn.addEventListener('click', function () {
				var wrapper = closest(btn, '.kramar-eb-color-field');
				if (!wrapper) return;
				var key = wrapper.getAttribute('data-color-key');
				var block = findBlock(blockId);
				if (!block) return;
				snapshot();
				delete state.forcedColorOverrides[blockId + ':' + key];
				block.settings[key] = '';
				renderSettings(block);
				refreshBlockPreview(blockId);
				markDirty();
			});
		});

		var visField = body.querySelector('[data-visibility-field="field"]');
		if (visField) {
			visField.addEventListener('change', function () {
				var block = findBlock(blockId);
				if (!block) return;
				snapshot();
				if (!block.visibility) block.visibility = { type: 'always', field: '', value: '' };
				block.visibility.field = visField.value;
				markDirty();
			});
		}
	}

	// Settings changed — re-render this block (and the canvas) from the server.
	function refreshBlockPreview(blockId) {
		scheduleRender();
	}

	// ─── Palette ──────────────────────────────────────────────────────────

	function bindPalette() {
		var palette = document.getElementById('kramar-eb-palette');
		if (!palette) return;

		// Click to add block. Inserts after selected block, or before placeholder if nothing selected.
		palette.addEventListener('click', function (e) {
			var item = closest(e.target, '.kramar-eb-palette-item');
			if (!item) return;
			var type = item.getAttribute('data-block-type');
			if (!type) return;
			snapshot();
			var block = createBlock(type);
			var phIdx = getPlaceholderIndex();
			var selIdx = state.selectedBlockId ? findBlockIndex(state.selectedBlockId) : -1;
			if (selIdx >= 0 && state.blocks[selIdx].type !== 'content-placeholder') {
				state.blocks.splice(selIdx + 1, 0, block);
			} else if (phIdx >= 0) {
				state.blocks.splice(phIdx, 0, block);
			} else {
				state.blocks.push(block);
			}
			renderCanvas();
			selectBlock(block.id);
			markDirty();
			scrollBlockIntoView(block.id);
		});

		// Drag start from palette.
		palette.addEventListener('dragstart', function (e) {
			var item = closest(e.target, '.kramar-eb-palette-item');
			if (!item) return;
			e.dataTransfer.setData('text/plain', 'palette:' + item.getAttribute('data-block-type'));
			e.dataTransfer.effectAllowed = 'copy';
		});
	}

	function bindPaletteSearch() {
		var search = document.querySelector('.kramar-eb-palette__search');
		if (!search) return;
		search.addEventListener('input', function () {
			var q = search.value.toLowerCase().trim();
			var items = document.querySelectorAll('.kramar-eb-palette-item');
			var groups = document.querySelectorAll('.kramar-eb-palette-group');

			for (var i = 0; i < items.length; i++) {
				var label = items[i].querySelector('.kramar-eb-palette-label');
				var text  = label ? label.textContent.toLowerCase() : '';
				var type  = (items[i].getAttribute('data-block-type') || '').toLowerCase();
				var match = !q || text.indexOf(q) !== -1 || type.indexOf(q) !== -1;
				items[i].style.display = match ? '' : 'none';
			}

			for (var g = 0; g < groups.length; g++) {
				var visible = groups[g].querySelectorAll('.kramar-eb-palette-item:not([style*="display: none"])');
				groups[g].style.display = visible.length ? '' : 'none';
			}
		});
	}

	// ─── Canvas Drag and Drop ─────────────────────────────────────────────

	var dragState = {
		type: null,       // 'palette' or 'reorder'
		blockId: null,    // for reorder
		blockType: null,  // for palette
		placeholder: null,
		insertIndex: -1,
	};

	function bindCanvasDelegated() {
		var container = document.getElementById('kramar-eb-canvas-container');
		if (!container) return;

		// Click delegation.
		container.addEventListener('click', function (e) {
			// Delete.
			if (closest(e.target, '.kramar-eb-block__delete')) {
				var blockEl = closest(e.target, '.kramar-eb-block');
				if (blockEl) {
					var id = blockEl.getAttribute('data-block-id');
					deleteBlock(id);
				}
				return;
			}
			// Duplicate.
			if (closest(e.target, '.kramar-eb-block__duplicate')) {
				var blockEl2 = closest(e.target, '.kramar-eb-block');
				if (blockEl2) {
					duplicateBlock(blockEl2.getAttribute('data-block-id'));
				}
				return;
			}
			// Select block (not on action buttons, handle, or locked blocks).
			var blockEl3 = closest(e.target, '.kramar-eb-block');
			if (blockEl3 && !closest(e.target, '.kramar-eb-block__actions') && !blockEl3.classList.contains('kramar-eb-block--locked')) {
				selectBlock(blockEl3.getAttribute('data-block-id'));
			}
		});

		// Drag-over from palette or reorder.
		container.addEventListener('dragover', function (e) {
			e.preventDefault();
			e.dataTransfer.dropEffect = 'copy';
			updateDropPlaceholder(container, e.clientY);
		});

		container.addEventListener('dragleave', function (e) {
			if (!container.contains(e.relatedTarget)) {
				removePlaceholder();
				dragState.insertIndex = -1;
			}
		});

		container.addEventListener('drop', function (e) {
			e.preventDefault();
			var data = e.dataTransfer.getData('text/plain');
			removePlaceholder();

			if (data && data.indexOf('palette:') === 0) {
				var type = data.slice(8);
				snapshot();
				var block = createBlock(type);
				var idx = dragState.insertIndex >= 0 ? dragState.insertIndex : state.blocks.length;
				state.blocks.splice(idx, 0, block);
				renderCanvas();
				selectBlock(block.id);
				markDirty();
			} else if (data && data.indexOf('reorder:') === 0) {
				var id = data.slice(8);
				var fromIdx = findBlockIndex(id);
				if (fromIdx !== -1 && dragState.insertIndex !== -1) {
					snapshot();
					var toIdx = dragState.insertIndex;
					var moved = state.blocks.splice(fromIdx, 1)[0];
					if (toIdx > fromIdx) toIdx--;
					state.blocks.splice(toIdx, 0, moved);
					renderCanvas();
					selectBlock(id);
					markDirty();
				}
			}
			dragState.insertIndex = -1;
		});

		// ── Mousedown-based drag for block reorder (proven reliable) ──
		var reorderDrag = { active: false, blockId: null, ghost: null, startY: 0 };

		container.addEventListener('mousedown', function (e) {
			var handle = closest(e.target, '.kramar-eb-block__handle');
			if (!handle) return;
			e.preventDefault();

			var blockEl = closest(handle, '.kramar-eb-block');
			if (!blockEl) return;

			reorderDrag.blockId = blockEl.getAttribute('data-block-id');
			reorderDrag.startY = e.clientY;
			reorderDrag.active = false;

			var ghost = blockEl.cloneNode(true);
			ghost.style.cssText = 'position:fixed;pointer-events:none;opacity:0.7;z-index:99999;width:' + blockEl.offsetWidth + 'px;box-shadow:0 4px 20px rgba(0,0,0,.15);border-radius:6px;left:' + blockEl.getBoundingClientRect().left + 'px;top:' + e.clientY + 'px;transform:translateY(-50%);';
			reorderDrag.ghost = ghost;

			function onMove(ev) {
				if (!reorderDrag.active && Math.abs(ev.clientY - reorderDrag.startY) < 5) return;

				if (!reorderDrag.active) {
					reorderDrag.active = true;
					document.body.appendChild(ghost);
					blockEl.style.opacity = '0.3';
				}

				ghost.style.top = ev.clientY + 'px';
				updateDropPlaceholder(container, ev.clientY);
			}

			function onUp() {
				document.removeEventListener('mousemove', onMove);
				document.removeEventListener('mouseup', onUp);

				if (reorderDrag.active && reorderDrag.ghost) {
					reorderDrag.ghost.remove();
					blockEl.style.opacity = '';

					var fromIdx = findBlockIndex(reorderDrag.blockId);
					var toIdx = dragState.insertIndex;

					// Prevent dropping ON the content placeholder itself.
					var phIdx = getPlaceholderIndex();
					if (phIdx >= 0 && toIdx === phIdx) {
						toIdx = fromIdx < phIdx ? phIdx : phIdx + 1;
					}

					if (fromIdx !== -1 && toIdx !== -1 && fromIdx !== toIdx) {
						snapshot();
						var moved = state.blocks.splice(fromIdx, 1)[0];
						if (toIdx > fromIdx) toIdx--;
						state.blocks.splice(toIdx, 0, moved);
						renderCanvas();
						selectBlock(reorderDrag.blockId);
						markDirty();
					}
				}

				removePlaceholder();
				dragState.insertIndex = -1;
				reorderDrag.active = false;
				reorderDrag.blockId = null;
				reorderDrag.ghost = null;
			}

			document.addEventListener('mousemove', onMove);
			document.addEventListener('mouseup', onUp);
		});
	}

	function updateDropPlaceholder(container, clientY) {
		var blocks = container.querySelectorAll('.kramar-eb-block');
		var insertIndex = state.blocks.length;

		for (var i = 0; i < blocks.length; i++) {
			var rect = blocks[i].getBoundingClientRect();
			var mid  = rect.top + rect.height / 2;
			if (clientY < mid) {
				insertIndex = i;
				break;
			}
		}

		dragState.insertIndex = insertIndex;

		if (!dragState.placeholder) {
			dragState.placeholder = document.createElement('div');
			dragState.placeholder.className = 'kramar-eb-drop-placeholder';
		}

		if (insertIndex < blocks.length) {
			container.insertBefore(dragState.placeholder, blocks[insertIndex]);
		} else {
			container.appendChild(dragState.placeholder);
		}
	}

	function removePlaceholder() {
		if (dragState.placeholder && dragState.placeholder.parentNode) {
			dragState.placeholder.parentNode.removeChild(dragState.placeholder);
		}
		dragState.placeholder = null;
	}

	// ─── Block Actions ────────────────────────────────────────────────────

	function deleteBlock(id) {
		var idx = findBlockIndex(id);
		if (idx === -1) return;
		snapshot();
		state.blocks.splice(idx, 1);
		if (state.selectedBlockId === id) {
			state.selectedBlockId = null;
			renderSettings(null);
		}
		renderCanvas();
		markDirty();
	}

	function duplicateBlock(id) {
		var idx = findBlockIndex(id);
		if (idx === -1) return;
		snapshot();
		var original = state.blocks[idx];
		var clone = JSON.parse(JSON.stringify(original));
		clone.id = uid();
		state.blocks.splice(idx + 1, 0, clone);
		renderCanvas();
		selectBlock(clone.id);
		markDirty();
	}

	function scrollBlockIntoView(id) {
		var el = document.querySelector('[data-block-id="' + id + '"]');
		if (el && el.scrollIntoView) {
			el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
		}
	}

	// ─── Toolbar ──────────────────────────────────────────────────────────

	function bindToolbar() {
		var saveBtn = document.querySelector('.kramar-eb-toolbar__save');
		if (saveBtn) {
			saveBtn.addEventListener('click', function () {
				saveBlocks();
			});
		}

		var previewBtn = document.querySelector('.kramar-eb-toolbar__preview');
		if (previewBtn) {
			previewBtn.addEventListener('click', function () {
				openPreview();
			});
		}

		var testBtn = document.querySelector('.kramar-eb-toolbar__test');
		if (testBtn) {
			testBtn.addEventListener('click', function () {
				openTestSend();
			});
		}
	}

	function openPreview() {
		var modal = document.getElementById('kramar-eb-preview-modal');
		if (!modal) {
			modal = createPreviewModal();
			document.body.appendChild(modal);
		}
		modal.style.display = 'flex';
		var closeBtn = modal.querySelector('.kramar-eb-preview-close');
		if (closeBtn) { closeBtn.focus(); }

		var iframe = modal.querySelector('.kramar-eb-preview-iframe');
		iframe.srcdoc = '<p style="padding:40px;text-align:center;font-family:sans-serif;color:#787c82;">' + esc(i18n.loadingPreview || 'Loading preview…') + '</p>';

		var formData = new FormData();
		formData.append('action', 'kramar_email_preview');
		formData.append('_ajax_nonce', nonce);
		formData.append('email_id', emailId);

		var xhr = new XMLHttpRequest();
		xhr.open('POST', ajaxUrl);
		xhr.onreadystatechange = function () {
			if (xhr.readyState !== 4) return;
			if (xhr.status === 200) {
				try {
					var resp = JSON.parse(xhr.responseText);
					if (resp.success && resp.data && resp.data.html) {
						iframe.srcdoc = resp.data.html;
					} else {
						iframe.srcdoc = '<p style="padding:40px;text-align:center;color:#d63638;">Failed to load preview.</p>';
					}
				} catch (e) {
					iframe.srcdoc = '<p style="padding:40px;text-align:center;color:#d63638;">Error parsing response.</p>';
				}
			}
		};
		xhr.send(formData);
	}

	function createPreviewModal() {
		var modal = document.createElement('div');
		modal.id = 'kramar-eb-preview-modal';
		modal.setAttribute('role', 'dialog');
		modal.setAttribute('aria-modal', 'true');
		modal.setAttribute('aria-label', i18n.emailPreview || 'Email Preview');
		modal.style.cssText = 'position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;';

		var overlay = document.createElement('div');
		overlay.style.cssText = 'position:absolute;inset:0;background:rgba(0,0,0,.6);';
		overlay.addEventListener('click', function () { modal.style.display = 'none'; });

		// Close on Escape.
		modal.addEventListener('keydown', function (e) {
			if (e.key === 'Escape') { modal.style.display = 'none'; }
		});

		var content = document.createElement('div');
		content.style.cssText = 'position:relative;background:#fff;border-radius:8px;width:90vw;max-width:800px;height:85vh;display:flex;flex-direction:column;overflow:hidden;z-index:1;';

		var toolbar = document.createElement('div');
		toolbar.style.cssText = 'display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid #dcdcde;';
		toolbar.innerHTML = '<strong style="flex:1;">' + esc(i18n.emailPreview || 'Email Preview') + '</strong>' +
			'<button type="button" class="button kramar-eb-preview-device" data-device="desktop" aria-label="' + esc(i18n.desktop || 'Desktop') + '" style="background:#2271b1;color:#fff;border-color:#2271b1;"><span class="dashicons dashicons-desktop"></span></button>' +
			'<button type="button" class="button kramar-eb-preview-device" data-device="mobile" aria-label="' + esc(i18n.mobile || 'Mobile') + '"><span class="dashicons dashicons-smartphone"></span></button>' +
			'<button type="button" class="button kramar-eb-preview-close" aria-label="' + esc(i18n.close || 'Close') + '" style="font-size:18px;line-height:1;">&times;</button>';

		var iframeWrap = document.createElement('div');
		iframeWrap.style.cssText = 'flex:1;padding:20px;background:#f0f0f1;display:flex;justify-content:center;overflow:auto;';

		var iframe = document.createElement('iframe');
		iframe.className = 'kramar-eb-preview-iframe';
		iframe.sandbox = 'allow-same-origin';
		iframe.style.cssText = 'border:none;width:100%;max-width:640px;height:100%;background:#f6f6f6;transition:max-width .3s;';

		iframeWrap.appendChild(iframe);
		content.appendChild(toolbar);
		content.appendChild(iframeWrap);
		modal.appendChild(overlay);
		modal.appendChild(content);

		var closeBtn = modal.querySelector('.kramar-eb-preview-close');
		if (closeBtn) {
			closeBtn.addEventListener('click', function () { modal.style.display = 'none'; });
		}

		modal.querySelectorAll('.kramar-eb-preview-device').forEach(function (btn) {
			btn.addEventListener('click', function () {
				modal.querySelectorAll('.kramar-eb-preview-device').forEach(function (b) {
					b.style.background = '';
					b.style.color = '';
					b.style.borderColor = '';
				});
				this.style.background = '#2271b1';
				this.style.color = '#fff';
				this.style.borderColor = '#2271b1';
				iframe.style.maxWidth = this.dataset.device === 'mobile' ? '375px' : '600px';
			});
		});

		return modal;
	}

	function openTestSend() {
		var dialog = document.getElementById('kramar-eb-test-dialog');
		if (!dialog) {
			dialog = createTestDialog();
			document.body.appendChild(dialog);
		}
		dialog.style.display = 'flex';
		dialog.querySelector('.kramar-eb-test-result').style.display = 'none';
		dialog.querySelector('.kramar-eb-test-input').focus();
	}

	function createTestDialog() {
		var dialog = document.createElement('div');
		dialog.id = 'kramar-eb-test-dialog';
		dialog.setAttribute('role', 'dialog');
		dialog.setAttribute('aria-modal', 'true');
		dialog.setAttribute('aria-label', i18n.sendTestEmail || 'Send Test Email');
		dialog.style.cssText = 'position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;';

		var overlay = document.createElement('div');
		overlay.style.cssText = 'position:absolute;inset:0;background:rgba(0,0,0,.6);';
		overlay.addEventListener('click', function () { dialog.style.display = 'none'; });

		dialog.addEventListener('keydown', function (e) {
			if (e.key === 'Escape') { dialog.style.display = 'none'; }
		});

		var content = document.createElement('div');
		content.style.cssText = 'position:relative;background:#fff;border-radius:8px;padding:24px;max-width:440px;width:90vw;z-index:1;';

		var adminEmail = cfg.globalSettings && cfg.globalSettings.from_email ? cfg.globalSettings.from_email : '';
		if (!adminEmail && cfg.emailConfig && cfg.emailConfig.recipient) adminEmail = cfg.emailConfig.recipient;

		content.innerHTML = '<h3 style="margin:0 0 8px;">' + esc(i18n.sendTestEmail || 'Send Test Email') + '</h3>' +
			'<p style="color:#787c82;margin:0 0 16px;">' + esc(i18n.testRecipientHint || 'Enter recipient address(es), comma-separated.') + '</p>' +
			'<input type="text" class="kramar-eb-test-input regular-text" aria-label="' + esc(i18n.sendTestEmail || 'Send Test Email') + '" style="width:100%;padding:8px 12px;border:1px solid #dcdcde;border-radius:4px;font-size:14px;" value="' + esc(adminEmail || '') + '" />' +
			'<div class="kramar-eb-test-result" style="display:none;margin-top:12px;padding:8px 12px;border-radius:4px;"></div>' +
			'<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end;">' +
			'<button type="button" class="button kramar-eb-test-cancel">' + esc(i18n.cancel || 'Cancel') + '</button>' +
			'<button type="button" class="button button-primary kramar-eb-test-send">' + esc(i18n.sendTest || 'Send Test') + '</button>' +
			'</div>';

		dialog.appendChild(overlay);
		dialog.appendChild(content);

		content.querySelector('.kramar-eb-test-cancel').addEventListener('click', function () {
			dialog.style.display = 'none';
		});

		content.querySelector('.kramar-eb-test-send').addEventListener('click', function () {
			var input = dialog.querySelector('.kramar-eb-test-input');
			var result = dialog.querySelector('.kramar-eb-test-result');
			var sendBtn = this;

			sendBtn.disabled = true;
			sendBtn.textContent = i18n.sending || 'Sending…';

			var formData = new FormData();
			formData.append('action', 'kramar_email_test_send');
			formData.append('_ajax_nonce', nonce);
			formData.append('email_id', emailId);
			formData.append('recipients', input.value);

			var xhr = new XMLHttpRequest();
			xhr.open('POST', ajaxUrl);
			xhr.onreadystatechange = function () {
				if (xhr.readyState !== 4) return;
				sendBtn.disabled = false;
				sendBtn.textContent = i18n.sendTest || 'Send Test';
				result.style.display = 'block';

				if (xhr.status === 200) {
					try {
						var resp = JSON.parse(xhr.responseText);
						if (resp.success) {
							dialog.style.display = 'none';
							showNotice('Test email sent!', 'success');
						} else {
							result.style.background = '#fce4e4';
							result.style.color = '#d63638';
							result.textContent = 'Failed: ' + (resp.data || 'Unknown error');
						}
					} catch (e) {
						result.style.background = '#fce4e4';
						result.style.color = '#d63638';
						result.textContent = 'Error parsing response.';
					}
				} else {
					result.style.background = '#fce4e4';
					result.style.color = '#d63638';
					result.textContent = 'Request failed.';
				}
			};
			xhr.send(formData);
		});

		return dialog;
	}

	// ─── Global Colors (template builder sidebar) ────────────────────────

	function bindGlobalColors() {
		var names = ['accent_color', 'text_color', 'body_bg', 'content_bg'];
		var palette = document.getElementById('kramar-eb-palette');
		if (!palette) return;

		names.forEach(function (name) {
			var row = palette.querySelector('.kramar-eb-gcolor[data-color-name="' + name + '"]');
			if (!row) return;
			bindHexControl(
				row.querySelector('.kramar-eb-hex'),
				function (hex) { globalSettings[name] = hex; renderCanvas(); },
				function (hex) { globalSettings[name] = hex; renderCanvas(); saveGlobalColors(); }
			);
		});
	}

	function saveGlobalColors() {
		var formData = new FormData();
		formData.append('action', 'kramar_email_save_global');
		formData.append('_ajax_nonce', nonce);
		formData.append('accent_color', globalSettings.accent_color || '#7f54b3');
		formData.append('body_bg', globalSettings.body_bg || '#f7f7f7');
		formData.append('content_bg', globalSettings.content_bg || '#ffffff');
		formData.append('text_color', globalSettings.text_color || '#525252');
		formData.append('from_name', globalSettings.from_name || '');
		formData.append('from_email', globalSettings.from_email || '');
		formData.append('reply_to', globalSettings.reply_to || '');
		formData.append('admin_recipients', globalSettings.admin_recipients || '');
		formData.append('global_bcc', globalSettings.global_bcc || '');
		formData.append('footer_text', globalSettings.footer_text || '');
		formData.append('logo_url', globalSettings.logo_url || '');
		formData.append('header_bg', globalSettings.header_bg || '#2c3338');
		formData.append('header_text_color', globalSettings.header_text_color || '#ffffff');

		var xhr = new XMLHttpRequest();
		xhr.open('POST', ajaxUrl);
		xhr.send(formData);
	}

	// ─── Save ─────────────────────────────────────────────────────────────

	function saveBlocks() {
		var subject = document.getElementById('kramar-eb-subject');
		var subjectVal = subject ? subject.value : '';

		var saveBtn = document.querySelector('.kramar-eb-toolbar__save');
		if (saveBtn) {
			saveBtn.disabled = true;
			saveBtn.textContent = 'Saving...';
		}

		var formData = new FormData();
		formData.append('action', 'kramar_email_save_blocks');
		formData.append('_ajax_nonce', nonce);
		formData.append('email_id', emailId);
		formData.append('blocks', JSON.stringify(state.blocks));
		formData.append('subject', subjectVal);

		var xhr = new XMLHttpRequest();
		xhr.open('POST', ajaxUrl);
		xhr.onreadystatechange = function () {
			if (xhr.readyState !== 4) return;
			if (saveBtn) {
				saveBtn.disabled = false;
				saveBtn.innerHTML = '<span class="dashicons dashicons-saved"></span> Save';
			}
			if (xhr.status === 200) {
				try {
					var resp = JSON.parse(xhr.responseText);
					if (resp.success) {
						clearDirty();
						showNotice(i18n.saved || 'Changes saved.', 'success');
					} else {
						showNotice('Save failed: ' + (resp.data || 'unknown error'), 'error');
					}
				} catch (ex) {
					showNotice('Save failed.', 'error');
				}
			} else {
				showNotice('Save failed.', 'error');
			}
		};
		xhr.send(formData);
	}

	// ─── Notices ──────────────────────────────────────────────────────────

	function showNotice(message, type) {
		var wrap = document.querySelector('.kramar-eb-toolbar');
		if (!wrap) return;

		var notice = document.createElement('div');
		notice.className = 'kramar-eb-notice-toast kramar-eb-notice-toast--' + (type || 'info');
		var icon = type === 'success' ? '<span class="dashicons dashicons-yes-alt" style="margin-right:8px;font-size:18px;line-height:1;"></span>' :
		           type === 'error' ? '<span class="dashicons dashicons-warning" style="margin-right:8px;font-size:18px;line-height:1;"></span>' : '';
		notice.innerHTML = icon + esc(message);
		wrap.appendChild(notice);

		setTimeout(function () {
			if (notice.parentNode) notice.parentNode.removeChild(notice);
		}, 3000);
	}

	// ─── Keyboard Shortcuts ───────────────────────────────────────────────

	function bindKeyboard() {
		document.addEventListener('keydown', function (e) {
			var isMac = navigator.platform.toUpperCase().indexOf('MAC') !== -1;
			var ctrl  = isMac ? e.metaKey : e.ctrlKey;

			if (ctrl && !e.shiftKey && (e.key === 's' || e.key === 'S')) {
				e.preventDefault();
				saveBlocks();
			}

			if (ctrl && !e.shiftKey && e.key === 'z') {
				e.preventDefault();
				undo();
			}

			if (ctrl && (e.key === 'y' || (e.shiftKey && e.key === 'z'))) {
				e.preventDefault();
				redo();
			}
		});
	}

	function bindBeforeUnload() {
		window.addEventListener('beforeunload', function (e) {
			if (state.dirty) {
				var msg = i18n.unsavedChanges || 'You have unsaved changes. Leave anyway?';
				e.returnValue = msg;
				return msg;
			}
		});
	}

	// ─── Utilities ────────────────────────────────────────────────────────

	function esc(str) {
		if (str === null || str === undefined) return '';
		return String(str)
			.replace(/&/g, '&amp;')
			.replace(/</g, '&lt;')
			.replace(/>/g, '&gt;')
			.replace(/"/g, '&quot;')
			.replace(/'/g, '&#039;');
	}

	function closest(el, selector) {
		while (el && el !== document) {
			if (el.matches && el.matches(selector)) return el;
			el = el.parentNode;
		}
		return null;
	}

	function filterMergeOptions(dropdown, query) {
		var options = dropdown.querySelectorAll('.kramar-eb-merge-option');
		var categories = dropdown.querySelectorAll('.kramar-eb-merge-category');
		var visibleCats = {};

		for (var i = 0; i < options.length; i++) {
			var searchText = options[i].getAttribute('data-search') || '';
			var show = !query || searchText.indexOf(query) !== -1;
			options[i].style.display = show ? '' : 'none';
			if (show) {
				var prevEl = options[i].previousElementSibling;
				while (prevEl && !prevEl.classList.contains('kramar-eb-merge-category')) {
					prevEl = prevEl.previousElementSibling;
				}
				if (prevEl) visibleCats[prevEl.textContent] = true;
			}
		}
		for (var j = 0; j < categories.length; j++) {
			categories[j].style.display = visibleCats[categories[j].textContent] ? '' : 'none';
		}
	}

	function insertAtCursor(textarea, text) {
		var start = textarea.selectionStart;
		var end   = textarea.selectionEnd;
		var value = textarea.value;
		var before = value.slice(0, start);
		var after = value.slice(end);

		var prefix = '';
		if (before.length > 0 && before[before.length - 1] !== ' ' && before[before.length - 1] !== '\n') {
			prefix = ' ';
		}
		var suffix = ' ';
		if (after.length > 0 && (after[0] === ' ' || after[0] === '\n' || after[0] === '.' || after[0] === ',' || after[0] === '!' || after[0] === '?')) {
			suffix = '';
		}

		var insert = prefix + text + suffix;
		textarea.value = before + insert + after;
		textarea.selectionStart = textarea.selectionEnd = start + insert.length;
		textarea.focus();
	}

	// ─── Boot ─────────────────────────────────────────────────────────────

	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', init);
	} else {
		init();
	}

})();
