/*
╔══════════════════════════════════════════════════════════════════════════════╗
║  PLUGIN:   SLIDER REVOLUTION 7                                                ║
║  MODULE:   PAGE EFFECTS  —  editor shell + registry (core)                    ║
║  AUTHOR:   ThemePunch                                                         ║
╚══════════════════════════════════════════════════════════════════════════════╝

  Provides: the generic "page effect" Gutenberg block (delegates its card to the effect type),
  and the on-page Recorder shell (full-screen overlay, live-page iframe stage, top bar with
  Save/Close + device tabs, a left panel for the effect tool, and an element-bind picker).
  Each addon registers SR7.PE.registerType(type, { card?, tool }); the tool owns its own panel UI,
  its iframe overlay, and load()/collect(). Save/load go through the framework AJAX (sr7_pe_*).
  Also hosts the shared responsive engine, the skin (--pe-* tokens), and the device switcher.

  @copyright 2026 ThemePunch
*/
(() => {
	"use strict";

	window.SR7 ??= {};
	SR7.PE ??= {};
	SR7.PE.types ??= {};

	const CFG = window.SR7PECfg || { types: {} };

	SR7.PE.registerType = (type, def) => { SR7.PE.types[type] = def || {}; };

	// =====================================================================================
	//  Responsive engine (mirror of public/js/page-effects.js — the editor page does not load
	//  the front bootstrap, so the same tiny device model + gVal-style #a resolver lives here too).
	//  Reuses SR7.D.devices when present (the SR7 engine isn't loaded in the block editor).
	// =====================================================================================
	SR7.PE.devices ??= (window.SR7 && SR7.D && Array.isArray(SR7.D.devices)) ? SR7.D.devices : ["wdesktop", "desktop", "notebook", "tablet", "mobile"];
	SR7.PE.deviceWidths ??= [1600, 1240, 1024, 778, 480];   // design widths — mirror core layer-size defaults (defaults.js)
	SR7.PE.deviceLabels ??= ["Wide", "Desktop", "Notebook", "Tablet", "Mobile"];
	SR7.PE.levelForWidth ??= (w) => { const d = SR7.PE.deviceWidths; for (let i = d.length - 1; i >= 0; i--) if (w <= d[i]) return i; return 0; };
	SR7.PE.lev ??= (field, level) => {
		if (!Array.isArray(field)) return field;
		let v = field[level];
		if (v != null && v !== "#a" && v !== "") return v;
		for (let i = level - 1; i >= 0; i--) { v = field[i]; if (v != null && v !== "#a" && v !== "") return v; }
		for (let i = level + 1; i < 5; i++) { v = field[i]; if (v != null && v !== "#a" && v !== "") return v; }
		return 0;
	};
	// write a per-level value, lazily promoting a legacy scalar to a 5-level array (scalar → base on
	// wide desktop, "#a" elsewhere) so editing one device cascades down to narrower ones, like SR7.
	SR7.PE.setLev = (obj, key, level, val) => {
		let f = obj[key];
		if (!Array.isArray(f)) { f = [f == null ? "#a" : f, "#a", "#a", "#a", "#a"]; obj[key] = f; }
		f[level] = val;
		return f;
	};

	// =====================================================================================
	//  Page Effects skin — ONE design language for every effect's authoring UI. Derived from the
	//  SR7 admin DARK theme tokens (base.css .sr-dark-theme + forms.css sr-drop-view / sr-slider):
	//  Inter + BLUE accent (--sr-col-b0 #309BFF) + SR7 dark surfaces + 10/4px radii, so the recorder
	//  reads as part of the SR7 family while Page Effects keeps its own structure. Exposed both as
	//  CSS custom props (--pe-*, on .sr7pe-overlay) and as a JS object (for SVG/canvas drawing colors).
	// =====================================================================================
	// Two palettes taken straight from SR7's own themes so the recorder shares the SAME color families as the
	// SR7 editor: DARK = base.css .sr-dark-theme tokens (verbatim), LIGHT = the :root (light) counterparts of
	// the same tokens. Shared bits (Inter, brand blue, radii) live in skinBase. The active palette is picked by
	// the SR7 dark/light cookie (sr7_colortheme via SR7.B.darkLightSwitch), DEFAULT LIGHT.
	SR7.PE.skinBase ??= {
		font: "'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif",
		accent: "#309BFF", accentStrong: "#5597FC", accentText: "#FFFFFF", good: "#008A6B", danger: "#FF6B6B",
		radius: "10px", radiusS: "4px"
	};
	SR7.PE.skins ??= {
		dark: {
			// verbatim SR7 dark tokens: text = --sr-col-w0-d0, control surface = --sr-col-tr-d0, border = --sr-col-w6-d0
			bg: "#1C1E20", surface: "#2A2C2F", surface2: "#323438", surface3: "#383B41",
			border: "#3F444A", border2: "#4A4E51", text: "#B7BBC0", text2: "#AAAEB3", textDim: "#777C80",
			accentBg: "rgba(48,155,255,.16)", shadow: "0 10px 30px rgba(0,0,0,.35)", stage: "#0b0d10", dropSel: "#20374D"
		},
		light: {
			bg: "#f5f5f5", surface: "#ffffff", surface2: "#F9F9F9", surface3: "#f0f0f1",
			border: "#dddddd", border2: "#cccccc", text: "#1a1a1a", text2: "#555555", textDim: "#777777",
			accentBg: "rgba(48,155,255,.14)", shadow: "0 8px 28px rgba(0,0,0,.14)", stage: "#eeeeee", dropSel: "#e6f1fc"
		}
	};
	// active theme from the SR7 cookie (default light); resolveSkin exposes the flat SR7.PE.skin used everywhere
	// (CSS var build + SVG/canvas drawing colors) so existing readers keep working.
	SR7.PE.theme ??= () => {
		const c = (window._tpt && _tpt.cookie && _tpt.cookie.get) ? (_tpt.cookie.get("sr7_colortheme") || "") : "";
		return String(c).indexOf("dark") !== -1 ? "dark" : "light";
	};
	SR7.PE.resolveSkin ??= () => (SR7.PE.skin = Object.assign({}, SR7.PE.skinBase, SR7.PE.skins[SR7.PE.theme()]));
	SR7.PE.resolveSkin();

	SR7.PE.injectSkin = () => {
		SR7.PE.resolveSkin();                 // pick dark/light from the SR7 cookie (may have changed since last call)
		const k = SR7.PE.skin;
		const vars = "--pe-font:" + k.font + ";--pe-bg:" + k.bg + ";--pe-surface:" + k.surface + ";--pe-surface-2:" + k.surface2 +
			";--pe-surface-3:" + k.surface3 + ";--pe-border:" + k.border + ";--pe-border-2:" + k.border2 +
			";--pe-text:" + k.text + ";--pe-text-2:" + k.text2 + ";--pe-text-dim:" + k.textDim +
			";--pe-accent:" + k.accent + ";--pe-accent-strong:" + k.accentStrong + ";--pe-accent-bg:" + k.accentBg +
			";--pe-accent-text:" + k.accentText + ";--pe-good:" + k.good + ";--pe-danger:" + k.danger +
			";--pe-radius:" + k.radius + ";--pe-radius-s:" + k.radiusS + ";--pe-shadow:" + k.shadow +
			";--pe-stage:" + k.stage + ";--pe-drop-sel:" + k.dropSel + ";";
		// theme vars go on :root and are RE-APPLIED every call, so a dark/light switch takes effect and the
		// drop list (appended to <body>, outside .sr7pe-overlay) inherits the active theme rather than a literal.
		let v = document.getElementById("sr7pe-skin-vars");
		if (!v) { v = document.createElement("style"); v.id = "sr7pe-skin-vars"; document.head.appendChild(v); }
		v.textContent = ":root{" + vars + "}";
		if (document.getElementById("sr7pe-skin")) return;   // structural component CSS injected once (uses the vars)
		const s = document.createElement("style"); s.id = "sr7pe-skin";
		s.textContent =
			".sr7pe-overlay{position:fixed;inset:0;z-index:100000;display:flex;flex-direction:column;background:var(--pe-bg);font-family:var(--pe-font);-webkit-font-smoothing:antialiased}" +
			".sr7pe-bar{flex:0 0 52px;display:flex;align-items:center;gap:12px;padding:0 16px;background:var(--pe-surface);border-bottom:1px solid var(--pe-border);color:var(--pe-text)}" +
			".sr7pe-brand{display:flex;align-items:center;gap:9px;font-size:14px;font-weight:600;color:var(--pe-text)}" +
			".sr7pe-logo{display:flex;flex:0 0 auto}.sr7pe-logo svg{display:block;width:24px;height:24px}" +
			".sr7pe-brand em{font-style:normal;color:var(--pe-text-dim);font-weight:400}" +
			".sr7pe-devices{display:flex;gap:2px;margin-left:6px;padding:3px;background:var(--pe-bg);border:1px solid var(--pe-border);border-radius:var(--pe-radius-s)}" +
			".sr7pe-dev{background:transparent;color:var(--pe-text-dim);border:0;border-radius:4px;padding:5px 11px;font:500 12px var(--pe-font);cursor:pointer;transition:.13s}" +
			".sr7pe-dev:hover{color:var(--pe-text);background:var(--pe-surface-3)}" +
			".sr7pe-dev.on{background:var(--pe-accent);color:var(--pe-accent-text)}" +
			".sr7pe-spacer{flex:1}" +
			".sr7pe-btn{background:var(--pe-surface-2);color:var(--pe-text);border:1px solid var(--pe-border-2);border-radius:var(--pe-radius-s);padding:7px 15px;font:500 13px var(--pe-font);cursor:pointer;transition:.13s}" +
			".sr7pe-btn:hover{border-color:var(--pe-text-dim);color:#fff}" +
			".sr7pe-btn--primary{background:var(--pe-accent);border-color:var(--pe-accent);color:var(--pe-accent-text)}" +
			".sr7pe-btn--primary:hover{background:var(--pe-accent-strong);border-color:var(--pe-accent-strong);color:#fff}" +
			".sr7pe-btn--saved{background:var(--pe-good);border-color:var(--pe-good);color:#fff}" +
			".sr7pe-body{flex:1;display:flex;min-height:0}" +
			".sr7pe-panel{width:308px;flex:0 0 308px;background:var(--pe-surface-2);border-right:1px solid var(--pe-border);overflow:auto;color:var(--pe-text);font-size:13px}" +
			".sr7pe-stage{flex:1;position:relative;background:var(--pe-stage) repeating-linear-gradient(45deg,transparent,transparent 10px,rgba(128,128,128,.06) 10px,rgba(128,128,128,.06) 20px);display:flex;justify-content:center;align-items:stretch;overflow:auto}" +
			".sr7pe-iframe{height:100%;border:0;display:block;background:#fff;box-shadow:var(--pe-shadow);transition:width .18s ease}" +

			// ---- SR7 dark-editor form components, EXTRACTED from sr-drop-view / sr-slider / sr-onoff and
			//      SCOPED to .sr7pe-panel so they reproduce the editor look WITHOUT the editor's component
			//      JS and WITHOUT leaking into Gutenberg's own controls. (User: "extract and guard".)
			".sr7pe-panel,.sr7pe-panel *{box-sizing:border-box}" +
			// dropdown — like sr-drop-view (#383B41 / 1px #4A4E51 / r4 / 13px-600), native arrow replaced
			".sr7pe-panel select{-webkit-appearance:none;-moz-appearance:none;appearance:none;font:600 13px/26px var(--pe-font);height:28px;color:var(--pe-text);background-color:var(--pe-surface-3);border:1px solid var(--pe-border-2);border-radius:var(--pe-radius-s);padding:0 26px 0 9px;cursor:pointer;background-image:url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%23AAAEB3' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/></svg>\");background-repeat:no-repeat;background-position:right 9px center}" +
			".sr7pe-panel select:hover{border-color:var(--pe-accent)}" +
			".sr7pe-panel select:focus{outline:none;border-color:var(--pe-accent);box-shadow:0 0 0 2px var(--pe-accent-bg)}" +
			".sr7pe-panel option{background:var(--pe-surface-2);color:var(--pe-text)}" +
			// custom dropdown (SR7.PE.dropField) — matches the editor's sr-drop EXACTLY (closed + open),
			// which a native <select> can't. Closed: bold VALUE left (the input), muted label right,
			// 10x6 chevron far right, 28px/lh26. Open list: dark panel, blue selected/hover on #20374D.
			".sr7pe-drop{position:relative;display:block;height:28px;margin:0 16px 8px;border:1px solid var(--pe-border-2);border-radius:var(--pe-radius-s);background:var(--pe-surface-3);padding:0 8px;cursor:pointer;font:600 13px/26px var(--pe-font);color:var(--pe-text);box-sizing:border-box}" +
			".sr7pe-drop:hover,.sr7pe-drop.open{border-color:var(--pe-accent)}" +
			".sr7pe-drop.disabled{opacity:.5;cursor:default;color:var(--pe-text-dim);border-color:var(--pe-border)}" +
			".sr7pe-drop.off{display:none}" +
			".sr7pe-drop-value{display:block;margin-right:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:26px}" +
			".sr7pe-drop-title{position:absolute;right:24px;top:0;line-height:28px;font-size:11px;font-weight:400;color:var(--pe-text-dim);pointer-events:none;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
			".sr7pe-drop-arrow{position:absolute;right:8px;top:0;height:28px;display:flex;align-items:center;color:var(--pe-text-dim);pointer-events:none}" +
			// NOTE: the open list is appended to <body> (outside .sr7pe-overlay) so it must NOT rely on the
			// --pe-* vars cascading — every value carries a literal fallback (like the colorpicker popover).
			".sr7pe-drop-list{position:fixed;z-index:100030;background:var(--pe-surface-2,#323438);border:1px solid var(--pe-border,#3F444A);border-radius:var(--pe-radius-s,4px);box-shadow:var(--pe-shadow,0 10px 30px rgba(0,0,0,.45));padding:5px;max-height:280px;overflow:auto;box-sizing:border-box;font-family:var(--pe-font,'Inter',system-ui,sans-serif)}" +
			".sr7pe-drop-opt{padding:8px 12px;border-radius:4px;font-size:13px;font-weight:500;color:var(--pe-text,#B7BBC0);cursor:pointer;white-space:nowrap}" +
			".sr7pe-drop-opt:hover,.sr7pe-drop-opt.selected{color:var(--pe-accent,#309BFF);background:var(--pe-drop-sel,#20374D)}" +
			// text / number input — same surface as dropdown
			".sr7pe-panel input[type=text],.sr7pe-panel input[type=number]{font:600 13px/24px var(--pe-font);color:var(--pe-text);background:var(--pe-surface-3);border:1px solid var(--pe-border-2);border-radius:var(--pe-radius-s);padding:2px 9px;height:28px}" +
			".sr7pe-panel input[type=text]:focus,.sr7pe-panel input[type=number]:focus{outline:none;border-color:var(--pe-accent)}" +
			// color swatch
			".sr7pe-panel input[type=color]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:40px;height:26px;border:1px solid var(--pe-border-2);border-radius:var(--pe-radius-s);background:none;cursor:pointer;padding:2px}" +
			".sr7pe-panel input[type=color]::-webkit-color-swatch-wrapper{padding:0}.sr7pe-panel input[type=color]::-webkit-color-swatch{border:none;border-radius:2px}" +
			// range — thin track + blue fill (sr-slider used --sr-col-b0); accent-color gives the fill natively
			".sr7pe-panel input[type=range]{-webkit-appearance:none;appearance:none;width:130px;height:14px;background:transparent;accent-color:var(--pe-accent);cursor:pointer}" +
			".sr7pe-panel input[type=range]::-webkit-slider-runnable-track{height:4px;border-radius:9999px;background:var(--pe-surface-3)}" +
			".sr7pe-panel input[type=range]::-moz-range-track{height:4px;border-radius:9999px;background:var(--pe-surface-3)}" +
			".sr7pe-panel input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;margin-top:-5px;border-radius:50%;background:#fff;border:2px solid var(--pe-text-dim);box-shadow:0 1px 2px rgba(0,0,0,.4)}" +
			".sr7pe-panel input[type=range]::-moz-range-thumb{width:13px;height:13px;border-radius:50%;background:#fff;border:2px solid var(--pe-text-dim)}" +
			// checkbox → SR7 pill toggle (sr-onoff look): grey off, blue on, white knob
			".sr7pe-panel input[type=checkbox]{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;vertical-align:middle;width:34px;height:18px;border-radius:9999px;background:var(--pe-border-2);position:relative;cursor:pointer;transition:.15s;flex:0 0 auto;margin:0}" +
			".sr7pe-panel input[type=checkbox]:checked{background:var(--pe-accent)}" +
			// kill WordPress's native checkmark (wp-core-ui injects a dashicon tick via ::before in the block editor)
			".sr7pe-panel input[type=checkbox]:before,.sr7pe-panel input[type=checkbox]:checked:before{content:none!important;display:none!important}" +
			// SR7 sr-onoff look: white sliding knob, grey off / blue on
			".sr7pe-panel input[type=checkbox]:after{content:'';position:absolute;width:14px;height:14px;border-radius:50%;background:#fff;top:50%;margin-top:-7px;left:2px;transition:left .15s;box-shadow:0 1px 2px rgba(0,0,0,.35)}" +
			".sr7pe-panel input[type=checkbox]:checked:after{left:18px}";
		document.head.appendChild(s);
	};

	// SR7 brand mark (the same SR mark the revslider Gutenberg block uses) — reused for the Page-Effect
	// inspector header so every effect's settings panel reads like the SR7 module block (one house).
	SR7.PE.logoSVG ??= "<svg viewBox='0 0 36 36' xmlns='http://www.w3.org/2000/svg' width='26' height='26'><g transform='translate(8 7.493)'><rect width='36' height='36' rx='5' transform='translate(-8 -7.493)' fill='#5c24ff'/><path d='M46.311,18.755l2.807,2.81a.2.2,0,0,1-.14.337H40.089a.121.121,0,0,1-.137-.137l-.006-8.912a.188.188,0,0,1,.322-.134l2.938,2.933a.109.109,0,0,0,.185-.006A6.5,6.5,0,0,0,43.9,7.7a6.271,6.271,0,0,0-5.255-2.944q-.123.009-.123-.117l0-3.991a.121.121,0,0,1,.148-.137c5.283.394,9.36,3.746,10.293,8.929a10.885,10.885,0,0,1-2.231,8.781.516.516,0,0,1-.168.145l-.211.114Q46.146,18.589,46.311,18.755Z' transform='translate(-28.331 -0.8)' fill='#fff'/><path d='M0,1.745V1.46l8.975.014a.124.124,0,0,1,.14.14l0,8.627a.251.251,0,0,1-.431.177L5.974,7.688a.133.133,0,0,0-.225.009C2.037,12.144,5,18.126,10.521,18.714a.168.168,0,0,1,.148.168v3.766a.123.123,0,0,1-.145.14A10.328,10.328,0,0,1,3.94,20.36Q-1.717,15.456.79,7.893A9.566,9.566,0,0,1,2.844,4.8a.151.151,0,0,0-.006-.234Z' transform='translate(-0.8 -1.481)' fill='#fff'/></g></svg>";

	// Inspector chrome for a Page-Effect block's settings panel. LIGHT-ONLY: the SR7 logo is placed onto the
	// panel's OWN title bar (so it stays one collapsible row with Gutenberg's native chevron), Inter on the
	// title, and the SR7 blue is fed into Gutenberg's controls via the SANCTIONED --wp-admin-theme-color var.
	// We ride Gutenberg's own controls (nothing of wp-components' internals restyled) so WP updates can't break
	// it — the logo is an additive ::before that just degrades to "no logo" if the panel markup ever changes.
	// (Dark was dropped: dark controls in a light editor read wrong and would mean chasing wp-components.)
	SR7.PE.injectInspectorChrome ??= () => {
		if (document.getElementById("sr7pe-inspector-css")) return;
		const k = SR7.PE.skin;
		const s = document.createElement("style"); s.id = "sr7pe-inspector-css";
		s.textContent =
			// The panel's brand icon is now provided per-effect via PanelBody's own `icon` prop (the SAME sprite
			// icon as the block), so it always matches the block — no generic SR-mark ::before here.
			".sr7pe-inspector .components-panel__body-title button{font-family:'Inter',-apple-system,BlinkMacSystemFont,system-ui,sans-serif;font-weight:600}" +
			".sr7pe-inspector .components-panel__body.is-opened h2{margin-bottom:8px}" +
			".sr7pe-inspector{--wp-admin-theme-color:" + k.accent + ";--wp-admin-theme-color-darker-10:" + k.accentStrong + ";--wp-admin-theme-color-darker-20:" + k.accentStrong + "}";
		document.head.appendChild(s);
	};

	// =====================================================================================
	//  Dropdown — reusable custom control matching the SR7 editor's sr-drop (closed + open list).
	//  Built (not a native <select>) so the value-left/label-right layout and the styled option list
	//  match exactly. value object stored externally; opts = [[value,label],…].
	// =====================================================================================
	let OPEN_DROP = null;
	const closeDrop = () => {
		if (!OPEN_DROP) return;
		const o = OPEN_DROP; OPEN_DROP = null;
		document.removeEventListener("pointerdown", o.outside, true);
		document.removeEventListener("keydown", o.onKey, true);
		if (o.list.parentNode) o.list.remove();
		o.el.classList.remove("open");
	};
	SR7.PE.closeDropdown = closeDrop;

	SR7.PE.dropField = (cfg) => {
		cfg = cfg || {};
		let opts = cfg.options || [], val = cfg.value;
		const el = document.createElement("div"); el.className = "sr7pe-drop" + (cfg.disabled ? " disabled" : ""); el.tabIndex = 0;
		const valEl = document.createElement("span"); valEl.className = "sr7pe-drop-value";
		const titleEl = document.createElement("span"); titleEl.className = "sr7pe-drop-title"; titleEl.textContent = cfg.title || "";
		const arrow = document.createElement("span"); arrow.className = "sr7pe-drop-arrow";
		arrow.innerHTML = "<svg width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/></svg>";
		el.appendChild(valEl); el.appendChild(titleEl); el.appendChild(arrow);

		const labelFor = (v) => { const o = opts.find(o => o[0] === v); return o ? o[1] : (v == null ? "" : v); };
		const paint = () => { valEl.textContent = labelFor(val); };
		const open = () => {
			if (el.classList.contains("disabled")) return;
			closeDrop();
			const list = document.createElement("div"); list.className = "sr7pe-drop-list";
			opts.forEach(o => {
				const it = document.createElement("div"); it.className = "sr7pe-drop-opt" + (o[0] === val ? " selected" : ""); it.textContent = o[1];
				it.addEventListener("pointerdown", (e) => { e.preventDefault(); val = o[0]; paint(); closeDrop(); if (cfg.onChange) cfg.onChange(val); });
				list.appendChild(it);
			});
			document.body.appendChild(list);
			const r = el.getBoundingClientRect(); list.style.left = r.left + "px"; list.style.width = r.width + "px";
			const lh = list.offsetHeight; let top = r.bottom + 4; if (top + lh > window.innerHeight - 8) top = Math.max(8, r.top - lh - 4);
			list.style.top = top + "px"; el.classList.add("open");
			const outside = (e) => { if (!list.contains(e.target) && !el.contains(e.target)) closeDrop(); };
			const onKey = (e) => { if (e.key === "Escape") closeDrop(); };
			document.addEventListener("pointerdown", outside, true); document.addEventListener("keydown", onKey, true);
			OPEN_DROP = { el, list, outside, onKey };
		};
		el.addEventListener("click", open);
		el.setValue = (v) => { val = v; paint(); };
		el.getValue = () => val;
		el.setDisabled = (d) => { el.classList.toggle("disabled", !!d); };
		el.setOptions = (o) => { opts = o || []; paint(); };
		paint();
		return el;
	};

	// =====================================================================================
	//  Icon picker — a searchable grid of icon-FONT glyphs from SR7's bundled fonts (Font Awesome,
	//  Pe-icon-7-stroke). We parse each font's CSS for class→codepoint ourselves (standalone: the SR7
	//  editor's SR7.B.iconUtils isn't guaranteed in this context). value = {family, code, cls, name};
	//  consumers render it as an SVG <text> glyph so it scales/rotates/colours like any stamp.
	// =====================================================================================
	SR7.PE.iconFonts = {
		FontAwesome:   { family: "FontAwesome", base: "fa", css: "font-awesome/css/font-awesome.css" },
		PeIcon7Stroke: { family: "Pe-icon-7-stroke", base: "", css: "pe-icon-7-stroke/css/pe-icon-7-stroke.css" }
	};
	const peFontsUrl = () => (window.SR7PECfg && SR7PECfg.fontsUrl) || (window.SR7 && SR7.E && SR7.E.peFontsUrl) || "";
	// inject the font-face CSS so the picker can DRAW the glyphs (and so SVG <text> can use the family)
	SR7.PE.ensureIconFonts = (doc) => {
		doc = doc || document; const base = peFontsUrl(); if (!base) return;
		for (const k in SR7.PE.iconFonts) {
			const id = "sr7pe-iconfont-" + k;
			if (!doc.getElementById(id)) { const l = doc.createElement("link"); l.id = id; l.rel = "stylesheet"; l.href = base + SR7.PE.iconFonts[k].css; doc.head.appendChild(l); }
		}
	};
	SR7.PE.loadIcons = async () => {
		if (SR7.PE._iconList) return SR7.PE._iconList;
		const base = peFontsUrl(), out = [];
		for (const k in SR7.PE.iconFonts) {
			const f = SR7.PE.iconFonts[k];
			try {
				const css = await fetch(base + f.css).then(r => r.text());
				const re = /\.([a-zA-Z0-9_-]+):before\s*\{\s*content:\s*["']\\([a-fA-F0-9]+)["']/g; let m;
				while ((m = re.exec(css))) out.push({ family: f.family, code: m[2], cls: (f.base ? f.base + " " : "") + m[1], name: m[1].replace(/^fa-|^pe-7s-/, "").replace(/-/g, " ") });
			} catch (e) {}
		}
		SR7.PE._iconList = out; return out;
	};
	let OPEN_ICONS = null;
	const closeIcons = () => { if (!OPEN_ICONS) return; const o = OPEN_ICONS; OPEN_ICONS = null; document.removeEventListener("pointerdown", o.outside, true); document.removeEventListener("keydown", o.onKey, true); if (o.pop.parentNode) o.pop.remove(); o.el.classList.remove("open"); };
	const injectIconCSS = () => {
		if (document.getElementById("sr7pe-icon-css")) return;
		const s = document.createElement("style"); s.id = "sr7pe-icon-css";
		s.textContent =
			".sr7pe-iconpop{position:fixed;z-index:100040;width:300px;background:#323438;border:1px solid #3F444A;border-radius:6px;box-shadow:0 10px 30px rgba(0,0,0,.45);padding:8px;box-sizing:border-box;font-family:'Inter',system-ui,sans-serif}" +
			".sr7pe-iconpop input{width:100%;box-sizing:border-box;height:30px;background:#383B41;border:1px solid #4A4E51;border-radius:4px;color:#C7CCD2;padding:0 9px;font-size:13px;margin-bottom:8px;outline:none}" +
			".sr7pe-icongrid{display:grid;grid-template-columns:repeat(6,1fr);gap:2px;max-height:300px;overflow:auto}" +
			".sr7pe-iconbtn{display:flex;align-items:center;justify-content:center;height:38px;border-radius:4px;color:#C7CCD2;cursor:pointer;font-size:18px}" +
			".sr7pe-iconbtn:hover{background:#20374D;color:#309BFF}.sr7pe-iconbtn.sel{background:#309BFF;color:#fff}" +
			".sr7pe-iconempty{padding:14px;color:#777C80;font-size:12px;text-align:center}";
		document.head.appendChild(s);
	};
	SR7.PE.iconField = (cfg) => {
		cfg = cfg || {};
		let val = cfg.value || null;
		const el = document.createElement("div"); el.className = "sr7pe-drop"; el.tabIndex = 0;
		const valEl = document.createElement("span"); valEl.className = "sr7pe-drop-value";
		const titleEl = document.createElement("span"); titleEl.className = "sr7pe-drop-title"; titleEl.textContent = cfg.title || "Icon";
		const arrow = document.createElement("span"); arrow.className = "sr7pe-drop-arrow";
		arrow.innerHTML = "<svg width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/></svg>";
		el.appendChild(valEl); el.appendChild(titleEl); el.appendChild(arrow);
		// render a glyph by font-family + codepoint (NOT the .fa class — SR7's font-awesome.css scopes the
		// .fa font-family to its own containers, which our popover isn't inside; the @font-face still works).
		const glyphHTML = (o) => '<span style="font-family:\'' + o.family + '\';font-style:normal;font-weight:normal;line-height:1">' + String.fromCodePoint(parseInt(o.code, 16) || 0) + '</span>';
		const paint = () => { valEl.innerHTML = (val && val.code) ? (glyphHTML(val) + '<span style="margin-left:7px">' + (val.name || "icon") + '</span>') : "Pick an icon…"; };
		const open = async () => {
			if (el.classList.contains("disabled")) return;
			injectIconCSS(); SR7.PE.ensureIconFonts(); closeIcons();
			const pop = document.createElement("div"); pop.className = "sr7pe-iconpop";
			const search = document.createElement("input"); search.type = "text"; search.placeholder = "Search icons…";
			const grid = document.createElement("div"); grid.className = "sr7pe-icongrid";
			pop.appendChild(search); pop.appendChild(grid); document.body.appendChild(pop);
			const r = el.getBoundingClientRect();
			pop.style.left = Math.max(8, Math.min(r.left, window.innerWidth - 308)) + "px";
			let top = r.bottom + 4; if (top + 360 > window.innerHeight - 8) top = Math.max(8, r.top - 364); pop.style.top = top + "px";
			el.classList.add("open");
			const all = await SR7.PE.loadIcons();
			const renderGrid = (q) => {
				grid.innerHTML = ""; const ql = (q || "").toLowerCase().trim();
				const list = ql ? all.filter(ic => ic.name.indexOf(ql) > -1 || ic.cls.indexOf(ql) > -1) : all;
				if (!list.length) { const e = document.createElement("div"); e.className = "sr7pe-iconempty"; e.textContent = all.length ? "No matches" : "Icons couldn’t load"; grid.appendChild(e); return; }
				list.slice(0, 600).forEach(ic => {
					const b = document.createElement("div"); b.className = "sr7pe-iconbtn" + (val && val.code === ic.code && val.family === ic.family ? " sel" : ""); b.title = ic.name; b.innerHTML = glyphHTML(ic);
					b.addEventListener("pointerdown", (ev) => { ev.preventDefault(); val = { family: ic.family, code: ic.code, cls: ic.cls, name: ic.name }; paint(); closeIcons(); if (cfg.onChange) cfg.onChange(val); });
					grid.appendChild(b);
				});
			};
			renderGrid("");
			search.addEventListener("input", () => renderGrid(search.value));
			setTimeout(() => search.focus(), 30);
			const outside = (e) => { if (!pop.contains(e.target) && !el.contains(e.target)) closeIcons(); };
			const onKey = (e) => { if (e.key === "Escape") closeIcons(); };
			document.addEventListener("pointerdown", outside, true); document.addEventListener("keydown", onKey, true);
			OPEN_ICONS = { el, pop, outside, onKey };
		};
		el.addEventListener("click", open);
		el.setValue = (v) => { val = v; paint(); };
		el.getValue = () => val;
		el.setDisabled = (d) => el.classList.toggle("disabled", !!d);
		paint(); return el;
	};

	// ---- AJAX (routes through the shared SR7 _tpt.ajax helper — the same path every other api.class.php call uses) ----
	const ajax = (clientAction, data) => new Promise((resolve) => {
		_tpt.ajax({ action: clientAction, data, callBack: (raw) => resolve(_tpt.fixResponse(raw)) });
	});
	SR7.PE.ajax = ajax;

	// =====================================================================================
	//  Block registration (generic — one per registered type, card delegated to the type)
	// =====================================================================================
	const cardStyle = { display: "flex", alignItems: "center", gap: "16px", width: "100%", boxSizing: "border-box", border: "1px solid #e0d8ff", borderRadius: "0", padding: "16px 20px", background: "linear-gradient(180deg,#faf8ff,#fff)", fontFamily: "'Inter',system-ui,sans-serif" };
	const badgeStyle = { display: "inline-flex", alignItems: "center", gap: "6px", flex: "0 0 auto", padding: "6px 12px", borderRadius: "999px", background: "linear-gradient(135deg,#9570FF,#5C24FF)", color: "#fff", fontSize: "12px", fontWeight: 700, letterSpacing: ".01em", whiteSpace: "nowrap", boxShadow: "0 4px 12px rgba(92,36,255,.30)" };
	SR7.PE.badgeStyle = badgeStyle;   // shared so self_block effects (Filmstrip) reuse the same "✦ SR7 …" pill

	// SR7 brand mark as a block-inserter icon (purple tile + SR logo). Every Page-Effect block wears it so they
	// read as one SR7 family (Slider Revolution, Filmstrip, Chalk Line …), distinguished by their title.
	SR7.PE.logoIcon = () => { const c = wp.element.createElement;
		return c("svg", { width: 24, height: 24, viewBox: "0 0 36 36", xmlns: "http://www.w3.org/2000/svg" },
			c("g", { transform: "translate(8 7.493)" },
				c("rect", { width: 36, height: 36, rx: 5, transform: "translate(-8 -7.493)", fill: "#5c24ff" }),
				c("path", { fill: "#fff", transform: "translate(-28.331 -0.8)", d: "M46.311,18.755l2.807,2.81a.2.2,0,0,1-.14.337H40.089a.121.121,0,0,1-.137-.137l-.006-8.912a.188.188,0,0,1,.322-.134l2.938,2.933a.109.109,0,0,0,.185-.006A6.5,6.5,0,0,0,43.9,7.7a6.271,6.271,0,0,0-5.255-2.944q-.123.009-.123-.117l0-3.991a.121.121,0,0,1,.148-.137c5.283.394,9.36,3.746,10.293,8.929a10.885,10.885,0,0,1-2.231,8.781.516.516,0,0,1-.168.145l-.211.114Q46.146,18.589,46.311,18.755Z" }),
				c("path", { fill: "#fff", transform: "translate(-0.8 -1.481)", d: "M0,1.745V1.46l8.975.014a.124.124,0,0,1,.14.14l0,8.627a.251.251,0,0,1-.431.177L5.974,7.688a.133.133,0,0,0-.225.009C2.037,12.144,5,18.126,10.521,18.714a.168.168,0,0,1,.148.168v3.766a.123.123,0,0,1-.145.14A10.328,10.328,0,0,1,3.94,20.36Q-1.717,15.456.79,7.893A9.566,9.566,0,0,1,2.844,4.8a.151.151,0,0,0-.006-.234Z" })));
	};

	// A block-inserter icon that <use>s a symbol from the SR7 sprite (the PHP side injects the sprite into the
	// block editor) on the SR7 purple tile — so each effect shows its OWN addon icon (Filmstrip = Addon_Filmstrip,
	// Chalk Line = a pencil, …), same purple family as the Slider Revolution block.
	// A COMPONENT (like the revslider block icon) so Gutenberg passes width/height per context — inserter /
	// toolbar / list view — and it scales (a fixed size looked huge in the toolbar). The purple ROUNDED tile
	// (rx) + a well-inset white symbol are baked into the svg so it reads like the SR module mark everywhere.
	// A self-sufficient purple tile + white addon glyph, mirroring the main block's SR7Icon (36-unit box,
	// rx 5, baked purple rect). The baked rect is what shows in contexts that DON'T apply the icon object's
	// `background` — the block toolbar switcher, list view, document outline. The `background` (see
	// registerBlocks) additionally fills the whole inserter cell, so the big lila bg matches the main block.
	SR7.PE.spriteIcon = (id) => (props) => {
		const c = wp.element.createElement;
		// Default width/height so the svg has an intrinsic size — some hosts (PanelBody's <Icon>) pass only a
		// `size` prop the svg can't read, and without width/height the icon expands to fill its container.
		const { size, ...p } = props || {};   // <Icon> passes a non-DOM `size` prop; consume it, don't spread it
		const dim = size || 24;
		return c("svg", Object.assign({ width: dim, height: dim, viewBox: "0 0 36 36", role: "img", "aria-hidden": true, xmlns: "http://www.w3.org/2000/svg" }, p),
			c("rect", { width: 36, height: 36, rx: 5, fill: "#5c24ff" }),
			c("use", { href: "#" + id, xlinkHref: "#" + id, x: 9, y: 9, width: 18, height: 18, fill: "#fff" }));
	};

	// The block-inserter hover preview. Each effect can supply its own image (meta.preview); otherwise it
	// falls back to the same placeholder image the main Slider Revolution block uses, so an effect with no
	// custom art still previews as one SR7 family. Served from core (SR7.E.plugin_url).
	const previewImage = (url) => {
		const el = wp.element.createElement;
		const src = url || (((window.SR7 && SR7.E && SR7.E.plugin_url) || "") + "admin/assets/images/sr7placeholder.webp");
		return el("img", { src: src, style: { width: "100%", display: "block" }, alt: "" });
	};
	SR7.PE.previewImage = previewImage;   // shared so self_block effects (Filmstrip) preview identically

	// SR7 block-button look — mirrors the module block's `.sr--block--button` (neutral bordered secondary,
	// Inter, weight 600). Shared on SR7.PE so self_block effects (Filmstrip) style their action buttons the
	// same, so every Page-Effect block's buttons read like the Slider Revolution module block's.
	SR7.PE.btnStyle = { fontFamily: "'Inter',system-ui,sans-serif", fontWeight: 600, boxShadow: "inset 0 0 0 1px #ccc", color: "#1e1e1e" };

	const defaultCard = (props, meta, onEdit) => {
		const el = wp.element.createElement, useBlockProps = wp.blockEditor.useBlockProps, Button = wp.components.Button, __ = wp.i18n.__;
		// a pencil glyph so the action reads like SR7's "Edit Module" button (icon + label, secondary style)
		const editIcon = el("svg", { width: 18, height: 18, viewBox: "0 0 24 24", "aria-hidden": true, focusable: false },
			el("path", { fill: "currentColor", d: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a.996.996 0 000-1.41l-2.34-2.34a.996.996 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" }));
		return el("div", useBlockProps({ className: "sr7-pe-block" }),
			el("div", { style: cardStyle },
				el("span", { style: badgeStyle }, el("span", { style: { fontSize: "13px", lineHeight: 1 } }, "✦"), "SR7 " + meta.title),
				el("div", { style: { flex: "1", minWidth: 0 } },
					el("div", { style: { color: "#646970", fontSize: "13px" } }, props.attributes.summary || __("Not set up yet — click Live Edit to draw your line.", "revslider"))
				),
				el(Button, { variant: "secondary", onClick: onEdit, style: SR7.PE.btnStyle },
					editIcon,
					el("span", { style: { marginLeft: "6px" } }, __("Live Edit", "revslider")))
			)
		);
	};

	// Unique effect id per block instance. Gutenberg "Duplicate" copies a block's attributes (incl.
	// effectId) but assigns a NEW clientId — so a duplicate would otherwise share the original's
	// effectId, hence its post-meta (editing one would edit both). We track which clientId claimed each
	// id this editor session and re-mint on collision; pre-existing duplicates self-heal on next open.
	const claimedIds = {};   // effectId -> clientId
	const mintEffectId = (clientId) => "pe" + String(clientId || "").replace(/-/g, "").slice(0, 12);
	const ensureEffectId = (props) => {
		const cid = props.clientId || "";
		let eid = props.attributes.effectId;
		if (!eid) {
			eid = mintEffectId(cid);
			props.setAttributes({ effectId: eid });
		} else if (claimedIds[eid] && claimedIds[eid] !== cid) {
			// effectId already owned by another instance → this block is a duplicate: take a fresh id and
			// drop the copied summary (its own meta is empty until configured).
			eid = mintEffectId(cid);
			props.setAttributes({ effectId: eid, summary: "" });
		}
		claimedIds[eid] = cid;
	};
	SR7.PE.ensureEffectId = ensureEffectId;

	let blocksRegistered = false;
	const registerBlocks = () => {
		if (blocksRegistered || !window.wp || !wp.blocks) return;
		blocksRegistered = true;

		Object.keys(CFG.types).forEach(type => {
			const meta = CFG.types[type];
			// Inspector-style effects register their own block (custom attributes + InspectorControls edit)
			// from their editor script — skip the generic recorder card here so we don't double-register.
			if (meta.self_block) return;
			wp.blocks.registerBlockType(meta.block, {
				apiVersion: 3,
				title: "Slider Revolution " + meta.title,
				keywords: ["Slider Revolution", "SR7", "themepunch", meta.title],
				category: "themepunch",
				icon: (meta.spriteIcon
					? { src: SR7.PE.spriteIcon(meta.spriteIcon), background: "#5c24ff", foreground: "#fff" }
					: SR7.PE.logoIcon()),
				description: meta.description,
				attributes: { effectId: { type: "string", default: "" }, summary: { type: "string", default: "" }, previewMode: { type: "boolean", default: false } },
				// inserter hover-preview: reuse the same placeholder image the main Slider Revolution block shows.
				example: { attributes: { previewMode: true } },
				edit: (props) => {
					if (props.attributes.previewMode) return previewImage(meta.preview);
					ensureEffectId(props);   // mint / de-duplicate this block's effectId
					const def = SR7.PE.types[type];
					const card = (def && def.card) ? def.card : defaultCard;
					return card(props, meta, () => openFromBlock(props, meta, type));
				},
				save: () => null
			});
		});
	};

	const openFromBlock = (props, meta, type) => {
		const post = wp.data.select("core/editor").getCurrentPost();
		const postId = wp.data.select("core/editor").getCurrentPostId();
		SR7.PE.openRecorder({
			type,
			postId,
			effectId: props.attributes.effectId,
			url: (post && post.link) ? post.link : ("/?page_id=" + postId),
			onSaved: (summary) => { props.setAttributes({ summary: String(summary || "") }); }
		});
	};

	// =====================================================================================
	//  Recorder shell
	// =====================================================================================
	let R = null; // active recorder session

	const btn = (label, cls) => { const b = document.createElement("button"); b.textContent = label; b.className = "sr7pe-btn" + (cls ? " " + cls : ""); return b; };

	// ---- device switcher (shell-owned; sets the editing level + resizes the iframe stage) ----
	const buildDeviceTabs = () => {
		R.devices.innerHTML = ""; R.tabs = [];
		SR7.PE.devices.forEach((dev, idx) => {
			const t = document.createElement("button");
			t.className = "sr7pe-dev";
			t.textContent = (SR7.PE.deviceLabels || SR7.PE.devices)[idx];
			t.title = SR7.PE.deviceWidths[idx] + "px";
			t.onclick = () => setLevel(idx);
			R.devices.appendChild(t); R.tabs.push(t);
		});
		paintTabs();
	};
	const paintTabs = () => { R.tabs.forEach((t, i) => t.classList.toggle("on", i === R.level)); };
	// width of the iframe for the active device: the design width, but never wider than the stage
	const applyDeviceWidth = () => {
		if (!R) return;
		const w = SR7.PE.deviceWidths[R.level], avail = R.stage.clientWidth || 9999;
		R.iframe.style.width = (R.level === 0 || w >= avail) ? "100%" : (w + "px");
	};
	const setLevel = (idx) => {
		if (!R || idx === R.level) return;
		R.level = idx; paintTabs(); applyDeviceWidth();
		// let the iframe reflow at the new width before the tool re-reads element positions
		const raf = (R.win && R.win.requestAnimationFrame) || window.requestAnimationFrame;
		raf(() => raf(() => { if (R && R.levelCb) R.levelCb(R.level); }));
	};

	// =====================================================================================
	//  Element-bind picker (framework service, shared by all effects). Snaps a viewport point to the
	//  most MEANINGFUL box under it and returns a STABLE css selector that matches the SAME element on
	//  the front end (the iframe loads the real permalink, so editor-generated selectors are front-valid).
	//  Priority while climbing from the clicked node → first of: sr7-module[id] · element with id ·
	//  Gutenberg wp-block-* wrapper · a meaningful block-level tag · else the clicked element itself.
	// =====================================================================================
	// block-level tags worth binding to (so clicking text inside a <p> snaps to the <p>, an image to <img>)
	const PE_MEANINGFUL = { IMG: 1, FIGURE: 1, PICTURE: 1, SVG: 1, VIDEO: 1, CANVAS: 1, BUTTON: 1, A: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, P: 1, BLOCKQUOTE: 1, UL: 1, OL: 1, LI: 1, TABLE: 1, SECTION: 1, HEADER: 1, FOOTER: 1, ARTICLE: 1, ASIDE: 1, NAV: 1 };
	const peEsc = (s) => (window.CSS && CSS.escape) ? CSS.escape(s) : String(s).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
	const peIsWpBlock = (el) => { const c = el.getAttribute && el.getAttribute("class"); return !!(c && /\bwp-block-\S/.test(c)); };
	SR7.PE.isWpBlock = peIsWpBlock;

	// Build a robust selector for el: an id when present, else a `>`-joined tag:nth-of-type path rooted at
	// the nearest ancestor that has an id (or <body>). Child combinator + nth-of-type is stable for the
	// statically-rendered front markup (which has no Gutenberg clientIds).
	SR7.PE.selectorFor = (el) => {
		if (!el || el.nodeType !== 1) return "";
		if (el.id) return "#" + peEsc(el.id);
		const parts = [];
		let node = el;
		while (node && node.nodeType === 1 && node.tagName !== "BODY" && node.tagName !== "HTML") {
			if (node.id) { parts.unshift("#" + peEsc(node.id)); break; }
			const tag = node.tagName.toLowerCase();
			let idx = 1, sib = node, many = false;
			while ((sib = sib.previousElementSibling)) if (sib.tagName === node.tagName) idx++;
			sib = node;
			while ((sib = sib.nextElementSibling)) if (sib.tagName === node.tagName) { many = true; break; }
			parts.unshift((idx > 1 || many) ? tag + ":nth-of-type(" + idx + ")" : tag);
			node = node.parentElement;
		}
		return parts.join(" > ");
	};

	const bindPicker = (clientX, clientY) => {
		if (!R || !R.doc) return null;
		const doc = R.doc;
		const under = doc.elementFromPoint(clientX, clientY);
		if (!under || under === doc.body || under === doc.documentElement) return null;
		// closest identifiable / meaningful box around the click
		let target = null, node = under;
		while (node && node !== doc.body) {
			if ((node.tagName === "SR7-MODULE" && node.id) || node.id || peIsWpBlock(node) || PE_MEANINGFUL[node.tagName]) { target = node; break; }
			node = node.parentElement;
		}
		if (!target) target = under;
		const selector = SR7.PE.selectorFor(target);
		let unique = false;
		try { const f = doc.querySelectorAll(selector); unique = (f.length === 1 && f[0] === target); } catch (e) {}
		return { selector, el: target, unique };
	};

	SR7.PE.openRecorder = (opts) => {
		if (R) closeRecorder();
		const def = SR7.PE.types[opts.type];
		if (!def || !def.tool) { window.alert("Effect tool not loaded: " + opts.type); return; }

		SR7.PE.injectSkin();

		const overlay = document.createElement("div"); overlay.className = "sr7pe-overlay";

		// top bar (shell-owned): brand, device tabs, spacer, Save, Close
		const bar = document.createElement("div"); bar.className = "sr7pe-bar";
		const brand = document.createElement("div"); brand.className = "sr7pe-brand";
		const logo = document.createElement("span"); logo.className = "sr7pe-logo"; logo.innerHTML = SR7.PE.logoSVG || "";
		const brandTxt = document.createElement("span");
		brandTxt.appendChild(document.createTextNode("Slider Revolution"));
		const em = document.createElement("em"); em.textContent = " · " + (def.label || def.title || "Page Effect"); brandTxt.appendChild(em);
		brand.appendChild(logo); brand.appendChild(brandTxt);
		const devices = document.createElement("div"); devices.id = "sr7pe-devices"; devices.className = "sr7pe-devices";
		const spacer = document.createElement("span"); spacer.className = "sr7pe-spacer";
		const save = btn(wp.i18n ? wp.i18n.__("Save", "revslider") : "Save", "sr7pe-btn--primary");
		const close = btn(wp.i18n ? wp.i18n.__("Close", "revslider") : "Close");
		[brand, devices, spacer, save, close].forEach(n => bar.appendChild(n));

		// body: left panel (tool UI) + iframe stage
		const body = document.createElement("div"); body.className = "sr7pe-body";
		const panel = document.createElement("div"); panel.className = "sr7pe-panel";
		// stage centres the iframe so narrower devices show as a framed viewport on the dark backdrop
		const stage = document.createElement("div"); stage.className = "sr7pe-stage";
		const iframe = document.createElement("iframe"); iframe.className = "sr7pe-iframe"; iframe.style.width = "100%";
		stage.appendChild(iframe); body.appendChild(panel); body.appendChild(stage);

		overlay.appendChild(bar); overlay.appendChild(body);
		document.body.appendChild(overlay);

		R = { opts, overlay, iframe, stage, panel, devices, save, dirty: false, doc: null, win: null, tool: def.tool, level: 1, levelCb: null, tabs: [] };
		buildDeviceTabs();

		save.onclick = doSave;
		close.onclick = () => { if (!R.dirty || window.confirm(wp.i18n ? wp.i18n.__("Close without saving?", "revslider") : "Close without saving?")) closeRecorder(); };

		iframe.addEventListener("load", () => {
			try { R.doc = iframe.contentDocument; R.win = iframe.contentWindow; void R.doc.body; }
			catch (err) { window.alert("Cannot access the page (cross-origin). It must be on this site."); return; }

			const ctx = {
				doc: R.doc, win: R.win, panel, devices,
				bindPicker,
				getLevel: () => R.level,                       // active editing device level (0..4)
				onLevelChange: (cb) => { R.levelCb = cb; },     // tool re-bakes/renders at the new level
				markDirty: () => { R.dirty = true; save.textContent = (wp.i18n ? wp.i18n.__("Save", "revslider") : "Save") + " *"; }
			};
			R.handle = R.tool.mount(ctx) || {};
			applyDeviceWidth();   // size the freshly-loaded iframe to the current device
			ajax("page_effects.get", { post_id: opts.postId, effect_id: opts.effectId }).then(r => {
				const stored = (r && r.success) ? r.stored : null; // api merges the response payload at the top level
				if (stored && stored.data && R.tool.load) R.tool.load(stored.data);
			});
		});
		// load the page with a flag so the live effect is suppressed in the iframe — only the editor draws
		iframe.src = opts.url + (opts.url.indexOf("?") > -1 ? "&" : "?") + "sr7pe_edit=1";
	};

	const doSave = () => {
		if (!R || !R.tool.collect) return;
		R.save.textContent = wp.i18n ? wp.i18n.__("Saving…", "revslider") : "Saving…";
		ajax("page_effects.save", { post_id: R.opts.postId, effect_id: R.opts.effectId, type: R.opts.type, effect: JSON.stringify(R.tool.collect()) })
			.then(r => {
				if (r && r.success) {
					R.dirty = false;
					R.save.textContent = wp.i18n ? wp.i18n.__("Saved ✓", "revslider") : "Saved ✓";
					R.save.classList.add("sr7pe-btn--saved");
					if (R.opts.onSaved) R.opts.onSaved(r.summary || "");
					setTimeout(() => { if (R) { R.save.textContent = wp.i18n ? wp.i18n.__("Save", "revslider") : "Save"; R.save.classList.remove("sr7pe-btn--saved"); } }, 1500);
				} else { R.save.textContent = "Save (error)"; }
			}).catch(() => { if (R) R.save.textContent = "Save (error)"; });
	};

	const closeRecorder = () => {
		if (!R) return;
		try { if (R.tool && R.tool.destroy) R.tool.destroy(); } catch (e) {}
		if (R.overlay) R.overlay.remove();
		R = null;
	};
	SR7.PE.closeRecorder = closeRecorder;

	// register blocks once the editor is ready
	if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", registerBlocks);
	else registerBlocks();
})();
