/**
 * GENE Motion — Scroll Hero: editor.
 *
 * Tiered by license (soft gate — the block is always usable, front end never
 * gated):
 *   Free  → one background (image/video, video scrubs) + one heading. Scroll
 *           length is a well-tuned fixed default.
 *   Pro   → add more headings that swap one-at-a-time as you scroll, plus the
 *           scroll-length control.
 * The canvas shows a calm static preview (the finished hero), never the pin.
 */
import { __ } from '@wordpress/i18n';
import {
	useBlockProps,
	InspectorControls,
	MediaUpload,
	MediaUploadCheck,
	PanelColorSettings,
} from '@wordpress/block-editor';
import {
	PanelBody,
	Button,
	RangeControl,
	SelectControl,
	TextControl,
	TextareaControl,
	ToggleControl,
	Notice,
} from '@wordpress/components';
import { Fragment, useState, useRef, useEffect } from '@wordpress/element';
import { mapOne, Media } from '../shared/media';
import { isPro, upgradeUrl } from '../shared/pro-gate';
import { beatPos, beatWindow } from '../shared/hero-timeline';

function clampN( n, min, max ) {
	return n < min ? min : n > max ? max : n;
}

// Editing aids: snap a placement % to a nearby anchor (edges / quarters / centre)
// and a scroll fraction to a 5% gridline, so headings and windows line up without
// pixel-hunting. `hit` drives the on-canvas guide highlight.
const SNAP_ANCHORS = [ 0, 25, 50, 75, 100 ];
function snapPct( v ) {
	for ( let i = 0; i < SNAP_ANCHORS.length; i++ ) {
		if ( Math.abs( v - SNAP_ANCHORS[ i ] ) <= 2.4 ) return { v: SNAP_ANCHORS[ i ], hit: true };
	}
	return { v: v, hit: false };
}
function snapFrac( f ) {
	const g = Math.round( f / 0.05 ) * 0.05;
	return Math.abs( f - g ) <= 0.02 ? g : f;
}

const MB = 1048576;
const GAUGE_COLOR = { ok: '#46b450', warn: '#dba617', bad: '#d63638', unknown: '#999' };
function fmtMB( bytes ) {
	return ( bytes / MB ).toFixed( 1 );
}
// A ready-to-copy re-export recipe: 24fps + all-keyframes keeps the scrub smooth
// and the sharpness the same while cutting frames (and file size).
const REENCODE_CMD =
	'ffmpeg -i in.mp4 -an -vf "fps=24,format=yuv420p" -c:v libx264 -crf 22 -g 1 -movflags +faststart out.mp4';

function lines( heading ) {
	const parts = ( heading || '' ).split( '\n' );
	return parts.map( ( line, i ) => (
		<Fragment key={ i }>
			{ line }
			{ i < parts.length - 1 ? <br /> : null }
		</Fragment>
	) );
}

// A background video's weight, judged by what phones actually load (the phone
// clip if set, else the main one), with plain-language advice + a copy-ready
// ffmpeg recipe. Guidance only — it never touches the video. Shared by the
// single hero and each multi-scene chapter so every clip gets the same check.
function WeightGauge( { mainBytes, mobBytes, isPro } ) {
	if ( ! mainBytes && ! mobBytes ) {
		return (
			<div style={ { marginTop: '12px', borderTop: '1px solid #e0e0e0', paddingTop: '12px' } }>
				<p style={ { fontSize: '12px', fontWeight: 600, margin: '0 0 6px' } }>
					{ __( 'Video weight', 'gene-section-builder' ) }
				</p>
				<p style={ { fontSize: '12px', color: GAUGE_COLOR.unknown, fontWeight: 600, margin: 0 } }>
					{ __( 'Measuring…', 'gene-section-builder' ) }
				</p>
			</div>
		);
	}
	const phoneBytes = mobBytes || mainBytes; // phones load the phone clip if set, else the main one
	const state = phoneBytes <= 8 * MB ? 'ok' : phoneBytes <= 15 * MB ? 'warn' : 'bad';
	const fill = clampN( phoneBytes / ( 20 * MB ), 0, 1 );
	const label = {
		ok: __( 'Light — smooth on phones too.', 'gene-section-builder' ),
		warn: __( 'A bit heavy for phones.', 'gene-section-builder' ),
		bad: __( 'Too heavy for phones — best to lighten it.', 'gene-section-builder' ),
	}[ state ];
	const heavy = state === 'warn' || state === 'bad';
	return (
		<div style={ { marginTop: '12px', borderTop: '1px solid #e0e0e0', paddingTop: '12px' } }>
			<p style={ { fontSize: '12px', fontWeight: 600, margin: '0 0 6px' } }>
				{ __( 'Video weight', 'gene-section-builder' ) }
			</p>
			<div style={ { height: '8px', borderRadius: '4px', background: '#e0e0e0', overflow: 'hidden' } }>
				<div style={ { height: '100%', width: fill * 100 + '%', background: GAUGE_COLOR[ state ], transition: 'width .2s' } } />
			</div>
			<p style={ { fontSize: '12px', margin: '6px 0 0', color: GAUGE_COLOR[ state ], fontWeight: 600 } }>
				{ label }
			</p>
			<p style={ { fontSize: '12px', color: '#757575', margin: '2px 0 0' } }>
				{ mobBytes
					? __( 'Phone:', 'gene-section-builder' ) + ' ' + fmtMB( mobBytes ) + ' MB · ' +
					  __( 'Desktop:', 'gene-section-builder' ) + ' ' + fmtMB( mainBytes )+ ' MB'
					: fmtMB( mainBytes ) + ' MB ' + __( '(same video on phone and desktop)', 'gene-section-builder' ) }
			</p>
			{ heavy ? (
				<ul style={ { fontSize: '12px', color: '#50575e', margin: '8px 0 0', paddingLeft: '18px' } }>
					<li>{ __( 'Use a shorter clip — a few seconds is plenty.', 'gene-section-builder' ) }</li>
					<li>{ __( 'Re-export at 24fps, all-keyframes — same sharpness, smaller file.', 'gene-section-builder' ) }</li>
					<li>
						{ isPro
							? __( 'Set a separate, lighter video for phones (above).', 'gene-section-builder' )
							: __( 'A separate, lighter phone video is available in Pro.', 'gene-section-builder' ) }
					</li>
					<li>{ __( 'If it is still heavy, compress it and re-upload.', 'gene-section-builder' ) }</li>
				</ul>
			) : null }
			{ heavy ? (
				<div style={ { marginTop: '6px' } }>
					<code style={ { display: 'block', fontSize: '11px', background: '#f0f0f1', padding: '6px', borderRadius: '3px', wordBreak: 'break-all' } }>
						{ REENCODE_CMD }
					</code>
					<Button
						size="small"
						variant="secondary"
						style={ { marginTop: '4px' } }
						onClick={ () => {
							if ( window.navigator && window.navigator.clipboard ) {
								window.navigator.clipboard.writeText( REENCODE_CMD );
							}
						} }
					>
						{ __( 'Copy command', 'gene-section-builder' ) }
					</Button>
				</div>
			) : null }
		</div>
	);
}

// A beat's CTA buttons editor (label / url / style). Shared by the single hero
// and each multi-scene chapter heading.
function BeatButtons( { buttons, onChange } ) {
	const list = Array.isArray( buttons ) ? buttons : [];
	const set = ( i, key, val ) => {
		const next = list.slice();
		next[ i ] = { ...next[ i ], [ key ]: val };
		onChange( next );
	};
	const add = () => onChange( [ ...list, { label: '', url: '', style: 'solid' } ] );
	const remove = ( i ) => onChange( list.filter( ( b, k ) => k !== i ) );
	return (
		<div style={ { marginTop: '8px', borderTop: '1px dashed #e0e0e0', paddingTop: '8px' } }>
			<p style={ { fontSize: '11px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.04em', color: '#757575', margin: '0 0 6px' } }>
				{ __( 'Buttons', 'gene-section-builder' ) }
			</p>
			{ list.map( ( b, i ) => (
				<div key={ i } style={ { border: '1px solid #ededed', borderRadius: '3px', padding: '8px', marginBottom: '6px' } }>
					<div style={ { display: 'flex', justifyContent: 'flex-end' } }>
						<Button
							size="small"
							icon="trash"
							isDestructive
							label={ __( 'Remove button', 'gene-section-builder' ) }
							onClick={ () => remove( i ) }
						/>
					</div>
					<TextControl
						label={ __( 'Button text', 'gene-section-builder' ) }
						value={ b.label || '' }
						onChange={ ( v ) => set( i, 'label', v ) }
					/>
					<TextControl
						label={ __( 'Link (URL)', 'gene-section-builder' ) }
						value={ b.url || '' }
						onChange={ ( v ) => set( i, 'url', v ) }
					/>
					<SelectControl
						label={ __( 'Style', 'gene-section-builder' ) }
						value={ b.style === 'ghost' ? 'ghost' : 'solid' }
						options={ [
							{ label: __( 'Solid', 'gene-section-builder' ), value: 'solid' },
							{ label: __( 'Outline', 'gene-section-builder' ), value: 'ghost' },
						] }
						onChange={ ( v ) => set( i, 'style', v ) }
					/>
				</div>
			) ) }
			<Button variant="tertiary" onClick={ add }>
				{ __( 'Add button', 'gene-section-builder' ) }
			</Button>
		</div>
	);
}

// Free placement: one heading's position / scroll window / effect. Shared by
// the single hero's headings and each chapter's headings, so both edit the
// same way; `set(key, value)` writes to whichever beat list owns the heading.
function BeatPlacement( { b, i, count, set, setPhone, clearPhone } ) {
	const hasPhone = typeof b.xM === 'number' || typeof b.yM === 'number';
	return (
		<div style={ { marginTop: '8px', borderTop: '1px dashed #e0e0e0', paddingTop: '8px' } }>
			<p style={ { fontSize: '11px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.04em', color: '#757575', margin: '0 0 6px' } }>
				{ __( 'Placement & timing', 'gene-section-builder' ) }
			</p>
			<RangeControl
				label={ __( 'Position X (%)', 'gene-section-builder' ) }
				value={ beatPos( b, false ).x }
				min={ 0 }
				max={ 100 }
				onChange={ ( v ) => set( 'x', v ) }
			/>
			<RangeControl
				label={ __( 'Position Y (%)', 'gene-section-builder' ) }
				value={ beatPos( b, false ).y }
				min={ 0 }
				max={ 100 }
				onChange={ ( v ) => set( 'y', v ) }
			/>
			<RangeControl
				label={ __( 'Appears at (%)', 'gene-section-builder' ) }
				help={ __( 'Where in the scroll this heading arrives.', 'gene-section-builder' ) }
				value={ Math.round( beatWindow( i, count, b ).in * 100 ) }
				min={ 0 }
				max={ 100 }
				onChange={ ( v ) => set( 'in', v ) }
			/>
			<RangeControl
				label={ __( 'Leaves at (%)', 'gene-section-builder' ) }
				help={ __( 'Where it starts to go. It can overlap other headings.', 'gene-section-builder' ) }
				value={ Math.round( beatWindow( i, count, b ).out * 100 ) }
				min={ 0 }
				max={ 100 }
				onChange={ ( v ) => set( 'out', v ) }
			/>
			<SelectControl
				label={ __( 'Effect', 'gene-section-builder' ) }
				value={ b.anim || '' }
				options={ [
					{ label: __( 'Use shared effect', 'gene-section-builder' ), value: '' },
					{ label: __( 'Fade', 'gene-section-builder' ), value: 'fade' },
					{ label: __( 'Rise', 'gene-section-builder' ), value: 'rise' },
					{ label: __( 'Slide', 'gene-section-builder' ), value: 'slide' },
					{ label: __( 'Reveal', 'gene-section-builder' ), value: 'reveal' },
				] }
				onChange={ ( v ) => set( 'anim', v ) }
			/>
			<ToggleControl
				label={ __( 'Different position on phones', 'gene-section-builder' ) }
				checked={ hasPhone }
				onChange={ ( on ) => {
					if ( on ) {
						// One write for both keys — two sequential set() calls would
						// each start from the stale pre-render list and drop one.
						setPhone( beatPos( b, false ).x, beatPos( b, false ).y );
					} else {
						clearPhone();
					}
				} }
			/>
			{ hasPhone ? (
				<>
					<RangeControl
						label={ __( 'Phone X (%)', 'gene-section-builder' ) }
						value={ beatPos( b, true ).x }
						min={ 0 }
						max={ 100 }
						onChange={ ( v ) => set( 'xM', v ) }
					/>
					<RangeControl
						label={ __( 'Phone Y (%)', 'gene-section-builder' ) }
						value={ beatPos( b, true ).y }
						min={ 0 }
						max={ 100 }
						onChange={ ( v ) => set( 'yM', v ) }
					/>
				</>
			) : null }
		</div>
	);
}

// Static (non-navigable) preview of a beat's buttons for the editor canvas.
// Item helpers: a beat with no text but with buttons is a button item — an
// independent CTA the timeline places and times apart from any heading.
function beatText( b ) {
	return ( b.heading || '' ).trim() || ( b.eyebrow || '' ).trim() || ( b.sub || '' ).trim();
}
function isBtnItem( b ) {
	return ! beatText( b ) && Array.isArray( b.buttons ) && b.buttons.length > 0;
}
// Track row label: the heading's first line, else the first button's label.
function tlItemLabel( b ) {
	const h = ( ( b.heading || '' ).split( '\n' )[ 0 ] || '' ).trim();
	if ( h ) return h;
	const btn = ( Array.isArray( b.buttons ) ? b.buttons : [] ).find(
		( x ) => x && ( x.label || '' ).trim()
	);
	return btn ? btn.label : '';
}

// Timeline invariant: headings are headings, buttons are buttons. Split any
// beat carrying both into a heading item + a button item placed just below it.
// Every beat's window is baked to its current resolved value first, so the
// longer list doesn't shift anyone's timing. Returns null when nothing splits.
function splitHeadingButtons( bts ) {
	const src = Array.isArray( bts ) ? bts : [];
	const carries = ( b ) => b && beatText( b ) && Array.isArray( b.buttons ) && b.buttons.length;
	if ( ! src.some( carries ) ) return null;
	const n = src.length;
	const out = [];
	src.forEach( ( b, i ) => {
		if ( ! b ) return;
		const w = beatWindow( i, n, b );
		const win = { in: Math.round( w.in * 100 ), out: Math.round( w.out * 100 ) };
		if ( ! carries( b ) ) {
			out.push( { ...b, ...win } );
			return;
		}
		const head = { ...b, ...win };
		delete head.buttons;
		const p = beatPos( b, false );
		const btn = {
			buttons: b.buttons,
			x: p.x,
			y: Math.min( 100, Math.round( p.y + 14 ) ),
			...win,
		};
		if ( typeof b.xM === 'number' || typeof b.yM === 'number' ) {
			const pm = beatPos( b, true );
			btn.xM = pm.x;
			btn.yM = Math.min( 100, Math.round( pm.y + 14 ) );
		}
		if ( b.anim ) btn.anim = b.anim;
		out.push( head, btn );
	} );
	return out;
}

// Turning free placement off folds each button item back under the nearest
// heading above it (else the first heading), so the classic hero keeps its
// CTA. Returns null when there's nothing to merge — including when no heading
// exists at all (the button items then just wait, hidden, for the next on).
function mergeButtonItems( bts ) {
	const src = Array.isArray( bts ) ? bts : [];
	const isBtn = ( b ) => b && isBtnItem( b );
	if ( ! src.some( isBtn ) || ! src.some( ( b ) => b && beatText( b ) ) ) return null;
	const headIdx = [];
	src.forEach( ( b, i ) => {
		if ( b && beatText( b ) ) headIdx.push( i );
	} );
	const extra = {};
	src.forEach( ( b, i ) => {
		if ( ! isBtn( b ) ) return;
		let t = -1;
		for ( let k = headIdx.length - 1; k >= 0; k-- ) {
			if ( headIdx[ k ] < i ) {
				t = headIdx[ k ];
				break;
			}
		}
		if ( t === -1 ) t = headIdx[ 0 ];
		( extra[ t ] = extra[ t ] || [] ).push( ...b.buttons );
	} );
	const out = [];
	src.forEach( ( b, i ) => {
		if ( isBtn( b ) ) return;
		out.push(
			extra[ i ]
				? { ...b, buttons: [ ...( Array.isArray( b.buttons ) ? b.buttons : [] ), ...extra[ i ] ] }
				: b
		);
	} );
	return out;
}

// A ghost pill so a freshly added button item (no label yet) still has a
// visible body to drag on the stage.
function btnPlaceholder() {
	return (
		<div className="gene-hero__actions">
			<span className="gene-hero__btn gene-hero__btn--solid" style={ { opacity: 0.55 } }>
				{ __( 'Button', 'gene-section-builder' ) }
			</span>
		</div>
	);
}

function previewButtons( b ) {
	const btns = ( Array.isArray( b.buttons ) ? b.buttons : [] ).filter(
		( x ) => x && ( x.label || '' ).trim()
	);
	if ( ! btns.length ) return null;
	return (
		<div className="gene-hero__actions">
			{ btns.map( ( x, i ) => (
				<span
					key={ i }
					className={ 'gene-hero__btn gene-hero__btn--' + ( x.style === 'ghost' ? 'ghost' : 'solid' ) }
				>
					{ x.label }
				</span>
			) ) }
		</div>
	);
}

export default function Edit( { attributes, setAttributes } ) {
	const { background = {}, backgroundMobile = {}, bgDim = 10, scrubScreens = 4, beats = [], align, headlinePos = 'bottom-left', endShatter = false, shatterZone = 20, shatterStyle = 'glass', timeline = false, beatAnim = 'rise', multiScene = false, scenes = [], headingColor = '', buttonColor = '', buttonTextColor = '', fontPreset = '', buttonShape = 'pill' } = attributes;

	const hasBg = !! ( background && background.url );
	const isVideo = hasBg && background.type === 'video';
	const hasMobile = !! ( backgroundMobile && backgroundMobile.url );

	// Multi-scene (chapters, Pro): a sequence of background videos. When on and a
	// scene has a background, the canvas previews a chapter and the single-hero
	// timeline is set aside (they're different modes).
	const sceneList = Array.isArray( scenes ) && scenes.length ? scenes : [];
	const isMultiActive = isPro && !! multiScene && sceneList.some( ( sc ) => sc && sc.background && sc.background.url );

	// Always show at least one heading editor; a single beat is the free tier.
	const list = beats.length ? beats : [ {} ];
	const shown = isPro ? list : list.slice( 0, 1 );
	// Free placement (Pro) applies to whichever mode is active: the single
	// hero's headings (isTimeline — drives the drag stage below), or each
	// chapter's own headings when chapters play (isTimelineMulti).
	const isTimeline = isPro && !! timeline && list.length > 0 && ! isMultiActive;
	const isTimelineMulti = isPro && !! timeline && isMultiActive;

	const [ selScene, setSelScene ] = useState( 0 );
	const setSceneField = ( i, key, value ) => {
		const base = sceneList.slice();
		base[ i ] = { ...base[ i ], [ key ]: value };
		setAttributes( { scenes: base } );
	};
	const addScene = () =>
		setAttributes( { scenes: [ ...sceneList, { eyebrow: '', heading: '', sub: '', bgDim: 10 } ] } );
	const removeScene = ( i ) => setAttributes( { scenes: sceneList.filter( ( s, k ) => k !== i ) } );
	const moveScene = ( i, dir ) => {
		const j = i + dir;
		if ( j < 0 || j >= sceneList.length ) return;
		const next = sceneList.slice();
		const tmp = next[ i ];
		next[ i ] = next[ j ];
		next[ j ] = tmp;
		setAttributes( { scenes: next } );
	};
	const onSceneBg = ( i, m ) => {
		const mm = mapOne( m );
		if ( m && m.filesizeInBytes ) mm.bytes = Number( m.filesizeInBytes );
		const base = sceneList.slice();
		base[ i ] = { ...base[ i ], background: mm };
		const next = { scenes: base };
		if ( ! align ) next.align = 'full';
		setAttributes( next );
	};
	const onSceneMobile = ( i, m ) => {
		const mm = mapOne( m );
		if ( m && m.filesizeInBytes ) mm.bytes = Number( m.filesizeInBytes );
		setSceneField( i, 'backgroundMobile', mm );
	};

	// A chapter's editable headings: its `beats` list, else the flat single
	// heading it always had. Always returns at least one so there's an editor.
	const beatsOf = ( sc ) => {
		const raw = Array.isArray( sc.beats ) ? sc.beats : [];
		if ( raw.length ) return raw;
		if ( ( sc.heading || '' ).trim() || ( sc.eyebrow || '' ).trim() || ( sc.sub || '' ).trim() ) {
			return [ { eyebrow: sc.eyebrow || '', heading: sc.heading || '', sub: sc.sub || '' } ];
		}
		return [ {} ];
	};
	// Write a chapter's headings to `beats` and clear the flat fields, so `beats`
	// is the single source of truth once the author touches a heading.
	const setSceneBeats = ( i, bts ) => {
		const base = sceneList.slice();
		base[ i ] = { ...base[ i ], beats: bts, eyebrow: '', heading: '', sub: '' };
		setAttributes( { scenes: base } );
	};
	const setSceneBeat = ( i, bi, key, value ) => {
		const bts = beatsOf( sceneList[ i ] || {} ).slice();
		bts[ bi ] = { ...bts[ bi ], [ key ]: value };
		setSceneBeats( i, bts );
	};
	const addSceneBeat = ( i ) => setSceneBeats( i, [ ...beatsOf( sceneList[ i ] || {} ), {} ] );
	const removeSceneBeat = ( i, bi ) => {
		const bts = beatsOf( sceneList[ i ] || {} ).filter( ( b, k ) => k !== bi );
		setSceneBeats( i, bts.length ? bts : [ {} ] );
	};
	const moveSceneBeat = ( i, bi, dir ) => {
		const bts = beatsOf( sceneList[ i ] || {} ).slice();
		const j = bi + dir;
		if ( j < 0 || j >= bts.length ) return;
		const t = bts[ bi ];
		bts[ bi ] = bts[ j ];
		bts[ j ] = t;
		setSceneBeats( i, bts );
	};
	const setSceneBeatPhone = ( i, bi, x, y ) => {
		const bts = beatsOf( sceneList[ i ] || {} ).slice();
		bts[ bi ] = { ...bts[ bi ], xM: x, yM: y };
		setSceneBeats( i, bts );
	};
	const clearSceneBeatPhone = ( i, bi ) => {
		const bts = beatsOf( sceneList[ i ] || {} ).slice();
		const nb = { ...bts[ bi ] };
		delete nb.xM;
		delete nb.yM;
		bts[ bi ] = nb;
		setSceneBeats( i, bts );
	};

	// The chapter shown on the canvas (numbered tabs switch it). Computed here
	// because the timeline editor below edits this chapter's headings too.
	const selSceneIdx = Math.max( 0, Math.min( selScene, Math.max( 0, sceneList.length - 1 ) ) );
	const curScene = sceneList[ selSceneIdx ] || {};
	const curBg = curScene.background || {};
	const curBeats = beatsOf( curScene );

	// While free placement is on, a heading never carries buttons: any beat
	// still holding both (older content) is split into a heading item and a
	// button item the moment it's edited here. One write = one undo step.
	useEffect( () => {
		if ( ! isPro || ! timeline ) return;
		if ( isMultiActive ) {
			let changed = false;
			const next = sceneList.map( ( sc ) => {
				const s = splitHeadingButtons( Array.isArray( sc.beats ) ? sc.beats : [] );
				if ( ! s ) return sc;
				changed = true;
				return { ...sc, beats: s };
			} );
			if ( changed ) setAttributes( { scenes: next } );
		} else {
			const s = splitHeadingButtons( beats );
			if ( s ) setAttributes( { beats: s } );
		}
	}, [ isPro, timeline, isMultiActive, beats, sceneList ] );

	// The timeline toggle. Turning it off folds button items back under their
	// headings (both the single hero's and every chapter's) in the same write,
	// so the classic hero keeps its CTA and nothing silently disappears.
	const setTimelineMode = ( on ) => {
		if ( on ) {
			setAttributes( { timeline: true } ); // the split effect above tidies up
			return;
		}
		const patch = { timeline: false };
		const merged = mergeButtonItems( beats );
		if ( merged ) patch.beats = merged;
		if ( sceneList.length ) {
			let changed = false;
			const next = sceneList.map( ( sc ) => {
				const m = mergeButtonItems( Array.isArray( sc.beats ) ? sc.beats : [] );
				if ( ! m ) return sc;
				changed = true;
				return { ...sc, beats: m };
			} );
			if ( changed ) patch.scenes = next;
		}
		setAttributes( patch );
	};

	// Design choices previewed live on the canvas — the same CSS vars / data
	// attributes save.js writes, so what you see is what the front end gets.
	const designStyle = {};
	if ( headingColor ) designStyle[ '--gene-hero-head-c' ] = headingColor;
	if ( buttonColor ) designStyle[ '--gene-hero-btn-bg' ] = buttonColor;
	if ( buttonTextColor ) designStyle[ '--gene-hero-btn-c' ] = buttonTextColor;
	const designProps = { style: designStyle };
	if ( fontPreset === 'sans' || fontPreset === 'serif' || fontPreset === 'mono' ) {
		designProps[ 'data-font' ] = fontPreset;
	}
	if ( buttonShape === 'rounded' || buttonShape === 'square' ) {
		designProps[ 'data-btn-shape' ] = buttonShape;
	}

	const blockProps = useBlockProps( {
		className:
			'gene-hero gene-hero--editor' +
			( isTimeline || isTimelineMulti ? ' gene-hero--timeline-edit' : '' ),
		'data-has-bg': hasBg ? '1' : '0',
		'data-pos': headlinePos === 'center' ? 'center' : 'bottom-left',
		...designProps,
	} );

	const onSelectBackground = ( m ) => {
		const mapped = mapOne( m );
		if ( m && m.filesizeInBytes ) mapped.bytes = Number( m.filesizeInBytes );
		const next = { background: mapped };
		if ( ! align ) next.align = 'full';
		setAttributes( next );
	};

	const setBeat = ( i, key, value ) => {
		const base = beats.length ? beats.slice() : [ {} ];
		base[ i ] = { ...base[ i ], [ key ]: value };
		setAttributes( { beats: base } );
	};
	const setBeatPhone = ( i, x, y ) => {
		const base = beats.length ? beats.slice() : [ {} ];
		base[ i ] = { ...base[ i ], xM: x, yM: y };
		setAttributes( { beats: base } );
	};
	const clearBeatPhone = ( i ) => {
		const base = beats.length ? beats.slice() : [ {} ];
		const nb = { ...base[ i ] };
		delete nb.xM;
		delete nb.yM;
		base[ i ] = nb;
		setAttributes( { beats: base } );
	};
	const addBeat = () => setAttributes( { beats: [ ...list, { eyebrow: '', heading: '', sub: '' } ] } );
	const removeBeat = ( i ) => setAttributes( { beats: list.filter( ( b, k ) => k !== i ) } );
	const moveBeat = ( i, dir ) => {
		const j = i + dir;
		if ( j < 0 || j >= list.length ) return;
		const next = list.slice();
		const tmp = next[ i ];
		next[ i ] = next[ j ];
		next[ j ] = tmp;
		setAttributes( { beats: next } );
	};

	// --- Video weight gauge (editor guidance only) -------------------------
	// Sum the background video's file size (main + optional phone clip) and show
	// a simple meter with plain-language advice. It never changes the video — it
	// only tells the author what to do (shorten / re-export lighter / re-upload).
	// Size comes from the media library; if it's missing we read Content-Length.
	const [ measured, setMeasured ] = useState( {} );
	const bytesOf = ( m ) => ( m && Number( m.bytes ) ) || ( m && measured[ m.url ] ) || 0;
	// A media item that needs measuring: a video with a url, no stored bytes, and
	// not yet measured this session.
	const needsMeasure = ( m ) =>
		m && m.type === 'video' && m.url && ! m.bytes && ! measured[ m.url ] ? m.url : null;
	const scenesSig = sceneList
		.map( ( s ) => ( ( s.background && s.background.url ) || '' ) + '|' + ( ( s.backgroundMobile && s.backgroundMobile.url ) || '' ) )
		.join( ',' );
	useEffect( () => {
		const want = [];
		if ( needsMeasure( background ) ) want.push( background.url );
		if ( hasMobile && needsMeasure( backgroundMobile ) ) want.push( backgroundMobile.url );
		// Each chapter's clips too, so the per-chapter weight gauges can read sizes.
		sceneList.forEach( ( s ) => {
			const a = needsMeasure( s.background );
			if ( a ) want.push( a );
			const b = needsMeasure( s.backgroundMobile );
			if ( b ) want.push( b );
		} );
		want.forEach( ( u ) => {
			if ( typeof window.fetch !== 'function' ) return;
			window
				.fetch( u, { method: 'HEAD' } )
				.then( ( r ) => {
					const cl = r.headers.get( 'content-length' );
					if ( cl ) setMeasured( ( prev ) => ( { ...prev, [ u ]: Number( cl ) } ) );
				} )
				.catch( () => {} );
		} );
	}, [ isVideo, background.url, background.bytes, hasMobile, backgroundMobile.url, backgroundMobile.bytes, scenesSig ] );

	// --- Frame-synced timeline editor (Pro) --------------------------------
	// A real editing surface: drag each heading on the hero, trim its scroll
	// window on a track below, and — for a video background — the frame you're
	// looking at scrubs to the moment being edited, so placement is done against
	// the actual footage. Everything commits on pointer-up (one undo step); the
	// numeric controls in the sidebar stay as the accessible fine-tune.
	// The same surface edits either the single hero's headings or — in chapter
	// mode — the selected chapter's own headings (tlList routes both).
	const stageRef = useRef( null );
	const trackRef = useRef( null );
	const [ selected, setSelected ] = useState( 0 );
	const [ device, setDevice ] = useState( 'desktop' );
	const [ scrubAt, setScrubAt ] = useState( null ); // 0..1, or null → use selected's `in`
	const [ drag, setDrag ] = useState( null );
	const tlOn = isTimeline || isTimelineMulti;
	const tlList = isTimelineMulti ? curBeats : list;
	const sel = Math.min( selected, tlList.length - 1 );
	const phone = device === 'phone';

	// The background whose frame the stage scrubs: the chapter's in multi mode.
	const tlBg = isTimelineMulti ? curBg : background;
	const tlIsVideo = !! ( tlBg && tlBg.url && tlBg.type === 'video' );
	// A chapter spends its last TRANS breaking to the next while headings run
	// over the hold before it (view.js) — map window fractions onto video time.
	const tlHold = isTimelineMulti ? 1 - 0.16 : 1;

	// The video frame to show: whatever moment is being edited, else selected's in.
	const seekAt =
		scrubAt != null ? scrubAt : beatWindow( sel, tlList.length, tlList[ sel ] || {} ).in;
	useEffect( () => {
		if ( ! tlOn || ! tlIsVideo || ! stageRef.current ) return;
		const v = stageRef.current.querySelector( '.gene-hero__bg video' );
		if ( ! v ) return;
		v.preload = 'auto';
		const seek = () => {
			if ( v.duration ) {
				try { v.currentTime = clampN( seekAt * tlHold, 0, 0.999 ) * v.duration; } catch ( e ) {}
			}
		};
		if ( v.readyState >= 1 ) seek();
		else v.addEventListener( 'loadedmetadata', seek, { once: true } );
	}, [ seekAt, tlOn, tlIsVideo, tlHold, tlBg && tlBg.url ] );

	// Effective (drag-aware) values for rendering while dragging.
	const effPos = ( i ) =>
		drag && drag.i === i && drag.x != null
			? { x: drag.x, y: drag.y }
			: beatPos( tlList[ i ] || {}, phone );
	const effWin = ( i ) =>
		drag && drag.i === i && drag.in != null
			? { in: drag.in, out: drag.out }
			: beatWindow( i, tlList.length, tlList[ i ] || {} );

	// Commit a drag to whichever list is being edited (one write = one undo).
	const writeTlBeat = ( i, patch ) => {
		if ( isTimelineMulti ) {
			const bts = curBeats.slice();
			bts[ i ] = { ...bts[ i ], ...patch };
			setSceneBeats( selSceneIdx, bts );
			return;
		}
		const base = beats.length ? beats.slice() : [ {} ];
		base[ i ] = { ...base[ i ], ...patch };
		setAttributes( { beats: base } );
	};
	const commitPos = ( i, x, y ) => {
		const rx = Math.round( clampN( x, 0, 100 ) );
		const ry = Math.round( clampN( y, 0, 100 ) );
		writeTlBeat( i, phone ? { xM: rx, yM: ry } : { x: rx, y: ry } );
	};
	const commitWin = ( i, inFrac, outFrac ) => {
		writeTlBeat( i, {
			in: Math.round( clampN( inFrac, 0, 1 ) * 100 ),
			out: Math.round( clampN( outFrac, 0, 1 ) * 100 ),
		} );
	};

	// Canvas drag: place a heading. Coordinates are % of the stage.
	const onBeatDown = ( e, i ) => {
		if ( ! tlOn || ! stageRef.current ) return;
		e.preventDefault();
		e.stopPropagation();
		setSelected( i );
		const rect = stageRef.current.getBoundingClientRect();
		const p = beatPos( tlList[ i ] || {}, phone );
		setDrag( { kind: 'move', i, rect, x: p.x, y: p.y } );
		setScrubAt( beatWindow( i, tlList.length, tlList[ i ] || {} ).in );
		try { e.currentTarget.setPointerCapture( e.pointerId ); } catch ( err ) {}
	};
	const onBeatMove = ( e, i ) => {
		if ( ! drag || drag.kind !== 'move' || drag.i !== i ) return;
		const rx = clampN( ( ( e.clientX - drag.rect.left ) / drag.rect.width ) * 100, 0, 100 );
		const ry = clampN( ( ( e.clientY - drag.rect.top ) / drag.rect.height ) * 100, 0, 100 );
		const sx = snapPct( rx ), sy = snapPct( ry );
		setDrag( { ...drag, x: sx.v, y: sy.v, snapX: sx.hit, snapY: sy.hit } );
	};
	const onBeatUp = ( e, i ) => {
		if ( ! drag || drag.i !== i ) return;
		if ( drag.kind === 'move' ) commitPos( i, drag.x, drag.y );
		setDrag( null );
		try { e.currentTarget.releasePointerCapture( e.pointerId ); } catch ( err ) {}
	};
	// Keyboard nudge: move the selected heading by 1% (Shift = 5%). Each press is
	// one undo step — the accessible counterpart to dragging.
	const onBeatKey = ( e, i ) => {
		if ( ! tlOn ) return;
		const step = e.shiftKey ? 5 : 1;
		const p = beatPos( tlList[ i ] || {}, phone );
		let x = p.x, y = p.y, hit = true;
		if ( e.key === 'ArrowLeft' ) x -= step;
		else if ( e.key === 'ArrowRight' ) x += step;
		else if ( e.key === 'ArrowUp' ) y -= step;
		else if ( e.key === 'ArrowDown' ) y += step;
		else hit = false;
		if ( ! hit ) return;
		e.preventDefault();
		setSelected( i );
		commitPos( i, x, y );
	};

	// Track drag: trim a heading's scroll window (or move the whole band).
	const trackFrac = ( clientX ) => {
		const r = trackRef.current.getBoundingClientRect();
		return clampN( ( clientX - r.left ) / ( r.width || 1 ), 0, 1 );
	};
	const onBandDown = ( e, i, kind ) => {
		e.preventDefault();
		e.stopPropagation();
		setSelected( i );
		const w = beatWindow( i, tlList.length, tlList[ i ] || {} );
		const at = trackFrac( e.clientX );
		setDrag( { kind, i, in: w.in, out: w.out, grab: at, base: w } );
		setScrubAt( kind === 'out' ? w.out : w.in );
		try { e.currentTarget.setPointerCapture( e.pointerId ); } catch ( err ) {}
	};
	const onBandMove = ( e, i ) => {
		if ( ! drag || drag.i !== i || ( drag.kind !== 'in' && drag.kind !== 'out' && drag.kind !== 'band' ) ) return;
		const at = trackFrac( e.clientX );
		let ni = drag.in, no = drag.out;
		if ( drag.kind === 'in' ) { ni = snapFrac( Math.min( at, drag.out ) ); setScrubAt( ni ); }
		else if ( drag.kind === 'out' ) { no = snapFrac( Math.max( at, drag.in ) ); setScrubAt( no ); }
		else {
			const span = drag.base.out - drag.base.in;
			let start = snapFrac( clampN( drag.base.in + ( at - drag.grab ), 0, 1 - span ) );
			ni = start; no = start + span; setScrubAt( ni );
		}
		setDrag( { ...drag, in: ni, out: no } );
	};
	const onBandUp = ( e, i ) => {
		if ( ! drag || drag.i !== i ) return;
		if ( drag.kind === 'in' || drag.kind === 'out' || drag.kind === 'band' ) commitWin( i, drag.in, drag.out );
		setDrag( null );
		try { e.currentTarget.releasePointerCapture( e.pointerId ); } catch ( err ) {}
	};

	// Shared timeline chrome: the toolbar (drag tip + Desktop/Phone), the snap
	// guides, and the window track. Rendered by both the single-hero stage and
	// the chapter stage — the handlers above already route to the right list.
	const tlBar = (
		<div className="gene-hero__tl-bar" contentEditable={ false }>
			<span className="gene-hero__tl-tip">
				{ __( 'Drag each heading to place it. Trim when it appears on the track below.', 'gene-section-builder' ) }
			</span>
			<span className="gene-hero__tl-dev">
				<Button size="small" isPressed={ ! phone } onClick={ () => setDevice( 'desktop' ) }>
					{ __( 'Desktop', 'gene-section-builder' ) }
				</Button>
				<Button size="small" isPressed={ phone } onClick={ () => setDevice( 'phone' ) }>
					{ __( 'Phone', 'gene-section-builder' ) }
				</Button>
			</span>
		</div>
	);
	const tlGuides =
		drag && drag.kind === 'move' ? (
			<div className="gene-hero__tl-guides" contentEditable={ false }>
				<div
					className={ 'gene-hero__tl-guide gene-hero__tl-guide--v' + ( drag.snapX ? ' is-snap' : '' ) }
					style={ { left: ( drag.snapX ? drag.x : 50 ) + '%' } }
				/>
				<div
					className={ 'gene-hero__tl-guide gene-hero__tl-guide--h' + ( drag.snapY ? ' is-snap' : '' ) }
					style={ { top: ( drag.snapY ? drag.y : 50 ) + '%' } }
				/>
			</div>
		) : null;
	const tlTrack = (
		<div className="gene-hero__tl-track-wrap" contentEditable={ false }>
			<div
				className="gene-hero__tl-track"
				ref={ trackRef }
				style={ { height: tlList.length * 26 + 10 + 'px' } }
			>
				{ tlList.map( ( b, i ) => {
					const w = effWin( i );
					const isSel = i === sel;
					const bandHandlers = ( kind ) => ( {
						onPointerDown: ( e ) => onBandDown( e, i, kind ),
						onPointerMove: ( e ) => onBandMove( e, i ),
						onPointerUp: ( e ) => onBandUp( e, i ),
					} );
					return (
						<div
							key={ i }
							className={ 'gene-hero__tl-band' + ( isSel ? ' is-selected' : '' ) }
							style={ {
								left: w.in * 100 + '%',
								width: Math.max( 2, ( w.out - w.in ) * 100 ) + '%',
								top: i * 26 + 5 + 'px',
							} }
							{ ...bandHandlers( 'band' ) }
						>
							<span className="gene-hero__tl-h gene-hero__tl-h--in" { ...bandHandlers( 'in' ) } />
							<span className="gene-hero__tl-label">
								{ i + 1 }. { tlItemLabel( b ) }
							</span>
							<span className="gene-hero__tl-h gene-hero__tl-h--out" { ...bandHandlers( 'out' ) } />
						</div>
					);
				} ) }
				<div className="gene-hero__tl-playhead" style={ { left: seekAt * 100 + '%' } } />
			</div>
			<div className="gene-hero__tl-scale">
				<span>
					{ isTimelineMulti
						? __( 'Chapter start', 'gene-section-builder' )
						: __( 'Scroll start', 'gene-section-builder' ) }
				</span>
				<span>
					{ isTimelineMulti
						? __( 'Chapter end', 'gene-section-builder' )
						: __( 'Scroll end', 'gene-section-builder' ) }
				</span>
			</div>
		</div>
	);

	// Multi-scene canvas: preview one chapter (the sequence plays only on the
	// live page). A row of numbered tabs switches which chapter is shown. With
	// free placement on, the chapter becomes the same drag stage as the single
	// hero: drag each heading, trim its window on the track, frame-synced.
	const curBeat = curBeats[ 0 ] || {};
	const multiCanvas = (
		<>
			<div className="gene-hero__ms-bar" contentEditable={ false }>
				{ sceneList.map( ( sc, i ) => (
					<button
						key={ i }
						type="button"
						className={ 'gene-hero__ms-tab' + ( i === selSceneIdx ? ' is-on' : '' ) }
						onClick={ () => {
							setSelScene( i );
							setSelected( 0 );
							setScrubAt( null );
							setDrag( null );
						} }
					>
						{ i + 1 }
					</button>
				) ) }
				{ ! isTimelineMulti ? (
					<span className="gene-hero__ms-tip">
						{ __( 'Preview a chapter. Each plays with the scroll on the live page.', 'gene-section-builder' ) }
					</span>
				) : null }
			</div>
			{ isTimelineMulti ? tlBar : null }
			<div className="gene-hero__pin" ref={ stageRef }>
				{ curBg.url ? (
					<div className="gene-hero__bg">
						<Media m={ curBg } />
						<div
							className="gene-hero__scrim"
							style={ { '--dim': ( ( typeof curScene.bgDim === 'number' ? curScene.bgDim : 0 ) || 0 ) / 100 } }
						/>
					</div>
				) : null }
				{ isTimelineMulti ? (
					/* Free placement: this chapter's headings, each draggable at its
					   own (x%, y%) — same stage as the single hero. The windows play
					   over this chapter's scroll segment on the live page. */
					<div className="gene-hero__beats">
						{ curBeats.map( ( bt, bi ) => {
							const p = effPos( bi );
							return (
								<div
									key={ bi }
									className={
										'gene-hero__beat' +
										( isBtnItem( bt ) ? ' gene-hero__beat--btn' : '' ) +
										( bi === sel ? ' is-selected' : '' )
									}
									style={ {
										left: p.x + '%',
										top: p.y + '%',
										transform: 'translate(-50%, -50%)',
										cursor: 'grab',
										touchAction: 'none',
									} }
									tabIndex={ 0 }
									onPointerDown={ ( e ) => onBeatDown( e, bi ) }
									onPointerMove={ ( e ) => onBeatMove( e, bi ) }
									onPointerUp={ ( e ) => onBeatUp( e, bi ) }
									onKeyDown={ ( e ) => onBeatKey( e, bi ) }
								>
									{ ( bt.eyebrow || '' ).trim() ? (
										<p className="gene-hero__eyebrow">{ bt.eyebrow }</p>
									) : null }
									{ ( bt.heading || '' ).trim() ? (
										<h2 className="gene-hero__head">{ lines( bt.heading ) }</h2>
									) : null }
									{ ( bt.sub || '' ).trim() ? (
										<p className="gene-hero__sub">{ bt.sub }</p>
									) : null }
									{ previewButtons( bt ) || ( isBtnItem( bt ) ? btnPlaceholder() : null ) }
								</div>
							);
						} ) }
					</div>
				) : (
				<div className="gene-hero__beats">
					{ ( curBeat.heading || '' ).trim() || ( curBeat.eyebrow || '' ).trim() || ( curBeat.sub || '' ).trim() ? (
						<div className="gene-hero__beat">
							{ ( curBeat.eyebrow || '' ).trim() ? (
								<p className="gene-hero__eyebrow">{ curBeat.eyebrow }</p>
							) : null }
							{ ( curBeat.heading || '' ).trim() ? (
								<h2 className="gene-hero__head">{ lines( curBeat.heading ) }</h2>
							) : null }
							{ ( curBeat.sub || '' ).trim() ? (
								<p className="gene-hero__sub">{ curBeat.sub }</p>
							) : null }
							{ previewButtons( curBeat ) }
							{ curBeats.length > 1 ? (
								<p className="gene-hero__ms-count">
									{ curBeats.length + ' ' + __( 'headings swap here', 'gene-section-builder' ) }
								</p>
							) : null }
						</div>
					) : (
						<p className="gene-hero__hint">
							{ __( 'Set a background and heading for this chapter.', 'gene-section-builder' ) }
						</p>
					) }
				</div>
				) }
				{ isTimelineMulti ? tlGuides : null }
			</div>
			{ isTimelineMulti ? tlTrack : null }
		</>
	);

	return (
		<>
			<InspectorControls>
				{ /* Single-hero panels. While chapters play they'd be inert (the
				     chapters replace the single background + headings), so they
				     step aside and the Chapters panel leads. */ }
				{ ! isMultiActive ? (
				<>
				<PanelBody title={ __( 'Background', 'gene-section-builder' ) }>
					<MediaUploadCheck>
						<MediaUpload
							allowedTypes={ [ 'image', 'video' ] }
							value={ background && background.id }
							onSelect={ onSelectBackground }
							render={ ( { open } ) => (
								<Button variant="secondary" onClick={ open } style={ { marginBottom: '8px' } }>
									{ hasBg
										? __( 'Replace background', 'gene-section-builder' )
										: __( 'Set a background (image or video)', 'gene-section-builder' ) }
								</Button>
							) }
						/>
					</MediaUploadCheck>
					{ hasBg ? (
						<>
							<Button
								variant="link"
								isDestructive
								onClick={ () => setAttributes( { background: {} } ) }
								style={ { display: 'block', marginBottom: '8px' } }
							>
								{ __( 'Remove background', 'gene-section-builder' ) }
							</Button>
							<RangeControl
								label={ __( 'Dim overlay (%)', 'gene-section-builder' ) }
								value={ bgDim }
								min={ 0 }
								max={ 85 }
								onChange={ ( v ) => setAttributes( { bgDim: v } ) }
							/>
							{ isVideo ? (
								<p style={ { fontSize: '12px', color: '#757575', margin: '4px 0 0' } }>
									{ __(
										'The video scrubs with scroll. Export it with every frame as a keyframe (ffmpeg -g 1) for smooth seeking.',
										'gene-section-builder'
									) }
								</p>
							) : null }
							{ isVideo && isPro ? (
								<div style={ { marginTop: '12px', borderTop: '1px solid #e0e0e0', paddingTop: '12px' } }>
									<p style={ { fontSize: '12px', fontWeight: 600, margin: '0 0 6px' } }>
										{ __( 'Mobile video (optional)', 'gene-section-builder' ) }
									</p>
									<MediaUploadCheck>
										<MediaUpload
											allowedTypes={ [ 'video' ] }
											value={ backgroundMobile && backgroundMobile.id }
											onSelect={ ( m ) => {
												const mm = mapOne( m );
												if ( m && m.filesizeInBytes ) mm.bytes = Number( m.filesizeInBytes );
												setAttributes( { backgroundMobile: mm } );
											} }
											render={ ( { open } ) => (
												<Button variant="secondary" onClick={ open }>
													{ hasMobile
														? __( 'Replace mobile video', 'gene-section-builder' )
														: __( 'Set a mobile video', 'gene-section-builder' ) }
												</Button>
											) }
										/>
									</MediaUploadCheck>
									{ hasMobile ? (
										<Button
											variant="link"
											isDestructive
											onClick={ () => setAttributes( { backgroundMobile: {} } ) }
											style={ { display: 'block', marginTop: '6px' } }
										>
											{ __( 'Remove mobile video', 'gene-section-builder' ) }
										</Button>
									) : null }
									<p style={ { fontSize: '12px', color: '#757575', margin: '6px 0 0' } }>
										{ __(
											'Shown on phones (≤781px) in place of the main video. Export it all-keyframes too (ffmpeg -g 1).',
											'gene-section-builder'
										) }
									</p>
								</div>
							) : null }
							{ isVideo && ! isPro ? (
								<p style={ { fontSize: '12px', color: '#757575', margin: '8px 0 0' } }>
									{ __(
										'Show a separate video on phones with Scroll Hero Pro.',
										'gene-section-builder'
									) }
								</p>
							) : null }
							{ isVideo ? (
								<WeightGauge
									mainBytes={ bytesOf( background ) }
									mobBytes={ hasMobile && backgroundMobile.type === 'video' ? bytesOf( backgroundMobile ) : 0 }
									isPro={ isPro }
								/>
							) : null }
						</>
					) : null }
				</PanelBody>

				<PanelBody title={ __( 'Heading', 'gene-section-builder' ) }>
					{ shown.map( ( b, i ) => (
						<div
							key={ i }
							style={ {
								border: '1px solid #e0e0e0',
								borderRadius: '4px',
								padding: '10px',
								marginBottom: '12px',
							} }
						>
							{ isPro && list.length > 1 ? (
								<div
									style={ {
										display: 'flex',
										justifyContent: 'space-between',
										alignItems: 'center',
										marginBottom: '6px',
									} }
								>
									<strong style={ { fontSize: '12px' } }>
										{ isBtnItem( b )
											? __( 'Button', 'gene-section-builder' )
											: __( 'Heading', 'gene-section-builder' ) }{ ' ' }
										{ i + 1 }
									</strong>
									<span>
										<Button
											size="small"
											icon="arrow-up-alt2"
											label={ __( 'Move up', 'gene-section-builder' ) }
											disabled={ i === 0 }
											onClick={ () => moveBeat( i, -1 ) }
										/>
										<Button
											size="small"
											icon="arrow-down-alt2"
											label={ __( 'Move down', 'gene-section-builder' ) }
											disabled={ i === list.length - 1 }
											onClick={ () => moveBeat( i, 1 ) }
										/>
										<Button
											size="small"
											icon="trash"
											isDestructive
											label={ __( 'Remove', 'gene-section-builder' ) }
											onClick={ () => removeBeat( i ) }
										/>
									</span>
								</div>
							) : null }
							{ ! isBtnItem( b ) ? (
								<>
									<TextControl
										label={ __( 'Label', 'gene-section-builder' ) }
										value={ b.eyebrow || '' }
										onChange={ ( v ) => setBeat( i, 'eyebrow', v ) }
									/>
									<TextareaControl
										label={ __( 'Heading', 'gene-section-builder' ) }
										help={ __( 'Line breaks are kept.', 'gene-section-builder' ) }
										value={ b.heading || '' }
										onChange={ ( v ) => setBeat( i, 'heading', v ) }
									/>
									<TextControl
										label={ __( 'Sub-line', 'gene-section-builder' ) }
										value={ b.sub || '' }
										onChange={ ( v ) => setBeat( i, 'sub', v ) }
									/>
								</>
							) : null }
							{ /* With the timeline on, buttons live as their own items (Add
							     button below) — heading+buttons beats are auto-split, so
							     headings never show an inline buttons editor there. */ }
							{ isBtnItem( b ) || ! isTimeline ? (
								<BeatButtons buttons={ b.buttons } onChange={ ( v ) => setBeat( i, 'buttons', v ) } />
							) : null }
							{ isTimeline ? (
								<BeatPlacement
									b={ b }
									i={ i }
									count={ list.length }
									set={ ( k, v ) => setBeat( i, k, v ) }
									setPhone={ ( x, y ) => setBeatPhone( i, x, y ) }
									clearPhone={ () => clearBeatPhone( i ) }
								/>
							) : null }
						</div>
					) ) }

					{ isPro ? (
						<>
							<Button variant="primary" onClick={ addBeat }>
								{ __( 'Add heading', 'gene-section-builder' ) }
							</Button>
							{ isTimeline ? (
								<Button
									variant="secondary"
									style={ { marginLeft: '8px' } }
									onClick={ () =>
										setAttributes( {
											beats: [ ...list, { buttons: [ { label: '', url: '', style: 'solid' } ] } ],
										} )
									}
								>
									{ __( 'Add button', 'gene-section-builder' ) }
								</Button>
							) : null }
							<div style={ { marginTop: '16px' } }>
								<SelectControl
									label={ __( 'Headline placement', 'gene-section-builder' ) }
									value={ headlinePos === 'center' ? 'center' : 'bottom-left' }
									options={ [
										{ label: __( 'Bottom left', 'gene-section-builder' ), value: 'bottom-left' },
										{ label: __( 'Centered', 'gene-section-builder' ), value: 'center' },
									] }
									onChange={ ( v ) => setAttributes( { headlinePos: v } ) }
								/>
							</div>
						</>
					) : (
						<Notice status="info" isDismissible={ false }>
							{ __(
								'Add more headings that appear one after another — and place them freely on a scroll timeline — with Scroll Hero Pro.',
								'gene-section-builder'
							) }
							<div style={ { marginTop: '8px' } }>
								<Button variant="secondary" href={ upgradeUrl } target="_blank" rel="noopener">
									{ __( 'Get Pro', 'gene-section-builder' ) }
								</Button>
							</div>
						</Notice>
					) }
				</PanelBody>

				{ /* Everything that shapes the scroll ride, in one place. */ }
				{ isPro || isVideo ? (
					<PanelBody title={ __( 'Scroll & motion', 'gene-section-builder' ) } initialOpen={ false }>
						{ isPro ? (
							<RangeControl
								label={ __( 'Scroll length (screens)', 'gene-section-builder' ) }
								help={ __(
									'How many viewport-heights of scrolling play through the hero.',
									'gene-section-builder'
								) }
								value={ scrubScreens }
								min={ 2 }
								max={ 8 }
								onChange={ ( v ) => setAttributes( { scrubScreens: v } ) }
							/>
						) : null }
						{ isPro ? (
							<>
								<ToggleControl
									label={ __( 'Free placement (timeline)', 'gene-section-builder' ) }
									help={ __(
										'Place each heading anywhere over the hero and set when it appears and leaves as visitors scroll — like slides on a timeline.',
										'gene-section-builder'
									) }
									checked={ !! timeline }
									onChange={ ( v ) => setTimelineMode( v ) }
								/>
								{ timeline ? (
									<SelectControl
										label={ __( 'Shared effect', 'gene-section-builder' ) }
										help={ __( 'Used by every heading unless it sets its own.', 'gene-section-builder' ) }
										value={ beatAnim }
										options={ [
											{ label: __( 'Fade', 'gene-section-builder' ), value: 'fade' },
											{ label: __( 'Rise', 'gene-section-builder' ), value: 'rise' },
											{ label: __( 'Slide', 'gene-section-builder' ), value: 'slide' },
											{ label: __( 'Reveal', 'gene-section-builder' ), value: 'reveal' },
										] }
										onChange={ ( v ) => setAttributes( { beatAnim: v } ) }
									/>
								) : null }
							</>
						) : null }
						{ isVideo ? (
							<div style={ isPro ? { marginTop: '12px', borderTop: '1px solid #e0e0e0', paddingTop: '12px' } : undefined }>
								<ToggleControl
									label={ __( 'Break apart at the end', 'gene-section-builder' ) }
									help={ __(
										'When the scroll reaches the end, the video breaks apart. Scrolling back reassembles it.',
										'gene-section-builder'
									) }
									checked={ !! endShatter }
									onChange={ ( v ) => setAttributes( { endShatter: v } ) }
								/>
								{ endShatter && isPro ? (
									<SelectControl
										label={ __( 'Break style', 'gene-section-builder' ) }
										value={ shatterStyle === 'split' ? 'split' : 'glass' }
										options={ [
											{ label: __( 'Glass shards', 'gene-section-builder' ), value: 'glass' },
											{ label: __( 'Split apart', 'gene-section-builder' ), value: 'split' },
										] }
										onChange={ ( v ) => setAttributes( { shatterStyle: v } ) }
									/>
								) : null }
								{ endShatter && isPro ? (
									<RangeControl
										label={ __( 'Break length (screens)', 'gene-section-builder' ) }
										help={ __(
											'How much scrolling the break takes. Shorter feels snappier.',
											'gene-section-builder'
										) }
										value={ shatterZone }
										min={ 8 }
										max={ 40 }
										step={ 2 }
										onChange={ ( v ) => setAttributes( { shatterZone: v } ) }
									/>
								) : null }
								{ endShatter && ! isPro ? (
									<p style={ { fontSize: '12px', color: '#757575', margin: '4px 0 0' } }>
										{ __(
											'Choose the break style (glass or split) and tune its speed with Scroll Hero Pro.',
											'gene-section-builder'
										) }
									</p>
								) : null }
							</div>
						) : null }
					</PanelBody>
				) : null }
				</>
				) : null }

			<PanelBody title={ __( 'Chapters (multi-scene)', 'gene-section-builder' ) } initialOpen={ !! multiScene }>
					{ isPro ? (
						<>
							<ToggleControl
								label={ __( 'Play chapters as you scroll', 'gene-section-builder' ) }
								help={ __(
									'Add several background videos; each scrubs with the scroll, then hands off to the next. While chapters play, the hero\'s single background and headings step aside.',
									'gene-section-builder'
								) }
								checked={ !! multiScene }
								onChange={ ( v ) => setAttributes( { multiScene: v } ) }
							/>
							{ multiScene ? (
								<>
									{ sceneList.length === 0 ? (
										<p style={ { fontSize: '12px', color: '#757575', margin: '8px 0' } }>
											{ __(
												'Add a chapter to begin. Each chapter is one background (a video works best) with a heading.',
												'gene-section-builder'
											) }
										</p>
									) : null }
									{ sceneList.map( ( sc, i ) => {
										const bg = sc.background || {};
										const scVideo = bg.type === 'video';
										const scDim = typeof sc.bgDim === 'number' ? sc.bgDim : 10;
										return (
											<div
												key={ i }
												style={ {
													border: '1px solid #e0e0e0',
													borderRadius: '4px',
													padding: '10px',
													marginBottom: '12px',
												} }
											>
												<div
													style={ {
														display: 'flex',
														justifyContent: 'space-between',
														alignItems: 'center',
														marginBottom: '6px',
													} }
												>
													<strong style={ { fontSize: '12px' } }>
														{ __( 'Chapter', 'gene-section-builder' ) } { i + 1 }
													</strong>
													<span>
														<Button
															size="small"
															icon="arrow-up-alt2"
															label={ __( 'Move up', 'gene-section-builder' ) }
															disabled={ i === 0 }
															onClick={ () => moveScene( i, -1 ) }
														/>
														<Button
															size="small"
															icon="arrow-down-alt2"
															label={ __( 'Move down', 'gene-section-builder' ) }
															disabled={ i === sceneList.length - 1 }
															onClick={ () => moveScene( i, 1 ) }
														/>
														<Button
															size="small"
															icon="trash"
															isDestructive
															label={ __( 'Remove', 'gene-section-builder' ) }
															onClick={ () => removeScene( i ) }
														/>
													</span>
												</div>
												<MediaUploadCheck>
													<MediaUpload
														allowedTypes={ [ 'image', 'video' ] }
														value={ bg.id }
														onSelect={ ( m ) => onSceneBg( i, m ) }
														render={ ( { open } ) => (
															<Button
																variant="secondary"
																onClick={ () => {
																	setSelScene( i );
																	open();
																} }
																style={ { marginBottom: '8px' } }
															>
																{ bg.url
																	? __( 'Replace background', 'gene-section-builder' )
																	: __( 'Set a background (image or video)', 'gene-section-builder' ) }
															</Button>
														) }
													/>
												</MediaUploadCheck>
												{ bg.url ? (
													<Button
														variant="link"
														isDestructive
														onClick={ () => setSceneField( i, 'background', {} ) }
														style={ { display: 'block', marginBottom: '8px' } }
													>
														{ __( 'Remove background', 'gene-section-builder' ) }
													</Button>
												) : null }
												{ scVideo ? (
													<p style={ { fontSize: '12px', color: '#757575', margin: '0 0 8px' } }>
														{ __(
															'Export it all-keyframes (ffmpeg -g 1) so the scrub stays smooth.',
															'gene-section-builder'
														) }
													</p>
												) : null }
												<RangeControl
													label={ __( 'Dim overlay (%)', 'gene-section-builder' ) }
													value={ scDim }
													min={ 0 }
													max={ 85 }
													onChange={ ( v ) => setSceneField( i, 'bgDim', v ) }
												/>
												{ /* Free placement positions each heading itself. */ }
												{ ! timeline ? (
												<SelectControl
													label={ __( 'Headline placement', 'gene-section-builder' ) }
													help={ __( 'This chapter only — each chapter can put its headline somewhere else.', 'gene-section-builder' ) }
													value={
														sc.pos === 'bottom-left' || sc.pos === 'center' || sc.pos === 'bottom-right'
															? sc.pos
															: ''
													}
													options={ [
														{ label: __( 'Default (hero setting)', 'gene-section-builder' ), value: '' },
														{ label: __( 'Bottom left', 'gene-section-builder' ), value: 'bottom-left' },
														{ label: __( 'Centered', 'gene-section-builder' ), value: 'center' },
														{ label: __( 'Bottom right', 'gene-section-builder' ), value: 'bottom-right' },
													] }
													onChange={ ( v ) => setSceneField( i, 'pos', v ) }
												/>
												) : null }
												{ beatsOf( sc ).map( ( bt, bi, arr ) => (
													<div
														key={ bi }
														style={
															arr.length > 1
																? { border: '1px solid #ededed', borderRadius: '3px', padding: '8px', marginBottom: '8px' }
																: { marginBottom: '4px' }
														}
													>
														{ arr.length > 1 ? (
															<div
																style={ {
																	display: 'flex',
																	justifyContent: 'space-between',
																	alignItems: 'center',
																	marginBottom: '4px',
																} }
															>
																<strong style={ { fontSize: '11px', color: '#757575' } }>
																	{ isBtnItem( bt )
																		? __( 'Button', 'gene-section-builder' )
																		: __( 'Heading', 'gene-section-builder' ) }{ ' ' }
																	{ bi + 1 }
																</strong>
																<span>
																	<Button
																		size="small"
																		icon="arrow-up-alt2"
																		label={ __( 'Move up', 'gene-section-builder' ) }
																		disabled={ bi === 0 }
																		onClick={ () => moveSceneBeat( i, bi, -1 ) }
																	/>
																	<Button
																		size="small"
																		icon="arrow-down-alt2"
																		label={ __( 'Move down', 'gene-section-builder' ) }
																		disabled={ bi === arr.length - 1 }
																		onClick={ () => moveSceneBeat( i, bi, 1 ) }
																	/>
																	<Button
																		size="small"
																		icon="trash"
																		isDestructive
																		label={ __( 'Remove', 'gene-section-builder' ) }
																		onClick={ () => removeSceneBeat( i, bi ) }
																	/>
																</span>
															</div>
														) : null }
														{ ! isBtnItem( bt ) ? (
															<>
																<TextControl
																	label={ __( 'Label', 'gene-section-builder' ) }
																	value={ bt.eyebrow || '' }
																	onChange={ ( v ) => setSceneBeat( i, bi, 'eyebrow', v ) }
																/>
																<TextareaControl
																	label={ __( 'Heading', 'gene-section-builder' ) }
																	help={ __( 'Line breaks are kept.', 'gene-section-builder' ) }
																	value={ bt.heading || '' }
																	onChange={ ( v ) => setSceneBeat( i, bi, 'heading', v ) }
																/>
																<TextControl
																	label={ __( 'Sub-line', 'gene-section-builder' ) }
																	value={ bt.sub || '' }
																	onChange={ ( v ) => setSceneBeat( i, bi, 'sub', v ) }
																/>
															</>
														) : null }
														{ isBtnItem( bt ) || ! timeline ? (
															<BeatButtons buttons={ bt.buttons } onChange={ ( v ) => setSceneBeat( i, bi, 'buttons', v ) } />
														) : null }
														{ timeline ? (
															<BeatPlacement
																b={ bt }
																i={ bi }
																count={ arr.length }
																set={ ( k, v ) => setSceneBeat( i, bi, k, v ) }
																setPhone={ ( x, y ) => setSceneBeatPhone( i, bi, x, y ) }
																clearPhone={ () => clearSceneBeatPhone( i, bi ) }
															/>
														) : null }
													</div>
												) ) }
												<Button variant="tertiary" onClick={ () => addSceneBeat( i ) }>
													{ __( 'Add heading to this chapter', 'gene-section-builder' ) }
												</Button>
												{ timeline ? (
													<Button
														variant="tertiary"
														onClick={ () =>
															setSceneBeats( i, [
																...beatsOf( sceneList[ i ] || {} ),
																{ buttons: [ { label: '', url: '', style: 'solid' } ] },
															] )
														}
													>
														{ __( 'Add button to this chapter', 'gene-section-builder' ) }
													</Button>
												) : null }
												{ scVideo ? (
													<div style={ { marginTop: '10px', borderTop: '1px dashed #e0e0e0', paddingTop: '10px' } }>
														<p style={ { fontSize: '11px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.04em', color: '#757575', margin: '0 0 6px' } }>
															{ __( 'Mobile clip (optional)', 'gene-section-builder' ) }
														</p>
														<MediaUploadCheck>
															<MediaUpload
																allowedTypes={ [ 'video' ] }
																value={ sc.backgroundMobile && sc.backgroundMobile.id }
																onSelect={ ( m ) => onSceneMobile( i, m ) }
																render={ ( { open } ) => (
																	<Button variant="secondary" onClick={ open }>
																		{ sc.backgroundMobile && sc.backgroundMobile.url
																			? __( 'Replace mobile clip', 'gene-section-builder' )
																			: __( 'Set a mobile clip', 'gene-section-builder' ) }
																	</Button>
																) }
															/>
														</MediaUploadCheck>
														{ sc.backgroundMobile && sc.backgroundMobile.url ? (
															<Button
																variant="link"
																isDestructive
																onClick={ () => setSceneField( i, 'backgroundMobile', {} ) }
																style={ { display: 'block', marginTop: '6px' } }
															>
																{ __( 'Remove mobile clip', 'gene-section-builder' ) }
															</Button>
														) : null }
														<p style={ { fontSize: '12px', color: '#757575', margin: '6px 0 0' } }>
															{ __( 'Shown on phones (≤781px) for this chapter. Export it all-keyframes too.', 'gene-section-builder' ) }
														</p>
														<WeightGauge
															mainBytes={ bytesOf( bg ) }
															mobBytes={ sc.backgroundMobile && sc.backgroundMobile.type === 'video' ? bytesOf( sc.backgroundMobile ) : 0 }
															isPro={ isPro }
														/>
													</div>
												) : null }
											</div>
										);
									} ) }
									<Button variant="primary" onClick={ addScene }>
										{ __( 'Add chapter', 'gene-section-builder' ) }
									</Button>
									<div style={ { marginTop: '14px', borderTop: '1px solid #e0e0e0', paddingTop: '12px' } }>
										<SelectControl
											label={ __( 'Chapter transition', 'gene-section-builder' ) }
											help={ __(
												'How one chapter breaks apart to reveal the next as you scroll.',
												'gene-section-builder'
											) }
											value={ shatterStyle === 'split' ? 'split' : 'glass' }
											options={ [
												{ label: __( 'Glass shards', 'gene-section-builder' ), value: 'glass' },
												{ label: __( 'Split apart', 'gene-section-builder' ), value: 'split' },
											] }
											onChange={ ( v ) => setAttributes( { shatterStyle: v } ) }
										/>
										<RangeControl
											label={ __( 'Scroll length (screens)', 'gene-section-builder' ) }
											help={ __(
												'How many viewport-heights of scrolling play through the hero.',
												'gene-section-builder'
											) }
											value={ scrubScreens }
											min={ 2 }
											max={ 8 }
											onChange={ ( v ) => setAttributes( { scrubScreens: v } ) }
										/>
										<ToggleControl
											label={ __( 'Free placement (timeline)', 'gene-section-builder' ) }
											help={ __(
												'Place each heading anywhere over the hero and set when it appears and leaves as visitors scroll — like slides on a timeline.',
												'gene-section-builder'
											) }
											checked={ !! timeline }
											onChange={ ( v ) => setTimelineMode( v ) }
										/>
										{ timeline ? (
											<SelectControl
												label={ __( 'Shared effect', 'gene-section-builder' ) }
												help={ __( 'Used by every heading unless it sets its own.', 'gene-section-builder' ) }
												value={ beatAnim }
												options={ [
													{ label: __( 'Fade', 'gene-section-builder' ), value: 'fade' },
													{ label: __( 'Rise', 'gene-section-builder' ), value: 'rise' },
													{ label: __( 'Slide', 'gene-section-builder' ), value: 'slide' },
													{ label: __( 'Reveal', 'gene-section-builder' ), value: 'reveal' },
												] }
												onChange={ ( v ) => setAttributes( { beatAnim: v } ) }
											/>
										) : null }
									</div>
									<p style={ { fontSize: '12px', color: '#757575', margin: '10px 0 0' } }>
										{ __(
											'Scroll length applies per chapter. Keep each clip short and all-keyframes — only the current and next chapter ever load.',
											'gene-section-builder'
										) }
									</p>
								</>
							) : null }
						</>
					) : (
						<Notice status="info" isDismissible={ false }>
							{ __(
								'Play a sequence of background videos — each scrubs with the scroll, then hands off to the next — with Scroll Hero Pro.',
								'gene-section-builder'
							) }
							<div style={ { marginTop: '8px' } }>
								<Button variant="secondary" href={ upgradeUrl } target="_blank" rel="noopener">
									{ __( 'Get Pro', 'gene-section-builder' ) }
								</Button>
							</div>
						</Notice>
					) }
				</PanelBody>

				<PanelColorSettings
				title={ __( 'Design', 'gene-section-builder' ) }
				initialOpen={ false }
				colorSettings={ [
					{
						value: headingColor || undefined,
						onChange: ( v ) => setAttributes( { headingColor: v || '' } ),
						label: __( 'Heading color', 'gene-section-builder' ),
					},
					{
						value: buttonColor || undefined,
						onChange: ( v ) => setAttributes( { buttonColor: v || '' } ),
						label: __( 'Button color', 'gene-section-builder' ),
					},
					{
						value: buttonTextColor || undefined,
						onChange: ( v ) => setAttributes( { buttonTextColor: v || '' } ),
						label: __( 'Button text color', 'gene-section-builder' ),
					},
				] }
			>
				<p style={ { fontSize: '12px', color: '#757575', margin: '0 0 12px' } }>
					{ __( 'The button color fills solid buttons and outlines ghost ones; the text color is for solid buttons.', 'gene-section-builder' ) }
				</p>
				{ isPro ? (
					<>
						<SelectControl
							label={ __( 'Font', 'gene-section-builder' ) }
							help={ __( 'System fonts only — nothing loads from outside, so the page stays fast.', 'gene-section-builder' ) }
							value={ fontPreset === 'sans' || fontPreset === 'serif' || fontPreset === 'mono' ? fontPreset : '' }
							options={ [
								{ label: __( 'Theme default', 'gene-section-builder' ), value: '' },
								{ label: __( 'Clean sans-serif', 'gene-section-builder' ), value: 'sans' },
								{ label: __( 'Elegant serif', 'gene-section-builder' ), value: 'serif' },
								{ label: __( 'Monospace', 'gene-section-builder' ), value: 'mono' },
							] }
							onChange={ ( v ) => setAttributes( { fontPreset: v } ) }
						/>
						<SelectControl
							label={ __( 'Button shape', 'gene-section-builder' ) }
							value={ buttonShape === 'rounded' || buttonShape === 'square' ? buttonShape : 'pill' }
							options={ [
								{ label: __( 'Pill', 'gene-section-builder' ), value: 'pill' },
								{ label: __( 'Rounded', 'gene-section-builder' ), value: 'rounded' },
								{ label: __( 'Square', 'gene-section-builder' ), value: 'square' },
							] }
							onChange={ ( v ) => setAttributes( { buttonShape: v } ) }
						/>
					</>
				) : (
					<p style={ { fontSize: '12px', color: '#757575', margin: 0 } }>
						{ __( 'Font choices and button shapes come with Scroll Hero Pro.', 'gene-section-builder' ) }{ ' ' }
						<a href={ upgradeUrl } target="_blank" rel="noopener noreferrer">
							{ __( 'Get Pro', 'gene-section-builder' ) }
						</a>
					</p>
				) }
			</PanelColorSettings>
			</InspectorControls>

			<div { ...blockProps }>
				{ isMultiActive ? multiCanvas : (
				<>
				{ isTimeline ? tlBar : null }
				<div className="gene-hero__pin" ref={ stageRef }>
					{ hasBg ? (
						<div className="gene-hero__bg">
							<Media m={ background } />
							<div className="gene-hero__scrim" style={ { '--dim': ( bgDim || 0 ) / 100 } } />
						</div>
					) : null }
					<div className="gene-hero__beats">
						{ list.some( ( b ) => beatText( b ) || ( isTimeline && isBtnItem( b ) ) ) ? (
							( isPro ? list : list.slice( 0, 1 ) ).map( ( b, i ) => {
								const p = isTimeline ? effPos( i ) : null;
								const isSel = isTimeline && i === sel;
								const style = isTimeline
									? {
											left: p.x + '%',
											top: p.y + '%',
											transform: 'translate(-50%, -50%)',
											cursor: 'grab',
											touchAction: 'none',
									  }
									: undefined;
								const handlers = isTimeline
									? {
											tabIndex: 0,
											onPointerDown: ( e ) => onBeatDown( e, i ),
											onPointerMove: ( e ) => onBeatMove( e, i ),
											onPointerUp: ( e ) => onBeatUp( e, i ),
											onKeyDown: ( e ) => onBeatKey( e, i ),
									  }
									: {};
								return (
									<div
										className={
											'gene-hero__beat' +
											( isTimeline && isBtnItem( b ) ? ' gene-hero__beat--btn' : '' ) +
											( isSel ? ' is-selected' : '' )
										}
										key={ i }
										style={ style }
										{ ...handlers }
									>
										{ ( b.eyebrow || '' ).trim() ? (
											<p className="gene-hero__eyebrow">{ b.eyebrow }</p>
										) : null }
										{ ( b.heading || '' ).trim() ? (
											<h2 className="gene-hero__head">{ lines( b.heading ) }</h2>
										) : null }
										{ ( b.sub || '' ).trim() ? (
											<p className="gene-hero__sub">{ b.sub }</p>
										) : null }
										{ previewButtons( b ) || ( isTimeline && isBtnItem( b ) ? btnPlaceholder() : null ) }
									</div>
								);
							} )
						) : (
							<p className="gene-hero__hint">
								{ __( 'Set a background and write a heading in the sidebar.', 'gene-section-builder' ) }
							</p>
						) }
					</div>
					{ isTimeline ? tlGuides : null }
				</div>
				{ isTimeline ? tlTrack : null }
				</>
				) }
			</div>
		</>
	);
}
