( function( wp ) {
	'use strict';

	var el = wp.element.createElement;
	var useState = wp.element.useState;
	var useEffect = wp.element.useEffect;
	var useRef = wp.element.useRef;
	var Fragment = wp.element.Fragment;
	var TextControl = wp.components.TextControl;
	var dispatch = wp.data.dispatch;
	var select = wp.data.select;
	var createBlock = wp.blocks.createBlock;
	var parse = wp.blocks.parse;
	var domReady = wp.domReady;
	var richText = wp.richText;
	var registerFormatType = richText && richText.registerFormatType;
	var apiFetch = wp.apiFetch || null;
	var paletteConfig = window.aichiPaletteConfig || {};
	var CLOSET_URL = 'https://ootakeoo.com/';
	var COORDINATE_FORMAT = 'ai-chiichan-palette-coordinate';
	var COORDINATE_SCHEMA_VERSION = 3;
	var COORDINATE_ASSET_SCHEMA_VERSION = 2;
	var COORDINATE_LEGACY_SCHEMA_VERSION = 1;
	var DEFAULT_COORDINATE_MAX_BYTES = 1572864;
	var DEFAULT_COORDINATE_ASSET_MAX_BYTES = 49152;
	var DEFAULT_COORDINATE_BACKGROUND_MAX_BYTES = 524288;

	if ( apiFetch && apiFetch.use && apiFetch.createNonceMiddleware && paletteConfig.nonce ) {
		apiFetch.use( apiFetch.createNonceMiddleware( paletteConfig.nonce ) );
	}

	var MARKER_FORMATS = {
		yellow: { type: 'ai-chiichan-palette/marker-yellow', className: 'aichi-marker-yellow', label: 'イエロー' },
		pink: { type: 'ai-chiichan-palette/marker-pink', className: 'aichi-marker-pink', label: 'ピンク' },
		blue: { type: 'ai-chiichan-palette/marker-blue', className: 'aichi-marker-blue', label: 'ブルー' }
	};

	/*
	 * Level11 ちょい足し登録所
	 * ------------------------
	 * 表示名・見本・実行処理を1定義にまとめています。今後、下線や波線などを
	 * 追加するときは、この配列（または公開register関数）へ定義を足すだけで、
	 * 2列カードのUIへ自動的に反映できます。
	 */
	var PAINT_DEFINITIONS = [
		{
			id: 'marker-yellow',
			label: 'イエロー',
			description: '黄色マーカー',
			previewClass: 'is-yellow',
			historyLabel: 'イエローマーカー',
			apply: function() { return applyMarker( 'yellow' ); }
		},
		{
			id: 'marker-pink',
			label: 'ピンク',
			description: 'ピンクマーカー',
			previewClass: 'is-pink',
			historyLabel: 'ピンクマーカー',
			apply: function() { return applyMarker( 'pink' ); }
		},
		{
			id: 'marker-blue',
			label: 'ブルー',
			description: 'ブルーマーカー',
			previewClass: 'is-blue',
			historyLabel: 'ブルーマーカー',
			apply: function() { return applyMarker( 'blue' ); }
		},
		{
			id: 'marker-remove',
			label: '解除',
			description: 'マーカーを外す',
			previewClass: 'is-remove',
			historyLabel: 'マーカー解除',
			apply: removeMarker
		}
	];

	if ( registerFormatType ) {
		Object.keys( MARKER_FORMATS ).forEach( function( key ) {
			var format = MARKER_FORMATS[ key ];
			try {
				registerFormatType( format.type, {
					title: 'AIちいちゃん ' + format.label + 'マーカー',
					tagName: 'span',
					className: format.className,
					edit: function() { return null; }
				} );
			} catch ( error ) {}
		} );
	}

	var ITEMS = [
		{
			id: 'heading-blue',
			bulkEngine: 'heading-all',
			category: '見出し',
			icon: '🟦',
			label: 'ブルー見出し',
			description: '塗りつぶしブルー',
			favoriteDefault: true,
			blocks: function() {
				return [
					createBlock( 'core/heading', {
						content: 'ここに見出しを入力します',
						level: 2,
						className: 'aichi-ai-type-heading aichi-palette-heading aichi-palette-heading-blue'
					} )
				];
			}
		},
		{
			id: 'heading-first',
			bulkEngine: 'heading-all',
			category: '見出し',
			icon: '✨',
			label: '1文字目ジャンボ',
			description: '最初の1文字を強調',
			favoriteDefault: true,
			blocks: function() {
				return [
					createBlock( 'core/heading', {
						content: '特典について',
						level: 2,
						className: 'aichi-ai-type-heading aichi-palette-heading aichi-palette-heading-first'
					} )
				];
			}
		},
		{
			id: 'heading-premium',
			bulkEngine: 'heading-all',
			category: '見出し',
			icon: '👑',
			label: '高級ブラック',
			description: '黒×ゴールド',
			favoriteDefault: false,
			blocks: function() {
				return [
					createBlock( 'core/heading', {
						content: 'ここに見出しを入力します',
						level: 2,
						className: 'aichi-ai-type-heading aichi-palette-heading aichi-palette-heading-premium'
					} )
				];
			}
		},
		{
			id: 'box-point',
			category: 'ボックス',
			icon: '💡',
			label: 'ポイント',
			description: 'よく使う青い囲み枠',
			favoriteDefault: true,
			blocks: function() {
				return [
					createBlock( 'core/group', {
						className: 'aichi-ai-type-box aichi-ai-box-kind-point aichi-palette-box aichi-palette-box-point',
						layout: { type: 'constrained' }
					}, [
						createBlock( 'core/paragraph', {
							content: '<strong>ここにポイントを入力します。</strong>'
						} )
					] )
				];
			}
		},
		{
			id: 'box-special',
			category: 'ボックス',
			icon: '🎁',
			label: '購入者限定特典',
			description: '特典紹介用',
			favoriteDefault: true,
			blocks: function() {
				return [
					createBlock( 'core/group', {
						className: 'aichi-ai-type-box aichi-ai-box-kind-special aichi-palette-box aichi-palette-box-special',
						layout: { type: 'constrained' }
					}, [
						createBlock( 'core/heading', {
							content: '購入者限定特典',
							level: 3
						} ),
						createBlock( 'core/paragraph', {
							content: 'ここに特典内容を入力します。'
						} )
					] )
				];
			}
		},
		{
			id: 'qa-blue',
			bulkEngine: 'qa',
			category: 'Q&A',
			icon: '💬',
			label: 'Q&A・ブルー',
			description: '質問と回答をセットで追加',
			favoriteDefault: true,
			blocks: function() {
				return [
					createBlock( 'core/group', {
						className: 'aichi-ai-type-qa aichi-palette-qa aichi-palette-qa-blue',
						layout: { type: 'constrained' }
					}, [
						createBlock( 'core/paragraph', {
							content: '<strong>ここに質問を入力します。</strong>',
							className: 'aichi-ai-type-qa-question aichi-palette-question'
						} ),
						createBlock( 'core/paragraph', {
							content: 'ここに回答を入力します。',
							className: 'aichi-ai-type-qa-answer aichi-palette-answer'
						} )
					] )
				];
			}
		},
		{
			id: 'qa-pink',
			bulkEngine: 'qa',
			category: 'Q&A',
			icon: '🎀',
			label: 'Q&A・ピンク',
			description: 'やさしいピンク',
			favoriteDefault: false,
			blocks: function() {
				return [
					createBlock( 'core/group', {
						className: 'aichi-ai-type-qa aichi-palette-qa aichi-palette-qa-pink',
						layout: { type: 'constrained' }
					}, [
						createBlock( 'core/paragraph', {
							content: '<strong>ここに質問を入力します。</strong>',
							className: 'aichi-ai-type-qa-question aichi-palette-question'
						} ),
						createBlock( 'core/paragraph', {
							content: 'ここに回答を入力します。',
							className: 'aichi-ai-type-qa-answer aichi-palette-answer'
						} )
					] )
				];
			}
		},
		{
			id: 'list-check',
			bulkEngine: 'list',
			category: '箇条書き',
			icon: '✅',
			label: '丸チェック',
			description: '読みやすいチェックリスト',
			favoriteDefault: true,
			blocks: function() {
				return [
					createBlock( 'core/list', {
						className: 'aichi-ai-type-list aichi-palette-list aichi-palette-list-check',
						values: '<ul><li>1つ目の項目</li><li>2つ目の項目</li><li>3つ目の項目</li></ul>'
					} )
				];
			}
		},
		{
			id: 'list-heart',
			bulkEngine: 'list',
			category: '箇条書き',
			icon: '♥',
			label: 'ハート',
			description: 'かわいい箇条書き',
			favoriteDefault: false,
			blocks: function() {
				return [
					createBlock( 'core/list', {
						className: 'aichi-ai-type-list aichi-palette-list aichi-palette-list-heart',
						values: '<ul><li>1つ目の項目</li><li>2つ目の項目</li><li>3つ目の項目</li></ul>'
					} )
				];
			}
		},
		{
			id: 'button-blue',
			bulkEngine: 'cta',
			category: 'ボタン',
			icon: '🔘',
			label: 'ブルーボタン',
			description: '丸くて目立つボタン',
			favoriteDefault: true,
			blocks: function() {
				return [
					createBlock( 'core/buttons', {}, [
						createBlock( 'core/button', {
							text: '詳しくはこちら',
							url: '',
							className: 'aichi-ai-type-cta aichi-palette-button aichi-palette-button-blue'
						} )
					] )
				];
			}
		},
		{
			id: 'button-gold',
			bulkEngine: 'cta',
			category: 'ボタン',
			icon: '💎',
			label: 'ゴールドボタン',
			description: '販売LP向け',
			favoriteDefault: false,
			blocks: function() {
				return [
					createBlock( 'core/buttons', {}, [
						createBlock( 'core/button', {
							text: '詳しくはこちら',
							url: '',
							className: 'aichi-ai-type-cta aichi-palette-button aichi-palette-button-gold'
						} )
					] )
				];
			}
		},
		{
			id: 'article-set',
			category: '記事セット',
			icon: '📄',
			label: '記事冒頭セット',
			description: 'タイトル・リード・ポイント',
			favoriteDefault: false,
			blocks: function() {
				return [
					createBlock( 'core/heading', {
						content: 'ここに記事内タイトルを入力します',
						level: 2,
					className: 'aichi-ai-type-heading aichi-palette-heading aichi-palette-heading-blue'
					} ),
					createBlock( 'core/paragraph', {
						content: 'ここにリード文を入力します。読者の悩みと、この記事で分かることを伝えます。'
					} ),
					createBlock( 'core/group', {
						className: 'aichi-ai-type-box aichi-ai-box-kind-point aichi-palette-box aichi-palette-box-point',
						layout: { type: 'constrained' }
					}, [
						createBlock( 'core/paragraph', {
							content: '<strong>この記事のポイント</strong>'
						} ),
						createBlock( 'core/list', {
							className: 'aichi-ai-type-list aichi-palette-list aichi-palette-list-check',
							values: '<ul><li>この記事で分かること</li><li>読者ができるようになること</li></ul>'
						} )
					] )
				];
			}
		}
	];

	/*
	 * Level5 セット定義レジストリ
	 * --------------------------
	 * 現在の個別スタンプを共通の意味タイプへ結び付けます。
	 * 将来は styles と articleSurface を持つセットを追加するだけで、
	 * 記事丸ごとの着せ替えへ拡張できます。
	 */
	var SET_DEFINITIONS = {
		classic: {
			id: 'classic',
			label: '標準セット',
			showInArticleSets: false,
			styles: {
				heading: [ 'heading-blue', 'heading-first', 'heading-premium' ],
				list: [ 'list-check', 'list-heart' ],
				qa: [ 'qa-blue', 'qa-pink' ],
				box: [ 'box-point', 'box-special' ],
				cta: [ 'button-blue', 'button-gold' ]
			},
			articleSurface: null,
			hasPageBackground: false,
			fontBody: null,
			fontHeading: null,
			requiresLicense: false
		},
		blue: {
			id: 'blue',
			label: 'ブルーセット',
			description: '爽やかなブルーで記事全体をコーデ',
			icon: '💙',
			styles: {
				heading: 'aichi-palette-heading aichi-palette-heading-blue',
				list: 'aichi-palette-list aichi-palette-list-check',
				qa: 'aichi-palette-qa aichi-palette-qa-blue',
				box: 'aichi-palette-box aichi-palette-box-point',
				cta: 'aichi-palette-button aichi-palette-button-blue'
			},
			articleSurface: null,
			fontBody: null,
			fontHeading: null,
			requiresLicense: false
		},
		cute: {
			id: 'cute',
			label: 'かわいいセット',
			description: 'やさしく、かわいらしい雰囲気で記事全体をコーデ',
			icon: '🌸',
			styles: {
				heading: 'aichi-palette-heading aichi-palette-heading-cute',
				list: 'aichi-palette-list aichi-palette-list-cute',
				qa: 'aichi-palette-qa aichi-palette-qa-cute',
				box: 'aichi-palette-box aichi-palette-box-cute',
				cta: 'aichi-palette-button aichi-palette-button-cute'
			},
			articleSurface: null,
			fontBody: null,
			fontHeading: null,
			requiresLicense: false
		}
	};

	/*
	 * Ver.1.2 追加コーデ登録所
	 * -------------------------
	 * PHP側で検証済みのコーデだけを、既存セットと同じレジストリへ登録します。
	 * 任意CSSや実行コードは受け取らず、安全なIDから固定クラス名を作ります。
	 */
	function coordinateToSetDefinition( coordinate ) {
		if ( ! coordinate || typeof coordinate.id !== 'string' ||
			! /^chiichan-[a-z0-9][a-z0-9-]{1,54}$/.test( coordinate.id ) ) {
			return null;
		}

		var suffix = 'custom-' + coordinate.id;
		var h2Only = [ 'nordic-christmas-h2-v1', 'nordic-christmas-h2-v2', 'nordic-christmas-h2-v3', 'christmas-card-h2-v2', 'christmas-card-h2-v2-finish', 'christmas-card-h2-v2-refinish', 'christmas-card-h2-v2-final' ].indexOf( coordinate.headingDesign ) >= 0;
		var h2AndH3 = coordinate.headingDesign === 'christmas-card-h2-h3-v1';
		var headingOnly = h2Only || h2AndH3;
		return {
			id: coordinate.id,
			label: String( coordinate.name || '追加コーデ' ),
			description: String( coordinate.description || '追加したコーデ' ),
			icon: String( coordinate.icon || '👗' ),
			version: String( coordinate.version || '1.0.0' ),
			isExternal: true,
			styles: {
				heading: 'aichi-palette-heading aichi-palette-heading-' + suffix,
				list: headingOnly ? null : 'aichi-palette-list aichi-palette-list-' + suffix,
				qa: headingOnly ? null : 'aichi-palette-qa aichi-palette-qa-' + suffix,
				box: headingOnly ? null : 'aichi-palette-box aichi-palette-box-' + suffix,
				cta: headingOnly ? null : 'aichi-palette-button aichi-palette-button-' + suffix
			},
			headingLevel: h2Only ? 2 : null,
			headingLevels: h2AndH3 ? [ 2, 3 ] : null,
			articleSurface: null,
			hasPageBackground: coordinate.hasPageBackground === true,
			fontBody: coordinate.fontBody || null,
			fontHeading: coordinate.fontHeading || null,
			requiresLicense: false
		};
	}

	function registerCoordinateDefinition( coordinate ) {
		var definition = coordinateToSetDefinition( coordinate );
		if ( ! definition || SET_DEFINITIONS[ definition.id ] ) return false;
		SET_DEFINITIONS[ definition.id ] = definition;
		return definition;
	}

	( Array.isArray( paletteConfig.coordinates ) ? paletteConfig.coordinates : [] ).forEach(
		registerCoordinateDefinition
	);

	/*
	 * Level9 書体定義
	 * ----------------
	 * 本文と見出しを別々のクラスとして保持し、将来は記事セットの
	 * fontBody / fontHeading から同じエンジンを利用できる構造にします。
	 */
	var FONT_DEFINITIONS = {
		gothic: { id: 'gothic', label: 'ゴシック', bodyClass: 'aichi-font-body-gothic', headingClass: 'aichi-font-heading-gothic' },
		rounded: { id: 'rounded', label: '丸ゴシック', bodyClass: 'aichi-font-body-rounded', headingClass: 'aichi-font-heading-rounded' },
		mincho: { id: 'mincho', label: '明朝', bodyClass: 'aichi-font-body-mincho', headingClass: 'aichi-font-heading-mincho' },
		original: { id: 'original', label: '元の書体', bodyClass: '', headingClass: '' }
	};

	function mergeFontClass( currentClassName, nextClassName ) {
		var kept = classTokens( currentClassName ).filter( function( token ) {
			return token.indexOf( 'aichi-font-body-' ) !== 0 &&
				token.indexOf( 'aichi-font-heading-' ) !== 0;
		} );
		classTokens( nextClassName ).forEach( function( token ) {
			if ( kept.indexOf( token ) < 0 ) kept.push( token );
		} );
		return kept.join( ' ' );
	}

	function applyFontConfiguration( bodyFontId, headingFontId, showResultNotice ) {
		var bodyDefinition = bodyFontId === null || typeof bodyFontId === 'undefined'
			? null
			: FONT_DEFINITIONS[ bodyFontId ];
		var headingDefinition = headingFontId === null || typeof headingFontId === 'undefined'
			? null
			: FONT_DEFINITIONS[ headingFontId ];
		if ( ! bodyDefinition && ! headingDefinition ) return 0;
		var changed = 0;

		function applyToBlocks( blocks ) {
			( blocks || [] ).forEach( function( block ) {
				var isHeading = block.name === 'core/heading';
				var definition = isHeading ? headingDefinition : bodyDefinition;
				if ( ! definition ) {
					if ( block.innerBlocks && block.innerBlocks.length ) applyToBlocks( block.innerBlocks );
					return;
				}
				var nextFontClass = isHeading ? definition.headingClass : definition.bodyClass;
				var currentClassName = block.attributes && block.attributes.className;
				var nextClassName = mergeFontClass( currentClassName, nextFontClass );
				if ( String( currentClassName || '' ) !== nextClassName ) {
					dispatch( 'core/block-editor' ).updateBlockAttributes( block.clientId, { className: nextClassName } );
					changed++;
				}
				if ( block.innerBlocks && block.innerBlocks.length ) applyToBlocks( block.innerBlocks );
			} );
		}

		applyToBlocks( select( 'core/block-editor' ).getBlocks() );
		if ( showResultNotice !== false ) {
			var label = bodyFontId === headingFontId && bodyDefinition
				? bodyDefinition.label
				: '指定の書体';
			showNotice( label + 'へ着せ替えました😊' );
		}
		return changed;
	}

	function applyFontProfile( fontId ) {
		return applyFontConfiguration( fontId, fontId, true );
	}

	function isFontProfileApplied( fontId ) {
		var definition = FONT_DEFINITIONS[ fontId ];
		if ( ! definition || fontId === 'original' ) return false;
		var found = false;
		function inspect( blocks ) {
			( blocks || [] ).forEach( function( block ) {
				var token = block.name === 'core/heading' ? definition.headingClass : definition.bodyClass;
				if ( token && hasClassToken( block.attributes && block.attributes.className, token ) ) found = true;
				if ( block.innerBlocks && block.innerBlocks.length ) inspect( block.innerBlocks );
			} );
		}
		inspect( select( 'core/block-editor' ).getBlocks() );
		return found;
	}

	var TYPE_CLASS_RULES = {
		heading: {
			semantic: 'aichi-ai-type-heading',
			palettePrefix: 'aichi-palette-heading'
		},
		list: {
			semantic: 'aichi-ai-type-list',
			palettePrefix: 'aichi-palette-list'
		},
		qa: {
			semantic: 'aichi-ai-type-qa',
			palettePrefix: 'aichi-palette-qa'
		},
		box: {
			semantic: 'aichi-ai-type-box',
			palettePrefix: 'aichi-palette-box'
		},
		cta: {
			semantic: 'aichi-ai-type-cta',
			palettePrefix: 'aichi-palette-button'
		}
	};

	function classTokens( className ) {
		return String( className || '' ).split( /\s+/ ).filter( Boolean );
	}

	function hasClassToken( className, token ) {
		return classTokens( className ).indexOf( token ) >= 0;
	}

	function mergeDecorationClass( currentClassName, nextClassName, semanticType ) {
		var rule = TYPE_CLASS_RULES[ semanticType ];
		var kept = classTokens( currentClassName ).filter( function( token ) {
			return ( ! rule || token.indexOf( rule.palettePrefix ) !== 0 ) &&
				token.indexOf( 'aichi-ai-set-' ) !== 0;
		} );
		var merged = kept.concat( classTokens( nextClassName ) );
		var unique = [];

		merged.forEach( function( token ) {
			if ( unique.indexOf( token ) < 0 ) unique.push( token );
		} );

		if ( rule && unique.indexOf( rule.semantic ) < 0 ) {
			unique.unshift( rule.semantic );
		}

		return unique.join( ' ' );
	}

	function mergeSetDecorationClass( currentClassName, nextClassName, semanticType, setId ) {
		var merged = mergeDecorationClass( currentClassName, nextClassName, semanticType );
		return mergeDecorationClass(
			merged,
			'aichi-ai-set-' + setId,
			null
		);
	}

	function addPageSetClass( currentClassName, setId ) {
		var tokens = classTokens( currentClassName ).filter( function( token ) {
			return token.indexOf( 'aichi-ai-page-set-' ) !== 0;
		} );
		tokens.push( 'aichi-ai-page-set-' + setId );
		return tokens.join( ' ' );
	}

	function updateArticlePageSetMarker( setId, enabled ) {
		var markerAdded = false;
		var changed = 0;
		function update( blocks ) {
			( blocks || [] ).forEach( function( block ) {
				var currentClassName = String( block.attributes && block.attributes.className || '' );
				var nextClassName = classTokens( currentClassName ).filter( function( token ) {
					return token.indexOf( 'aichi-ai-page-set-' ) !== 0;
				} ).join( ' ' );
				if ( enabled && ! markerAdded ) {
					nextClassName = addPageSetClass( nextClassName, setId );
					markerAdded = true;
				}
				if ( currentClassName !== nextClassName ) {
					dispatch( 'core/block-editor' ).updateBlockAttributes( block.clientId, { className: nextClassName } );
					changed++;
				}
				if ( block.innerBlocks && block.innerBlocks.length ) update( block.innerBlocks );
			} );
		}
		update( select( 'core/block-editor' ).getBlocks() );
		return changed;
	}

	function collectRecognizedBlocks() {
		var recognized = [];

		function collect( blocks ) {
			( blocks || [] ).forEach( function( block ) {
				var type = identifySemanticType( block );
				if ( type ) recognized.push( { block: block, type: type } );
				if ( block.innerBlocks && block.innerBlocks.length ) collect( block.innerBlocks );
			} );
		}

		collect( select( 'core/block-editor' ).getBlocks() );
		return recognized;
	}

	/*
	 * ボックスの役割は着せ替えても消さない。
	 * 旧記事は従来classまたは内部の「購入者限定特典」見出しから安全に移行する。
	 */
	function identifyBoxKind( block ) {
		if ( ! block ) return 'point';
		var className = block.attributes && block.attributes.className;
		if ( hasClassToken( className, 'aichi-ai-box-kind-special' ) ||
			hasClassToken( className, 'aichi-palette-box-special' ) ) return 'special';
		if ( hasClassToken( className, 'aichi-ai-box-kind-point' ) ||
			hasClassToken( className, 'aichi-palette-box-point' ) ) return 'point';

		var text = '';
		function collectText( current ) {
			if ( ! current ) return;
			var attributes = current.attributes || {};
			[ 'content', 'text', 'values' ].forEach( function( key ) {
				if ( typeof attributes[ key ] === 'string' ) text += ' ' + attributes[ key ];
			} );
			( current.innerBlocks || [] ).forEach( collectText );
		}
		collectText( block );
		return /購入者限定|限定特典/.test( text ) ? 'special' : 'point';
	}

	function applyArticleSet( setId ) {
		var definition = SET_DEFINITIONS[ setId ];
		if ( ! definition || ! definition.styles ) return 0;

		var changed = 0;
		collectRecognizedBlocks().forEach( function( entry ) {
			if ( ! isSetEntryApplicable( definition, entry ) ) return;
			var style = definition.styles[ entry.type ];
			if ( entry.type === 'box' && typeof style === 'string' ) {
				style += ' aichi-ai-box-kind-' + identifyBoxKind( entry.block );
			}

			var nextClassName = mergeSetDecorationClass(
					entry.block.attributes.className,
					style,
					entry.type,
					setId
				);
			dispatch( 'core/block-editor' ).updateBlockAttributes( entry.block.clientId, {
				className: nextClassName
			} );
			if ( entry.type === 'qa' ) markInnerQaTypes( entry.block );
			changed++;
		} );
		var pageSurfaceChanged = updateArticlePageSetMarker( setId, definition.hasPageBackground === true );

		/* nullは現在の書体を維持。指定があるセットだけ本文・見出しを連動させる。 */
		changed += applyFontConfiguration(
			definition.fontBody,
			definition.fontHeading,
			false
		);

		showNotice(
			changed || pageSurfaceChanged
				? ( changed ? changed + '個の装飾を' : '記事背景を' ) + definition.label + 'へ着せ替えました😊'
				: '着せ替えできる装飾がありません😊'
		);
		return changed;
	}

	/* 見出し試作コーデは許可した階層以外を着せ替えない。未指定の既存セットは従来どおり全階層へ適用する。 */
	function isSetEntryApplicable( definition, entry ) {
		if ( ! definition || ! definition.styles || ! entry || ! definition.styles[ entry.type ] ) return false;
		if ( entry.type === 'heading' && Array.isArray( definition.headingLevels ) ) {
			var allowedLevel = entry.block && entry.block.attributes && ( entry.block.attributes.level || 2 );
			return definition.headingLevels.indexOf( allowedLevel ) >= 0;
		}
		if ( entry.type !== 'heading' || ! definition.headingLevel ) return true;
		var level = entry.block && entry.block.attributes && ( entry.block.attributes.level || 2 );
		return level === definition.headingLevel;
	}

	function isArticleSetApplied( setId ) {
		var definition = SET_DEFINITIONS[ setId ];
		if ( ! definition ) return false;
		var recognized = collectRecognizedBlocks().filter( function( entry ) {
			return isSetEntryApplicable( definition, entry );
		} );
		return recognized.length > 0 && recognized.every( function( entry ) {
			return hasClassToken( entry.block.attributes.className, 'aichi-ai-set-' + setId );
		} );
	}

	var editorPageSurfaceTimer = null;
	var lastEditorPageSet = null;

	function currentPageSetToken() {
		var found = '';
		var blockEditorStore = null;
		try {
			blockEditorStore = select && select( 'core/block-editor' );
		} catch ( error ) {
			return found;
		}
		if ( ! blockEditorStore || typeof blockEditorStore.getBlocks !== 'function' ) return found;

		function inspect( blocks ) {
			( blocks || [] ).some( function( block ) {
				var tokens = classTokens( block.attributes && block.attributes.className );
				for ( var index = 0; index < tokens.length; index++ ) {
					if ( /^aichi-ai-page-set-chiichan-[a-z0-9][a-z0-9-]{1,54}$/.test( tokens[ index ] ) ) {
						found = tokens[ index ];
						return true;
					}
				}
				return block.innerBlocks && block.innerBlocks.length ? inspect( block.innerBlocks ) : false;
			} );
			return !! found;
		}
		inspect( blockEditorStore.getBlocks() );
		return found;
	}

	function syncEditorPageSurface() {
		try {
			var token = currentPageSetToken();
			lastEditorPageSet = token;
			var documents = [ document ];
			Array.prototype.forEach.call( document.querySelectorAll( 'iframe' ), function( frame ) {
				try {
					if ( frame.contentDocument ) documents.push( frame.contentDocument );
				} catch ( error ) {}
			} );
			documents.forEach( function( currentDocument ) {
				var targets = Array.prototype.slice.call( currentDocument.querySelectorAll( '.editor-styles-wrapper' ) );
				if ( currentDocument.body && currentDocument.body.classList.contains( 'block-editor-iframe__body' ) ) {
					targets.push( currentDocument.body );
				}
				targets.forEach( function( target ) {
					Array.prototype.slice.call( target.classList ).forEach( function( className ) {
						if ( className.indexOf( 'aichi-ai-page-set-' ) === 0 ) target.classList.remove( className );
					} );
					if ( token ) target.classList.add( token );
				} );
			} );
		} catch ( error ) {
			/* 背景プレビューの失敗をパレット本体へ波及させない。 */
		}
	}

	function scheduleEditorPageSurfaceSync() {
		window.clearTimeout( editorPageSurfaceTimer );
		editorPageSurfaceTimer = window.setTimeout( syncEditorPageSurface, 60 );
	}

	function markInnerQaTypes( block ) {
		if ( ! block || ! block.innerBlocks ) return;

		block.innerBlocks.forEach( function( innerBlock, index ) {
			if ( innerBlock.name !== 'core/paragraph' ) return;
			var semantic = index === 0
				? 'aichi-ai-type-qa-question aichi-palette-question'
				: 'aichi-ai-type-qa-answer aichi-palette-answer';

			dispatch( 'core/block-editor' ).updateBlockAttributes(
				innerBlock.clientId,
				{ className: mergeDecorationClass( innerBlock.attributes.className, semantic, null ) }
			);
		} );
	}

	function identifySemanticType( block ) {
		if ( ! block ) return '';
		var className = block.attributes && block.attributes.className;

		if ( block.name === 'core/heading' || hasClassToken( className, 'aichi-ai-type-heading' ) ) return 'heading';
		/* WordPress標準の未装飾ul/olも、内容を変えずに箇条書きとして認識する。 */
		if ( block.name === 'core/list' ) return 'list';
		if ( block.name === 'core/group' && hasClassToken( className, 'aichi-ai-type-qa' ) ) return 'qa';
		if ( block.name === 'core/group' && hasClassToken( className, 'aichi-ai-type-box' ) ) return 'box';
		if ( isRecognizedButtonBlock( block ) ) return 'cta';
		return '';
	}

	function isRecognizedButtonBlock( block ) {
		if ( ! block ) return false;
		if ( block.name === 'core/button' ) return true;

		/* Cocoonは確実なブロック名・専用クラスだけを許可し、一般リンクは除外する。 */
		var cocoonNames = [
			'cocoon-blocks/button',
			'cocoon-blocks/button-wrap',
			'cocoon-blocks/button-1',
			'cocoon-blocks/button-2'
		];
		if ( cocoonNames.indexOf( block.name ) >= 0 ) return true;

		var className = block.attributes && block.attributes.className;
		var cocoonClasses = [
			'wp-block-cocoon-blocks-button',
			'wp-block-cocoon-blocks-button-wrap',
			'cocoon-block-button',
			'btn-wrap'
		];
		return cocoonClasses.some( function( token ) {
			return hasClassToken( className, token );
		} );
	}

	window.aichiPaletteLevel5Registry = {
		version: '1.3.6',
		sets: SET_DEFINITIONS,
		typeRules: TYPE_CLASS_RULES,
		identifyBlock: identifySemanticType,
		identifyBoxKind: identifyBoxKind,
		parseQa: parseQaLines,
		mergeDecorationClass: mergeDecorationClass,
		applyArticleSet: applyArticleSet,
		registerSet: function( definition ) {
			if ( ! definition || ! definition.id || ! definition.styles ) return false;
			SET_DEFINITIONS[ definition.id ] = definition;
			return true;
		},
		registerCoordinate: registerCoordinateDefinition,
		coordinateFormat: COORDINATE_FORMAT,
		coordinateSchemaVersion: COORDINATE_SCHEMA_VERSION,
		validateCoordinateFile: validateCoordinateFileObject,
		fonts: FONT_DEFINITIONS,
		applyFont: applyFontProfile,
		applyFontConfiguration: applyFontConfiguration,
		paintDecorations: PAINT_DEFINITIONS,
		registerPaintDecoration: function( definition ) {
			if ( ! definition || ! definition.id || ! definition.label || typeof definition.apply !== 'function' ) return false;
			for ( var index = 0; index < PAINT_DEFINITIONS.length; index++ ) {
				if ( PAINT_DEFINITIONS[ index ].id === definition.id ) {
					PAINT_DEFINITIONS[ index ] = definition;
					return true;
				}
			}
			PAINT_DEFINITIONS.push( definition );
			return true;
		}
	};

	/*
	 * Level6 装飾履歴エンジン
	 * -----------------------
	 * WordPress標準のUndo/Redoは使用せず、パレットが変更したclassNameだけを
	 * 最大20操作記録します。本文、リンク、画像、URL、altは履歴へ保存も
	 * 上書きもしないため、装飾を戻してもユーザーコンテンツを巻き戻しません。
	 */
	var DECORATION_HISTORY_LIMIT = 20;
	var decorationHistory = {
		undo: [],
		redo: [],
		listeners: []
	};

	function collectDecorationSnapshot() {
		var snapshot = {};

		function collect( blocks ) {
			( blocks || [] ).forEach( function( block ) {
				if ( block && block.clientId ) {
					var attributes = block.attributes || {};
					var contentHtml = richTextAttributeToHtml( attributes.content );
					snapshot[ block.clientId ] = {
						className: String( attributes.className || '' ),
						/* RichTextDataもHTMLへ正規化し、Undoで本文を空にしない。 */
						content: contentHtml
					};
				}
				if ( block.innerBlocks && block.innerBlocks.length ) {
					collect( block.innerBlocks );
				}
			} );
		}

		collect( select( 'core/block-editor' ).getBlocks() );
		return snapshot;
	}

	function notifyDecorationHistory() {
		decorationHistory.listeners.slice().forEach( function( listener ) {
			listener();
		} );
	}

	function subscribeDecorationHistory( listener ) {
		decorationHistory.listeners.push( listener );
		return function() {
			decorationHistory.listeners = decorationHistory.listeners.filter(
				function( current ) {
					return current !== listener;
				}
			);
		};
	}

	function createDecorationRecord( before, after, label ) {
		var ids = {};
		var changes = [];

		Object.keys( before ).forEach( function( id ) { ids[ id ] = true; } );
		Object.keys( after ).forEach( function( id ) { ids[ id ] = true; } );

		Object.keys( ids ).forEach( function( clientId ) {
			var beforeExists = Object.prototype.hasOwnProperty.call( before, clientId );
			var afterExists = Object.prototype.hasOwnProperty.call( after, clientId );
			var beforeValue = beforeExists ? before[ clientId ] : { className: '', content: null };
			var afterValue = afterExists ? after[ clientId ] : { className: '', content: null };

			/* 削除されたブロックは復元しない。本文を古い状態へ戻さないため。 */
			if ( ! afterExists ) return;
			var attributes = {};
			var beforeAttributes = {};
			if ( beforeValue.className !== afterValue.className ) {
				beforeAttributes.className = beforeValue.className;
				attributes.className = afterValue.className;
			}
			if ( beforeValue.content !== afterValue.content ) {
				beforeAttributes.content = beforeValue.content;
				attributes.content = afterValue.content;
			}
			if ( ! Object.keys( attributes ).length ) return;

			/* 新規ブロックは装飾だけを戻せるよう、変更前を空クラスとする。 */
			changes.push( {
				clientId: clientId,
				beforeAttributes: beforeAttributes,
				afterAttributes: attributes
			} );
		} );

		return changes.length ? { label: label, changes: changes } : null;
	}

	function rememberDecorationChange( before, label ) {
		window.setTimeout( function() {
			var record = createDecorationRecord(
				before,
				collectDecorationSnapshot(),
				label
			);
			if ( ! record ) return;

			decorationHistory.undo.push( record );
			if ( decorationHistory.undo.length > DECORATION_HISTORY_LIMIT ) {
				decorationHistory.undo.shift();
			}
			decorationHistory.redo = [];
			notifyDecorationHistory();
		}, 0 );
	}

	function runWithDecorationHistory( label, action ) {
		var before = collectDecorationSnapshot();
		var result = action();
		rememberDecorationChange( before, label );
		return result;
	}

	function applyDecorationRecord( record, useAfter ) {
		var editorSelect = select( 'core/block-editor' );
		var editorDispatch = dispatch( 'core/block-editor' );

		record.changes.forEach( function( change ) {
			if ( ! editorSelect.getBlock( change.clientId ) ) return;
			editorDispatch.updateBlockAttributes(
				change.clientId,
				useAfter ? change.afterAttributes : change.beforeAttributes
			);
		} );
	}

	function undoDecoration() {
		if ( ! decorationHistory.undo.length ) return false;
		var record = decorationHistory.undo.pop();
		applyDecorationRecord( record, false );
		decorationHistory.redo.push( record );
		notifyDecorationHistory();
		showNotice( '装飾を1つ戻しました😊' );
		return true;
	}

	function redoDecoration() {
		if ( ! decorationHistory.redo.length ) return false;
		var record = decorationHistory.redo.pop();
		applyDecorationRecord( record, true );
		decorationHistory.undo.push( record );
		notifyDecorationHistory();
		showNotice( '装飾をやり直しました😊' );
		return true;
	}

	window.aichiPaletteLevel5Registry.history = {
		limit: DECORATION_HISTORY_LIMIT,
		canUndo: function() { return decorationHistory.undo.length > 0; },
		canRedo: function() { return decorationHistory.redo.length > 0; },
		undo: undoDecoration,
		redo: redoDecoration,
		createRecord: createDecorationRecord,
		runAction: runWithDecorationHistory
	};

	/* Level5向け公開名を保ったまま、Level6向け公開名も追加する。 */
	window.aichiPaletteLevel6Registry = window.aichiPaletteLevel5Registry;
	window.aichiPaletteLevel7Registry = window.aichiPaletteLevel5Registry;

	function getDefaultFavorites() {
		return ITEMS.filter( function( item ) {
			return item.favoriteDefault;
		} ).map( function( item ) {
			return item.id;
		} );
	}

	function readFavorites() {
		try {
			var stored = window.localStorage.getItem( 'aichiPaletteFavorites' );
			return stored ? JSON.parse( stored ) : getDefaultFavorites();
		} catch ( error ) {
			return getDefaultFavorites();
		}
	}

	function saveFavorites( favorites ) {
		try {
			window.localStorage.setItem(
				'aichiPaletteFavorites',
				JSON.stringify( favorites )
			);
		} catch ( error ) {}
	}

	function getSelectedInfo() {
		var editorSelect = select( 'core/block-editor' );
		var clientId = editorSelect.getSelectedBlockClientId();
		if ( ! clientId ) {
			return null;
		}
		return {
			clientId: clientId,
			block: editorSelect.getBlock( clientId ),
			index: editorSelect.getBlockIndex( clientId ),
			rootClientId: editorSelect.getBlockRootClientId( clientId )
		};
	}

	function plainContent( block ) {
		if ( ! block || ! block.attributes ) return '';
		var rawContent = block.attributes.content || block.attributes.values || '';
		var normalizedContent = richTextAttributeToHtml( rawContent );
		var content = normalizedContent === null ? '' : normalizedContent;
		var div = document.createElement( 'div' );
		div.innerHTML = String( content );
		return div.textContent || div.innerText || '';
	}

	function contentLines( block ) {
		if ( ! block || ! block.attributes ) return [];
		var rawContent = block.attributes.content || block.attributes.values || '';
		var normalizedContent = richTextAttributeToHtml( rawContent );
		var content = normalizedContent === null ? '' : String( normalizedContent );
		content = content
			.replace( /<br\s*\/?>/gi, '\n' )
			.replace( /<\/(p|div|li)>/gi, '\n' )
			.replace( /<[^>]+>/g, '' );

		var textarea = document.createElement( 'textarea' );
		textarea.innerHTML = content;

		return textarea.value
			.split( /\r?\n/ )
			.map( function( line ) {
				return line.trim();
			} )
			.filter( Boolean );
	}

	function escapeHtml( text ) {
		var div = document.createElement( 'div' );
		div.textContent = text;
		return div.innerHTML;
	}

	function insertAfterSelection( blocksToInsert ) {
		var info = getSelectedInfo();
		if ( info ) {
			dispatch( 'core/block-editor' ).insertBlocks(
				blocksToInsert,
				info.index + 1,
				info.rootClientId
			);
		} else {
			dispatch( 'core/block-editor' ).insertBlocks( blocksToInsert );
		}
	}

	function showNotice( message ) {
		var notices = dispatch( 'core/notices' );
		if ( notices && notices.createNotice ) {
			notices.createNotice( 'info', message, {
				type: 'snackbar',
				isDismissible: true
			} );
		}
	}

	var savedRichTextSelection = null;

	/*
	 * WordPress 6.8以降を含む新しいブロックエディターでは、RichText属性が
	 * 従来のHTML文字列ではなくRichTextDataとして返る場合がある。
	 * マーカー処理ではどちらも同じHTMLへ正規化し、本文そのものは変更しない。
	 */
	function richTextAttributeToHtml( attributeValue ) {
		if ( typeof attributeValue === 'string' ) return attributeValue;
		if ( ! attributeValue || typeof attributeValue !== 'object' ) return null;

		if ( typeof attributeValue.toHTMLString === 'function' ) {
			try {
				return attributeValue.toHTMLString();
			} catch ( error ) {}
		}

		if ( richText && typeof richText.toHTMLString === 'function' ) {
			try {
				return richText.toHTMLString( { value: attributeValue } );
			} catch ( error ) {}
		}

		if ( typeof attributeValue.originalHTML === 'string' ) {
			return attributeValue.originalHTML;
		}

		return null;
	}
	if ( wp.data && wp.data.subscribe ) {
		wp.data.subscribe( function() {
			scheduleEditorPageSurfaceSync();
			var editorSelect = select( 'core/block-editor' );
			if ( ! editorSelect || ! editorSelect.getSelectionStart || ! editorSelect.getSelectionEnd ) return;
			var start = editorSelect.getSelectionStart();
			var end = editorSelect.getSelectionEnd();
			if (
				start && end && start.clientId && start.clientId === end.clientId &&
				start.attributeKey && start.attributeKey === end.attributeKey &&
				typeof start.offset === 'number' && typeof end.offset === 'number' &&
				start.offset !== end.offset
			) {
				savedRichTextSelection = {
					clientId: start.clientId,
					attributeKey: start.attributeKey,
					start: Math.min( start.offset, end.offset ),
					end: Math.max( start.offset, end.offset )
				};
			}
		} );
	}

	function applyMarker( color ) {
		if ( ! richText ) {
			showNotice( 'このWordPressではマーカーを利用できません。' );
			return false;
		}
		var selection = savedRichTextSelection;
		var format = MARKER_FORMATS[ color ];
		if ( ! selection || ! format ) {
			showNotice( 'マーカーを付ける文字を選択してください😊' );
			return false;
		}
		var block = select( 'core/block-editor' ).getBlock( selection.clientId );
		var attributeValue = block && block.attributes && block.attributes[ selection.attributeKey ];
		var html = richTextAttributeToHtml( attributeValue );
		if ( html === null ) {
			showNotice( 'この場所には安全にマーカーを付けられません。' );
			return false;
		}
		var value = richText.create( { html: html } );
		value = richText.applyFormat( value, { type: format.type }, selection.start, selection.end );
		dispatch( 'core/block-editor' ).updateBlockAttributes( selection.clientId, {
			[ selection.attributeKey ]: richText.toHTMLString( { value: value } )
		} );
		showNotice( format.label + 'マーカーを付けました😊' );
		return true;
	}

	function removeMarker() {
		if ( ! richText || ! savedRichTextSelection ) {
			showNotice( 'マーカーを外す文字を選択してください😊' );
			return false;
		}
		var selection = savedRichTextSelection;
		var block = select( 'core/block-editor' ).getBlock( selection.clientId );
		var attributeValue = block && block.attributes && block.attributes[ selection.attributeKey ];
		var html = richTextAttributeToHtml( attributeValue );
		if ( html === null ) {
			showNotice( 'この場所には安全にマーカーを外せません。' );
			return false;
		}
		var value = richText.create( { html: html } );
		Object.keys( MARKER_FORMATS ).forEach( function( key ) {
			value = richText.removeFormat( value, MARKER_FORMATS[ key ].type, selection.start, selection.end );
		} );
		dispatch( 'core/block-editor' ).updateBlockAttributes( selection.clientId, {
			[ selection.attributeKey ]: richText.toHTMLString( { value: value } )
		} );
		showNotice( '選択部分のマーカーを外しました😊' );
		return true;
	}

	function getHeadingStyle( item ) {
		return item.blocks()[0].attributes.className;
	}

	function applyStyleToAllHeadings( item ) {
		var headings = [];

		function collectHeadings( blocks ) {
			blocks.forEach( function( block ) {
				var headingLevel = block.attributes && ( block.attributes.level || 2 );
				if ( block.name === 'core/heading' && headingLevel >= 2 && headingLevel <= 6 ) {
					headings.push( block );
				}

				if ( block.innerBlocks && block.innerBlocks.length ) {
					collectHeadings( block.innerBlocks );
				}
			} );
		}

		collectHeadings( select( 'core/block-editor' ).getBlocks() );

		if ( ! headings.length ) {
			showNotice( 'H2〜H6見出しがありません😊' );
			return 0;
		}

		headings.forEach( function( block ) {
			dispatch( 'core/block-editor' ).updateBlockAttributes( block.clientId, {
				className: mergeDecorationClass(
					block.attributes.className,
					getHeadingStyle( item ),
					'heading'
				)
			} );
		} );
		showNotice( headings.length + '個のH2〜H6見出しを変換しました😊' );
		return headings.length;
	}

	function parseListLines( lines ) {
		var orderedCount = 0;
		var unorderedCount = 0;
		var items = lines.map( function( line ) {
			var ordered = line.match( /^\s*\d+\s*[、,.．)]\s*(.+)$/ );
			if ( ordered ) {
				orderedCount++;
				return ordered[1].trim();
			}

			var unordered = line.match( /^\s*[-・]\s*(.+)$/ );
			if ( unordered ) {
				unorderedCount++;
				return unordered[1].trim();
			}

			return line.trim();
		} ).filter( Boolean );

		if ( items.length < 2 || orderedCount + unorderedCount < 2 ) {
			return null;
		}

		return {
			ordered: orderedCount > 0 && orderedCount >= unorderedCount,
			items: items
		};
	}

	function createListBlockFromParsed( parsed, className ) {
		var tag = parsed.ordered ? 'ol' : 'ul';
		var values =
			'<' +
			tag +
			'>' +
			parsed.items
				.map( function( item ) {
					return '<li>' + escapeHtml( item ) + '</li>';
				} )
				.join( '' ) +
			'</' +
			tag +
			'>';

		return createBlock( 'core/list', {
			ordered: parsed.ordered,
			values: values,
			className: className
		} );
	}

	function transformSelectedTextToList() {
		var info = getSelectedInfo();

		if ( ! info || ! info.block ) {
			showNotice( '箇条書きにしたい文章を選択してください😊' );
			return;
		}

		if ( info.block.name !== 'core/paragraph' ) {
			showNotice( '段落の文章を選択してください😊' );
			return;
		}

		var parsed = parseListLines( contentLines( info.block ) );
		if ( ! parsed ) {
			showNotice( '2行以上の文章を入力してください😊' );
			return;
		}

		dispatch( 'core/block-editor' ).replaceBlock(
			info.clientId,
			createListBlockFromParsed( parsed, '' )
		);

		showNotice(
			parsed.ordered
				? '番号付きリストへ変換しました😊'
				: '箇条書きリストへ変換しました😊'
		);
	}

	function applyListToAllText( item ) {
		var changed = 0;
		var className = item.blocks()[0].attributes.className || '';

		function transformBlocks( blocks ) {
			blocks.forEach( function( block ) {
				if ( block.name === 'core/list' ) {
					dispatch( 'core/block-editor' ).updateBlockAttributes( block.clientId, {
						className: mergeDecorationClass(
							block.attributes.className,
							className,
							'list'
						)
					} );
					changed++;
					return;
				}

				if ( block.name === 'core/paragraph' ) {
					var parsed = parseListLines( contentLines( block ) );
					if ( parsed ) {
						dispatch( 'core/block-editor' ).replaceBlock(
							block.clientId,
							createListBlockFromParsed( parsed, className )
						);
						changed++;
						return;
					}
				}

				if ( block.innerBlocks && block.innerBlocks.length ) {
					transformBlocks( block.innerBlocks );
				}
			} );
		}

		transformBlocks( select( 'core/block-editor' ).getBlocks() );

		if ( changed ) {
			showNotice( changed + '個の箇条書きを変換しました😊' );
		} else {
			showNotice( '変換できる箇条書きがありません😊' );
		}

		return changed;
	}

	function applyButtonsToAll( item ) {
		var changed = 0;
		var className = item.blocks()[0].innerBlocks[0].attributes.className || '';

		function transformBlocks( blocks ) {
			( blocks || [] ).forEach( function( block ) {
				if ( isRecognizedButtonBlock( block ) ) {
					dispatch( 'core/block-editor' ).updateBlockAttributes( block.clientId, {
						className: mergeDecorationClass( block.attributes.className, className, 'cta' )
					} );
					changed++;
				}
				if ( block.innerBlocks && block.innerBlocks.length ) transformBlocks( block.innerBlocks );
			} );
		}

		transformBlocks( select( 'core/block-editor' ).getBlocks() );
		showNotice( changed ? changed + '個のボタンを変換しました😊' : '変換できるボタンがありません😊' );
		return changed;
	}

	function parseQaLines( lines ) {
		var questionParts = [];
		var answerParts = [];
		var state = '';

		lines.forEach( function( sourceLine ) {
			var line = sourceLine.trim();
			var q = line.match( /^Q(?:\s*$|[、,.．:：]\s*(.*)$)/i );
			var a = line.match( /^A(?:\s*$|[、,.．:：]\s*(.*)$)/i );

			if ( q ) {
				state = 'question';
				if ( q[1] ) questionParts.push( q[1].trim() );
				return;
			}

			if ( a ) {
				state = 'answer';
				if ( a[1] ) answerParts.push( a[1].trim() );
				return;
			}

			if ( state === 'question' ) questionParts.push( line );
			if ( state === 'answer' ) answerParts.push( line );
		} );

		var question = questionParts.filter( Boolean ).join( '\n' );
		var answer = answerParts.filter( Boolean ).join( '\n' );

		if ( ! question || ! answer ) {
			return null;
		}

		return {
			question: question,
			answer: answer
		};
	}

	function createQaBlock( item, qa ) {
		var template = item.blocks()[0];

		return createBlock(
			'core/group',
			{
				className: template.attributes.className,
				layout: { type: 'constrained' }
			},
			[
				createBlock( 'core/paragraph', {
					content: '<strong>' + escapeHtml( qa.question ).replace( /\n/g, '<br>' ) + '</strong>',
					className: 'aichi-ai-type-qa-question aichi-palette-question'
				} ),
				createBlock( 'core/paragraph', {
					content: escapeHtml( qa.answer ).replace( /\n/g, '<br>' ),
					className: 'aichi-ai-type-qa-answer aichi-palette-answer'
				} )
			]
		);
	}

	function isQaLabelBlock( block, label ) {
		if ( ! block || block.name !== 'core/paragraph' ) return false;
		return plainContent( block ).replace( /\s+/g, '' ).toUpperCase() === label;
	}

	function createQaBlockFromParagraphs( item, questionBlock, answerBlock ) {
		var template = item.blocks()[0];
		var questionClass = mergeDecorationClass( questionBlock.attributes && questionBlock.attributes.className, 'aichi-ai-type-qa-question aichi-palette-question', null );
		var answerClass = mergeDecorationClass( answerBlock.attributes && answerBlock.attributes.className, 'aichi-ai-type-qa-answer aichi-palette-answer', null );

		return createBlock( 'core/group', {
			className: template.attributes.className,
			layout: { type: 'constrained' }
		}, [
			createBlock( 'core/paragraph', Object.assign( {}, questionBlock.attributes, { className: questionClass } ) ),
			createBlock( 'core/paragraph', Object.assign( {}, answerBlock.attributes, { className: answerClass } ) )
		] );
	}

	function applyQaToAllText( item ) {
		var changed = 0;

		function transformBlocks( blocks ) {
			var index = 0;

			while ( index < blocks.length ) {
				var block = blocks[ index ];
				var blockClassName = block.attributes && block.attributes.className;

				if (
					block.name === 'core/group' &&
					( hasClassToken( blockClassName, 'aichi-ai-type-qa' ) ||
					  hasClassToken( blockClassName, 'aichi-palette-qa' ) )
				) {
					var template = item.blocks()[0];
					dispatch( 'core/block-editor' ).updateBlockAttributes( block.clientId, {
						className: mergeDecorationClass(
							blockClassName,
							template.attributes.className,
							'qa'
						)
					} );
					markInnerQaTypes( block );
					changed++;
					index++;
					continue;
				}

				/* Q / 質問 / A / 回答の4段落形式。質問・回答のHTML属性を保持する。 */
				if (
					index + 3 < blocks.length &&
					isQaLabelBlock( blocks[ index ], 'Q' ) &&
					blocks[ index + 1 ].name === 'core/paragraph' &&
					isQaLabelBlock( blocks[ index + 2 ], 'A' ) &&
					blocks[ index + 3 ].name === 'core/paragraph'
				) {
					dispatch( 'core/block-editor' ).replaceBlocks(
						[ blocks[ index ].clientId, blocks[ index + 1 ].clientId, blocks[ index + 2 ].clientId, blocks[ index + 3 ].clientId ],
						[ createQaBlockFromParagraphs( item, blocks[ index + 1 ], blocks[ index + 3 ] ) ]
					);
					changed++;
					index += 4;
					continue;
				}

				if ( block.name === 'core/paragraph' ) {
					var sequenceLines = [];
					var sequenceIds = [];
					var qa = null;
					var consumed = 0;

					for ( var count = 1; count <= 4 && index + count <= blocks.length; count++ ) {
						var candidate = blocks[ index + count - 1 ];
						if ( candidate.name !== 'core/paragraph' ) break;
						sequenceLines = sequenceLines.concat( contentLines( candidate ) );
						sequenceIds.push( candidate.clientId );
						qa = parseQaLines( sequenceLines );
						if ( qa ) {
							consumed = count;
							break;
						}
					}

					if ( qa && consumed ) {
						dispatch( 'core/block-editor' ).replaceBlocks(
							sequenceIds,
							[ createQaBlock( item, qa ) ]
						);
						changed++;
						index += consumed;
						continue;
					}
				}

				if ( block.innerBlocks && block.innerBlocks.length ) {
					transformBlocks( block.innerBlocks );
				}

				index++;
			}
		}

		transformBlocks( select( 'core/block-editor' ).getBlocks() );

		if ( changed ) {
			showNotice( changed + '個のQ&Aを変換しました😊' );
		} else {
			showNotice( '変換できるQ&Aがありません😊' );
		}

		return changed;
	}

	function transformSelectedBlock( item ) {
		var info = getSelectedInfo();
		if ( ! info || ! info.block ) return false;

		var selectedText = plainContent( info.block ).trim();
		var selectedContent =
			info.block.attributes && typeof info.block.attributes.content === 'string'
				? info.block.attributes.content
				: escapeHtml( selectedText );

		if (
			item.category === '見出し' &&
			info.block.name === 'core/heading' &&
			( info.block.attributes.level || 2 ) === 2
		) {
			dispatch( 'core/block-editor' ).updateBlockAttributes(
				info.clientId,
				{
					className: mergeDecorationClass(
						info.block.attributes.className,
						getHeadingStyle( item ),
						'heading'
					)
				}
			);
			return true;
		}

		if (
			item.category === 'ボタン' &&
			isRecognizedButtonBlock( info.block )
		) {
			var selectedButtonClass = item.blocks()[0].innerBlocks[0].attributes.className;
			dispatch( 'core/block-editor' ).updateBlockAttributes( info.clientId, {
				className: mergeDecorationClass( info.block.attributes.className, selectedButtonClass, 'cta' )
			} );
			return true;
		}

		if (
			item.category === 'Q&A' &&
			info.block.name === 'core/group' &&
			( hasClassToken( info.block.attributes.className, 'aichi-ai-type-qa' ) ||
			  hasClassToken( info.block.attributes.className, 'aichi-palette-qa' ) )
		) {
			var selectedQaTemplate = item.blocks()[0];
			dispatch( 'core/block-editor' ).updateBlockAttributes( info.clientId, {
				className: mergeDecorationClass(
					info.block.attributes.className,
					selectedQaTemplate.attributes.className,
					'qa'
				)
			} );
			markInnerQaTypes( info.block );
			return true;
		}

		if (
			item.category === '箇条書き' &&
			info.block.name === 'core/list'
		) {
			var selectedListClass = item.blocks()[0].attributes.className;
			dispatch( 'core/block-editor' ).updateBlockAttributes( info.clientId, {
				className: mergeDecorationClass(
					info.block.attributes.className,
					selectedListClass,
					'list'
				)
			} );
			return true;
		}

		if (
			item.category === 'Q&A' &&
			( info.block.name === 'core/paragraph' || info.block.name === 'core/heading' )
		) {
			var qaTemplate = item.blocks()[0];
			dispatch( 'core/block-editor' ).replaceBlock(
				info.clientId,
				createBlock(
					'core/group',
					{
						className: qaTemplate.attributes.className,
						layout: { type: 'constrained' }
					},
					[
						createBlock( 'core/paragraph', {
							content: '<strong>' + ( selectedContent || 'ここに質問を入力します。' ) + '</strong>',
							className: 'aichi-ai-type-qa-question aichi-palette-question'
						} ),
						createBlock( 'core/paragraph', {
							content: 'ここに回答を入力します。',
							className: 'aichi-ai-type-qa-answer aichi-palette-answer'
						} )
					]
				)
			);
			return true;
		}

		if (
			item.category === 'ボックス' &&
			info.block.name === 'core/group' &&
			( hasClassToken( info.block.attributes.className, 'aichi-ai-type-box' ) ||
			  hasClassToken( info.block.attributes.className, 'aichi-palette-box' ) )
		) {
			var selectedBoxTemplate = item.blocks()[0];
			dispatch( 'core/block-editor' ).updateBlockAttributes( info.clientId, {
				className: mergeDecorationClass(
					info.block.attributes.className,
					selectedBoxTemplate.attributes.className,
					'box'
				)
			} );
			return true;
		}

		if (
			item.category === 'ボックス' &&
			info.block.name === 'core/paragraph'
		) {
			var boxTemplate = item.blocks()[0];
			dispatch( 'core/block-editor' ).replaceBlock(
				info.clientId,
				createBlock(
					'core/group',
					{
						className: boxTemplate.attributes.className,
						layout: { type: 'constrained' }
					},
					[
						createBlock( 'core/paragraph', {
							content: selectedContent || 'ここに文章を入力します。'
						} )
					]
				)
			);
			return true;
		}

		return false;
	}

	function insertItem( item ) {
		var transformed = transformSelectedBlock( item );
		if ( ! transformed ) {
			insertAfterSelection( item.blocks() );
		}

		try {
			var recent = JSON.parse(
				window.localStorage.getItem( 'aichiPaletteRecent' ) || '[]'
			);
			recent = [ item.id ].concat(
				recent.filter( function( id ) {
					return id !== item.id;
				} )
			).slice( 0, 8 );
			window.localStorage.setItem(
				'aichiPaletteRecent',
				JSON.stringify( recent )
			);
		} catch ( error ) {}
	}

	/*
	 * 一発変換エンジン登録所
	 * ----------------------
	 * スタンプ側の bulkEngine と、ここに登録した処理を結び付けます。
	 * 将来セットを追加するときは ITEMS にデータを追加し、必要な場合だけ
	 * この登録所へ新しい変換処理を1つ追加すれば、UI本体の変更は不要です。
	 */
	var BULK_ENGINES = {
		'heading-all': function( item ) {
			return applyStyleToAllHeadings( item );
		},
		list: function( item ) {
			return applyListToAllText( item );
		},
		qa: function( item ) {
			return applyQaToAllText( item );
		},
		cta: function( item ) {
			return applyButtonsToAll( item );
		}
	};

	function runBulkEngine( item ) {
		var engine = item.bulkEngine && BULK_ENGINES[ item.bulkEngine ];

		if ( ! engine ) {
			showNotice( 'このスタンプは通常追加で使えます😊' );
			return false;
		}

		engine( item );
		return true;
	}

	function collectPaletteDocuments() {
		var documents = [];

		function collect( currentDocument ) {
			if ( ! currentDocument || documents.indexOf( currentDocument ) >= 0 ) return;
			documents.push( currentDocument );
			Array.prototype.forEach.call( currentDocument.querySelectorAll ? currentDocument.querySelectorAll( 'iframe' ) : [], function( frame ) {
				try {
					collect( frame.contentDocument );
				} catch ( error ) {}
			} );
		}

		collect( document );
		return documents;
	}

	/* 追加直後も可能な範囲で、現在の編集画面と編集iframeへ安全なCSSを反映する。 */
	function updateCoordinateStyleSheets( css ) {
		collectPaletteDocuments().forEach( function( currentDocument ) {
			if ( ! currentDocument.createElement ) return;
			var style = currentDocument.getElementById
				? currentDocument.getElementById( 'aichi-palette-custom-coordinate-styles' )
				: null;
			if ( ! style ) {
				style = currentDocument.createElement( 'style' );
				style.id = 'aichi-palette-custom-coordinate-styles';
				style.type = 'text/css';
				var parent = currentDocument.head || currentDocument.body;
				if ( ! parent || ! parent.appendChild ) return;
				parent.appendChild( style );
			}
			style.textContent = String( css || '' );
		} );
	}

	/* サーバー送信前の一次検査。最終判定はWordPress側で同じ項目を再検証する。 */
	function validateCoordinateFileObject( coordinate ) {
		if ( ! coordinate || Array.isArray( coordinate ) || typeof coordinate !== 'object' ) {
			return 'このファイルはAIちいちゃんパレット用コーデとして読み込めません。';
		}

		var encoded;
		try {
			encoded = JSON.stringify( coordinate );
		} catch ( error ) {
			return 'コーデファイルを読み取れませんでした。';
		}

		if ( /<\s*script|<\?php|javascript\s*:|data\s*:\s*text\/html|on[a-z]+\s*=/i.test( encoded ) ) {
			return '危険なコードが含まれているため、このコーデは追加できません。';
		}

		var allowedKeys = [ 'format', 'schemaVersion', 'id', 'version', 'name', 'description', 'icon', 'style', 'fontBody', 'fontHeading', 'headingDesign', 'assets', 'pageBackground' ];
		if ( Object.keys( coordinate ).some( function( key ) { return allowedKeys.indexOf( key ) < 0; } ) ) {
			return '許可されていない項目が含まれているため、このコーデは追加できません。';
		}
		var schemaVersion = Number( coordinate.schemaVersion );
		if ( coordinate.format !== COORDINATE_FORMAT ||
			[ COORDINATE_LEGACY_SCHEMA_VERSION, COORDINATE_ASSET_SCHEMA_VERSION, COORDINATE_SCHEMA_VERSION ].indexOf( schemaVersion ) < 0 ) {
			return 'このファイルはAIちいちゃんパレット用コーデとして読み込めません。';
		}
		if ( schemaVersion === COORDINATE_LEGACY_SCHEMA_VERSION && coordinate.assets !== undefined ) {
			return '画像素材を含むコーデには新しいファイル形式が必要です。';
		}
		if ( typeof coordinate.id !== 'string' || ! /^chiichan-[a-z0-9][a-z0-9-]{1,54}$/.test( coordinate.id ) ) {
			return 'コーデIDが正しくありません。';
		}
		if ( typeof coordinate.version !== 'string' || ! /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z0-9.-]+)?$/i.test( coordinate.version ) ) {
			return 'コーデのバージョンが正しくありません。';
		}
		if ( typeof coordinate.name !== 'string' || ! coordinate.name ||
			typeof coordinate.description !== 'string' || ! coordinate.description ||
			typeof coordinate.icon !== 'string' || ! coordinate.icon ) {
			return 'コーデ名またはアイコンが正しくありません。';
		}
		if ( ! coordinate.style || Array.isArray( coordinate.style ) || typeof coordinate.style !== 'object' ) {
			return 'コーデのスタイル情報が正しくありません。';
		}

		var colorKeys = [ 'primary', 'secondary', 'accent', 'background', 'surface', 'text' ];
		var styleKeys = colorKeys.concat( [ 'radius' ] );
		if ( Object.keys( coordinate.style ).some( function( key ) { return styleKeys.indexOf( key ) < 0; } ) ||
			colorKeys.some( function( key ) { return typeof coordinate.style[ key ] !== 'string' || ! /^#[0-9a-f]{6}$/i.test( coordinate.style[ key ] ); } ) ) {
			return 'コーデに使用できないスタイルが含まれています。';
		}
		if ( coordinate.headingDesign !== undefined &&
			[ 'default', 'nordic-christmas-h2-v1', 'nordic-christmas-h2-v2', 'nordic-christmas-h2-v3', 'christmas-card-h2-v2', 'christmas-card-h2-v2-finish', 'christmas-card-h2-v2-refinish', 'christmas-card-h2-v2-final', 'christmas-card-h2-h3-v1', 'christmas-card-complete-v1' ].indexOf( coordinate.headingDesign ) < 0 ) {
			return '指定された見出しデザインには対応していません。';
		}

		if ( coordinate.assets !== undefined ) {
			if ( ! coordinate.assets || Array.isArray( coordinate.assets ) || typeof coordinate.assets !== 'object' ) {
				return 'コーデの画像素材情報が正しくありません。';
			}
			var standardAssetSlots = [ 'heading', 'accent', 'subheading', 'special', 'qa' ];
			var backgroundAssetSlots = [ 'backgroundDesktop', 'backgroundMobile' ];
			var assetSlots = schemaVersion === COORDINATE_SCHEMA_VERSION
				? standardAssetSlots.concat( backgroundAssetSlots )
				: standardAssetSlots;
			if ( Object.keys( coordinate.assets ).some( function( key ) { return assetSlots.indexOf( key ) < 0; } ) ) {
				return '許可されていない画像素材が含まれています。';
			}
			var assetError = Object.keys( coordinate.assets ).some( function( slot ) {
				var asset = coordinate.assets[ slot ];
				if ( ! asset || Array.isArray( asset ) || typeof asset !== 'object' ) return true;
				if ( Object.keys( asset ).some( function( key ) { return [ 'mime', 'data' ].indexOf( key ) < 0; } ) ) return true;
				if ( [ 'image/png', 'image/webp', 'image/jpeg' ].indexOf( asset.mime ) < 0 ) return true;
				if ( typeof asset.data !== 'string' || ! asset.data || ! /^[A-Za-z0-9+\/=]+$/.test( asset.data ) ) return true;
				var limit = backgroundAssetSlots.indexOf( slot ) >= 0
					? DEFAULT_COORDINATE_BACKGROUND_MAX_BYTES
					: DEFAULT_COORDINATE_ASSET_MAX_BYTES;
				return Math.ceil( asset.data.length * 3 / 4 ) > limit;
			} );
			if ( assetError ) {
				return '使用できない画像素材が含まれています。';
			}
		}
		if ( coordinate.pageBackground !== undefined ) {
			if ( schemaVersion !== COORDINATE_SCHEMA_VERSION || ! coordinate.pageBackground ||
				Array.isArray( coordinate.pageBackground ) || typeof coordinate.pageBackground !== 'object' ||
				! coordinate.assets || ! coordinate.assets.backgroundDesktop ) {
				return 'ページ背景の設定またはPC用背景画像を確認できません。';
			}
			var backgroundKeys = [ 'baseColor', 'contentColor', 'contentOpacity', 'desktopSize', 'desktopPosition', 'desktopRepeat', 'mobileSize', 'mobilePosition', 'mobileRepeat' ];
			if ( Object.keys( coordinate.pageBackground ).some( function( key ) { return backgroundKeys.indexOf( key ) < 0; } ) ) {
				return 'ページ背景に対応していない設定が含まれています。';
			}
			if ( ! /^#[0-9a-f]{6}$/i.test( String( coordinate.pageBackground.baseColor || '' ) ) ||
				! /^#[0-9a-f]{6}$/i.test( String( coordinate.pageBackground.contentColor || '' ) ) ) {
				return 'ページ背景に使用できない色が含まれています。';
			}
			var opacity = Number( coordinate.pageBackground.contentOpacity );
			if ( ! Number.isInteger( opacity ) || opacity < 82 || opacity > 100 ) {
				return '本文背景の透明度は82〜100で指定してください。';
			}
			var sizes = [ 'cover', 'contain', 'auto' ];
			var positions = [ 'top-left', 'top-center', 'top-right', 'center-center' ];
			var repeats = [ 'no-repeat', 'repeat', 'repeat-x', 'repeat-y' ];
			if ( sizes.indexOf( coordinate.pageBackground.desktopSize || 'cover' ) < 0 ||
				sizes.indexOf( coordinate.pageBackground.mobileSize || 'cover' ) < 0 ||
				positions.indexOf( coordinate.pageBackground.desktopPosition || 'top-center' ) < 0 ||
				positions.indexOf( coordinate.pageBackground.mobilePosition || 'top-center' ) < 0 ||
				repeats.indexOf( coordinate.pageBackground.desktopRepeat || 'no-repeat' ) < 0 ||
				repeats.indexOf( coordinate.pageBackground.mobileRepeat || 'no-repeat' ) < 0 ) {
				return 'ページ背景の表示方法に対応していない値が含まれています。';
			}
		}
		return '';
	}

	function coordinateErrorMessage( error ) {
		var message = error && error.message ? String( error.message ) : '';
		return message && message.length <= 180
			? message
			: 'コーデを追加できませんでした。もう一度お試しください。';
	}

	function currentEditorPostId() {
		var configured = Number( paletteConfig.currentPostId ) || 0;
		if ( configured > 0 ) return configured;
		var editorStore = select( 'core/editor' );
		return editorStore && editorStore.getCurrentPostId ? Number( editorStore.getCurrentPostId() ) || 0 : 0;
	}

	/*
	 * Ver.1.1 クローゼット導線
	 * --------------------------
	 * WordPressの記事編集やパレット処理には触れず、各スクロール領域の
	 * 最下部からクローゼットを別タブで開くだけの独立したリンクです。
	 */
	function renderClosetLink( key ) {
		return el(
			'div',
			{ key: key, className: 'aichi-palette-closet' },
			el(
				'a',
				{
					href: CLOSET_URL,
					target: '_blank',
					rel: 'noopener noreferrer',
					className: 'aichi-palette-closet-link',
					'aria-label': 'AIちいちゃんクローゼットで新しいコーデを見る（新しいタブで開きます）'
				},
				'👗 新しいコーデを見る'
			),
			el( 'span', { className: 'aichi-palette-closet-name' }, 'AIちいちゃんクローゼット' )
		);
	}

	function App() {
		var _open = useState( false );
		var open = _open[0];
		var setOpen = _open[1];

		var _manage = useState( false );
		var manage = _manage[0];
		var setManage = _manage[1];

		var _category = useState( 'お気に入り' );
		var category = _category[0];
		var setCategory = _category[1];

		var _search = useState( '' );
		var search = _search[0];
		var setSearch = _search[1];

		var _favorites = useState( readFavorites() );
		var favorites = _favorites[0];
		var setFavorites = _favorites[1];

		var _bulkMode = useState( false );
		var bulkMode = _bulkMode[0];
		var setBulkMode = _bulkMode[1];

		var _historyVersion = useState( 0 );
		var historyVersion = _historyVersion[0];
		var setHistoryVersion = _historyVersion[1];

		useEffect( function() {
			saveFavorites( favorites );
		}, [ favorites ] );

		useEffect( function() {
			return subscribeDecorationHistory( function() {
				setHistoryVersion( function( value ) { return value + 1; } );
			} );
		}, [] );

		var canUndo = decorationHistory.undo.length > 0;
		var canRedo = decorationHistory.redo.length > 0;
		void historyVersion;

		var _setPanel = useState( false );
		var setPanel = _setPanel[0];
		var setSetPanel = _setPanel[1];
		var _roleSection = useState( 'parts' );
		var roleSection = _roleSection[0];
		var setRoleSection = _roleSection[1];
		var _fontStatus = useState( { phase: 'idle', text: '', fontId: '' } );
		var fontStatus = _fontStatus[0];
		var setFontStatus = _fontStatus[1];
		var fontApplyTimer = useRef( null );
		var fontHideTimer = useRef( null );
		var _coordinateRevision = useState( 0 );
		var coordinateRevision = _coordinateRevision[0];
		var setCoordinateRevision = _coordinateRevision[1];
		var _coordinateBusy = useState( false );
		var coordinateBusy = _coordinateBusy[0];
		var setCoordinateBusy = _coordinateBusy[1];
		var _coordinateStatus = useState( { phase: 'idle', text: '' } );
		var coordinateStatus = _coordinateStatus[0];
		var setCoordinateStatus = _coordinateStatus[1];
		var coordinateFileInput = useRef( null );
		var coordinateStatusTimer = useRef( null );

		/*
		 * Ver.1 書体フィードバック
		 * -------------------------
		 * 書体エンジンと履歴処理には触れず、UI側で短い待機表示を行います。
		 * 180ms以内の連打は最後に押した書体へまとめ、履歴の重複を防ぎます。
		 */
		function queueFontChange( fontId ) {
			var font = FONT_DEFINITIONS[ fontId ];
			if ( ! font ) return false;

			if ( fontApplyTimer.current ) window.clearTimeout( fontApplyTimer.current );
			if ( fontHideTimer.current ) window.clearTimeout( fontHideTimer.current );

			setFontStatus( {
				phase: 'working',
				fontId: fontId,
				text: fontId === 'original'
					? '🔤 元の書体に戻しています…'
					: '🔤 ' + font.label + 'に着せ替え中…'
			} );

			fontApplyTimer.current = window.setTimeout( function() {
				fontApplyTimer.current = null;
				try {
					runWithDecorationHistory( font.label + 'へ変更', function() {
						return applyFontProfile( fontId );
					} );

					setFontStatus( {
						phase: 'done',
						fontId: fontId,
						text: fontId === 'original'
							? '✓ 元の書体に戻しました'
							: '✓ ' + font.label + 'に着せ替えました'
					} );
				} catch ( error ) {
					setFontStatus( {
						phase: 'error',
						fontId: fontId,
						text: '書体の変更に失敗しました。もう一度お試しください。'
					} );
					try {
						showNotice( '処理に失敗しました。もう一度お試しください。' );
					} catch ( noticeError ) {}
				}

				fontHideTimer.current = window.setTimeout( function() {
					fontHideTimer.current = null;
					setFontStatus( { phase: 'idle', text: '', fontId: '' } );
				}, 720 );
			}, 180 );
			return true;
		}

		useEffect( function() {
			return function() {
				if ( fontApplyTimer.current ) window.clearTimeout( fontApplyTimer.current );
				if ( fontHideTimer.current ) window.clearTimeout( fontHideTimer.current );
				if ( coordinateStatusTimer.current ) window.clearTimeout( coordinateStatusTimer.current );
			};
		}, [] );

		function showCoordinateStatus( phase, text, autoHide ) {
			if ( coordinateStatusTimer.current ) {
				window.clearTimeout( coordinateStatusTimer.current );
				coordinateStatusTimer.current = null;
			}
			setCoordinateStatus( { phase: phase, text: text } );
			if ( autoHide ) {
				coordinateStatusTimer.current = window.setTimeout( function() {
					coordinateStatusTimer.current = null;
					setCoordinateStatus( { phase: 'idle', text: '' } );
				}, 2400 );
			}
		}

		function importCoordinateFile( file ) {
			if ( coordinateBusy || ! file ) return;
			if ( ! apiFetch || ! window.FileReader ) {
				showCoordinateStatus( 'error', 'この環境ではコーデファイルを読み込めません。', true );
				return;
			}

			var maxBytes = Number( paletteConfig.maxCoordinateBytes ) || DEFAULT_COORDINATE_MAX_BYTES;
			maxBytes = Math.min( maxBytes, DEFAULT_COORDINATE_MAX_BYTES );
			if ( ! /\.json$/i.test( String( file.name || '' ) ) ) {
				showCoordinateStatus( 'error', 'AIちいちゃんパレット用のJSONファイルを選んでください。', true );
				return;
			}
			if ( ! file.size || file.size > maxBytes ) {
				showCoordinateStatus( 'error', 'コーデファイルのサイズが大きすぎるか、内容が空です。', true );
				return;
			}

			setCoordinateBusy( true );
			showCoordinateStatus( 'working', '👗 コーデファイルを確認しています…', false );

			var reader = new window.FileReader();
			reader.onerror = function() {
				setCoordinateBusy( false );
				showCoordinateStatus( 'error', 'コーデファイルを読み取れませんでした。', true );
			};
			reader.onload = function( event ) {
				var coordinate;
				try {
					coordinate = JSON.parse( String( event.target && event.target.result || '' ) );
				} catch ( error ) {
					setCoordinateBusy( false );
					showCoordinateStatus( 'error', '壊れたコーデファイルのため読み込めません。', true );
					return;
				}

				var validationError = validateCoordinateFileObject( coordinate );
				if ( validationError ) {
					setCoordinateBusy( false );
					showCoordinateStatus( 'error', validationError, true );
					return;
				}
				if ( SET_DEFINITIONS[ coordinate.id ] ) {
					setCoordinateBusy( false );
					showCoordinateStatus( 'error', 'このコーデはすでに追加されています。', true );
					return;
				}

				apiFetch( {
					path: '/aichi-palette/v1/coordinates',
					method: 'POST',
					data: { coordinate: coordinate, postId: currentEditorPostId() }
				} ).then( function( response ) {
					var registered = registerCoordinateDefinition( response && response.coordinate );
					if ( ! registered ) throw new Error( 'コーデをセット一覧へ追加できませんでした。' );
					updateCoordinateStyleSheets( response.css || '' );
					setCoordinateRevision( function( value ) { return value + 1; } );
					showCoordinateStatus( 'done', response.message || '🎉 コーデを追加しました', true );
					showNotice( response.message || '🎉 コーデを追加しました' );
				} ).catch( function( error ) {
					showCoordinateStatus( 'error', coordinateErrorMessage( error ), true );
				} ).then( function() {
					setCoordinateBusy( false );
				} );
			};
			reader.readAsText( file, 'UTF-8' );
		}

		function archiveCoordinate( definition ) {
			if ( coordinateBusy || ! definition || ! definition.isExternal || ! apiFetch ) return;
			if ( ! window.confirm( '「' + definition.label + '」をセット一覧から削除しますか？\n既存記事の装飾はそのまま残ります。' ) ) return;

			setCoordinateBusy( true );
			showCoordinateStatus( 'working', '追加コーデを整理しています…', false );
			apiFetch( {
				path: '/aichi-palette/v1/coordinates/' + encodeURIComponent( definition.id ) + '?postId=' + encodeURIComponent( currentEditorPostId() ),
				method: 'DELETE'
			} ).then( function( response ) {
				delete SET_DEFINITIONS[ definition.id ];
				updateCoordinateStyleSheets( response.css || '' );
				setCoordinateRevision( function( value ) { return value + 1; } );
				showCoordinateStatus( 'done', response.message || 'コーデを一覧から削除しました。', true );
			} ).catch( function( error ) {
				showCoordinateStatus( 'error', coordinateErrorMessage( error ), true );
			} ).then( function() {
				setCoordinateBusy( false );
			} );
		}

		/* 保存方式は変えず、表示直前だけ既存データと安全に同期する。 */
		function refreshFavoritesView() {
			var storedFavorites = readFavorites();
			setFavorites( function( currentFavorites ) {
				return JSON.stringify( currentFavorites ) === JSON.stringify( storedFavorites )
					? currentFavorites
					: storedFavorites;
			} );
		}

		useEffect( function() {
			if ( open && roleSection === 'parts' && ! manage && category === 'お気に入り' ) {
				refreshFavoritesView();
			}
		}, [ open, roleSection, manage, category, historyVersion ] );

		var articleSets = Object.keys( SET_DEFINITIONS ).map( function( setId ) {
			return SET_DEFINITIONS[ setId ];
		} ).filter( function( definition ) {
			return definition && definition.showInArticleSets !== false &&
				typeof definition.styles === 'object';
		} );
		void coordinateRevision;

		function toggleFavorite( id ) {
			if ( favorites.indexOf( id ) >= 0 ) {
				setFavorites(
					favorites.filter( function( value ) {
						return value !== id;
					} )
				);
			} else {
				setFavorites( favorites.concat( [ id ] ) );
			}
		}

		var categories = [
			'お気に入り',
			'見出し',
			'ボックス',
			'Q&A',
			'箇条書き',
			'ボタン',
			'記事セット'
			,'文字装飾'
		];

		var visibleItems = ITEMS.filter( function( item ) {
			var categoryMatch =
				category === 'お気に入り'
					? favorites.indexOf( item.id ) >= 0
					: item.category === category;

			var searchMatch =
				! search ||
				item.label.indexOf( search ) >= 0 ||
				item.description.indexOf( search ) >= 0 ||
				item.category.indexOf( search ) >= 0;

			return categoryMatch && searchMatch;
		} );

		return el(
			Fragment,
			null,
			el(
				'button',
				{
					type: 'button',
					className:
						'aichi-palette-tab' +
					( open ? ' is-open' : '' ),
				onClick: function() {
					if ( ! open ) {
						setCategory( 'お気に入り' );
						refreshFavoritesView();
					}
					setOpen( ! open );
					if ( open ) setManage( false );
					},
					'aria-label': 'AIちいちゃんパレットを開く'
				},
				el( 'span', { className: 'aichi-palette-tab-icon' }, 'AI' ),
				el( 'span', { className: 'aichi-palette-tab-text' }, 'ちいちゃん' )
			),
			open &&
				el(
					'div',
					{
						className:
							'aichi-palette-panel' +
							( manage ? ' is-manage' : '' )
					},
					el(
						'div',
						{ className: 'aichi-palette-header' },
						el(
							'div',
							null,
							el(
								'strong',
								{ className: 'aichi-palette-title' },
								'AIちいちゃんパレット Pro'
							),
							el(
								'div',
								{ className: 'aichi-palette-subtitle' },
								manage
									? 'お気に入りを整理する'
									: '使いたいスタンプを選ぶ'
							)
						),
						el(
							'button',
							{
								type: 'button',
								className: 'aichi-palette-close',
								onClick: function() {
									setOpen( false );
									setManage( false );
								}
							},
							'×'
						)
					),
					el(
						'div',
						{ className: 'aichi-palette-quick-actions' },
						el(
							'button',
							{
								type: 'button',
								className: 'aichi-palette-insert aichi-palette-set-toggle' +
									( setPanel ? ' is-active' : '' ),
								onClick: function() { setSetPanel( ! setPanel ); setManage( false ); },
								'aria-expanded': setPanel
							},
							el( 'span', { className: 'aichi-palette-icon' }, '👗' ),
							el( 'span', { className: 'aichi-palette-label' }, '一発着せ替え' )
						)
					),
					el(
						'div',
						{ className: 'aichi-palette-history-actions' },
						el(
							'button',
							{
								type: 'button',
								disabled: ! canUndo,
								onClick: undoDecoration,
								'aria-label': 'パレットの装飾を1つ戻す'
							},
							'↶ 戻る'
						),
						el(
							'button',
							{
								type: 'button',
								disabled: ! canRedo,
								onClick: redoDecoration,
								'aria-label': '戻した装飾をやり直す'
							},
							'↷ やり直す'
						)
					),
					setPanel &&
						el(
							'div',
							{ className: 'aichi-palette-set-panel' },
							articleSets.map( function( definition ) {
								var applied = isArticleSetApplied( definition.id );
							return el(
								'div',
								{
									key: definition.id,
									className: 'aichi-palette-set-card is-' + definition.id +
										( definition.isExternal ? ' is-custom-coordinate' : '' )
								},
								el( 'div', { className: 'aichi-palette-set-name' }, ( definition.icon || '👗' ) + ' ' + definition.label ),
								definition.isExternal && el(
									'div',
									{ className: 'aichi-palette-coordinate-meta' },
									'追加コーデ v' + definition.version
								),
								definition.isExternal && paletteConfig.canManageCoordinates && el(
									'button',
									{
										type: 'button',
										className: 'aichi-palette-delete-coordinate',
										disabled: coordinateBusy,
										onClick: function() { archiveCoordinate( definition ); },
										'aria-label': definition.label + 'をセット一覧から削除',
										title: '追加コーデを削除'
									},
									'×'
								),
								el( 'div', { className: 'aichi-palette-set-description' }, definition.description || '記事全体をまとめてコーデ' ),
									applied && el( 'div', { className: 'aichi-palette-set-status' }, '✓ ' + definition.label + '適用中' ),
									el( 'button', {
										type: 'button',
										className: 'aichi-palette-apply-set',
										onClick: function() {
											runWithDecorationHistory( definition.label + 'を適用', function() {
												return applyArticleSet( definition.id );
											} );
										}
									}, applied ? definition.label + 'を再適用' : 'このセットに着せ替える' )
								);
							} ),
							paletteConfig.canManageCoordinates && el(
								'div',
								{ className: 'aichi-palette-coordinate-import' },
								el( 'input', {
									ref: coordinateFileInput,
									type: 'file',
									className: 'aichi-palette-coordinate-file',
									accept: '.json,application/json',
									onChange: function( event ) {
										var input = event.target;
										var file = input && input.files && input.files[0];
										if ( input ) input.value = '';
										importCoordinateFile( file );
									}
								} ),
								el(
									'button',
									{
										type: 'button',
										className: 'aichi-palette-add-coordinate',
										disabled: coordinateBusy,
										onClick: function() {
											if ( coordinateFileInput.current ) {
												coordinateFileInput.current.value = '';
												coordinateFileInput.current.click();
											}
										}
									},
									coordinateBusy ? 'コーデを確認中…' : '👗 コーデを追加'
								),
								coordinateStatus.phase !== 'idle' && el(
									'div',
									{
										className: 'aichi-palette-coordinate-status is-' + coordinateStatus.phase,
										role: 'status',
										'aria-live': 'polite'
									},
									coordinateStatus.text
								)
							),
							renderClosetLink( 'closet-sets' )
						),
					el( 'div', { className: 'aichi-palette-role-tabs' },
						[ [ 'parts', '🧩 パーツ' ], [ 'text', '🔤 文字' ], [ 'extra', '🖍 ちょい足し' ] ].map( function( role ) {
							return el( 'button', {
								key: role[0],
								type: 'button',
								className: roleSection === role[0] ? 'is-active' : '',
								onClick: function() {
									setRoleSection( role[0] );
									setSetPanel( false );
									setManage( false );
									if ( role[0] === 'parts' ) {
										setCategory( 'お気に入り' );
										setSearch( '' );
										refreshFavoritesView();
									}
								}
							}, role[1] );
						} )
					),
					! setPanel && roleSection === 'parts' && el(
						'div',
						{ className: 'aichi-palette-parts-actions' },
						el( 'button', {
							type: 'button',
							className: 'aichi-palette-insert aichi-palette-convert' + ( bulkMode ? ' is-active' : '' ),
							onClick: function() { setBulkMode( ! bulkMode ); },
							'aria-pressed': bulkMode,
							'aria-label': '記事内を選んだ装飾で一括変換するモード'
						}, el( 'span', { className: 'aichi-palette-icon' }, '⚡' ), el( 'span', { className: 'aichi-palette-label' }, bulkMode ? '一発変換 ON' : 'パーツを一発変換' ) )
					),
					! setPanel && roleSection === 'text' && el(
						'div',
						{
							className: 'aichi-palette-font-wrap',
							'aria-busy': fontStatus.phase === 'working'
						},
						el( 'div', { className: 'aichi-palette-role-title' }, '🔤 文字を着せ替える' ),
						el(
							'div',
							{
								className: 'aichi-palette-font-status is-' + fontStatus.phase,
								role: 'status',
								'aria-live': 'polite',
								'aria-atomic': 'true',
								'aria-hidden': fontStatus.phase === 'idle'
							},
							fontStatus.text || '書体を変更しています'
						),
						el( 'div', { className: 'aichi-palette-font-panel' },
							[ 'gothic', 'rounded', 'mincho', 'original' ].map( function( fontId ) {
								var font = FONT_DEFINITIONS[ fontId ];
								return el( 'button', {
									key: fontId,
									type: 'button',
									className: 'aichi-font-choice is-' + fontId +
										( isFontProfileApplied( fontId ) ? ' is-active' : '' ) +
										( fontStatus.phase === 'working' && fontStatus.fontId === fontId ? ' is-changing' : '' ),
									onClick: function() {
										queueFontChange( fontId );
									}
								}, font.label );
							} )
						),
						renderClosetLink( 'closet-fonts' )
					),
					! setPanel && roleSection === 'extra' && el(
						'div',
						{ className: 'aichi-palette-marker-wrap' },
						el( 'div', { className: 'aichi-palette-role-title' }, '🖍 ちょい足し装飾' ),
						el( 'div', { className: 'aichi-palette-marker-panel' },
							PAINT_DEFINITIONS.map( function( definition ) {
								return el( 'button', {
									key: definition.id,
									type: 'button',
									className: 'aichi-paint-card ' + ( definition.previewClass || '' ),
									onMouseDown: function( event ) { event.preventDefault(); },
									onClick: function() {
										runWithDecorationHistory( definition.historyLabel || definition.label, definition.apply );
									}
								},
								el( 'span', { className: 'aichi-paint-label' }, definition.label ),
								el( 'span', { className: 'aichi-paint-preview', 'aria-hidden': 'true' } ),
								el( 'small', null, definition.description || '' )
								);
							} )
						),
						renderClosetLink( 'closet-paints' )
					),
					! setPanel && roleSection === 'parts' && ! manage &&
						el(
							'div',
							{ className: 'aichi-palette-mini-actions' },
							el(
								'button',
								{
									type: 'button',
									className: 'is-active',
									onClick: function() {
										setCategory( 'お気に入り' );
										refreshFavoritesView();
									}
								},
								'⭐ お気に入り'
							),
							el(
								'button',
								{
									type: 'button',
									onClick: function() {
										setManage( true );
									}
								},
								'⚙ 管理'
							)
						),
					! setPanel && roleSection === 'parts' && manage &&
						el( TextControl, {
							label: 'スタンプを検索',
							value: search,
							onChange: setSearch,
							placeholder: 'Q&A、見出し、特典など'
						} ),
					! setPanel && roleSection === 'parts' && manage &&
						el(
							'div',
							{ className: 'aichi-palette-categories' },
							categories.map( function( name ) {
								return el(
									'button',
									{
										key: name,
										type: 'button',
										className:
											category === name
												? 'is-active'
												: '',
										onClick: function() {
											setCategory( name );
											if ( name === 'お気に入り' ) refreshFavoritesView();
										}
									},
									name
								);
							} )
						),
					! setPanel && roleSection === 'parts' && el(
						'div',
						{ className: 'aichi-palette-grid' },
						visibleItems.length
							? visibleItems.map( function( item ) {
									var isFavorite =
										favorites.indexOf( item.id ) >= 0;

									return el(
										'div',
										{
											key: item.id,
											className: 'aichi-palette-card'
										},
										el(
											'button',
											{
												type: 'button',
								className:
									'aichi-palette-insert',
									onClick: function() {
										if ( bulkMode ) {
											if ( runWithDecorationHistory( item.label + ' 一発変換', function() {
												return runBulkEngine( item );
											} ) ) {
												setBulkMode( false );
											}
											return;
										}
										runWithDecorationHistory( item.label + ' 個別装飾', function() {
											return insertItem( item );
										} );
									}
											},
											el(
												'span',
												{
													className:
														'aichi-palette-icon'
												},
												item.icon
											),
											el(
												'span',
												{
													className:
														'aichi-palette-label'
												},
												item.label
											),
											el(
												'small',
												null,
												item.description
											)
										),
										manage &&
											el(
												'button',
												{
													type: 'button',
													className:
														'aichi-palette-favorite' +
														( isFavorite
															? ' is-favorite'
															: '' ),
													onClick: function() {
														toggleFavorite( item.id );
													},
													title: isFavorite
														? 'お気に入りから外す'
														: 'お気に入りへ追加'
												},
												isFavorite ? '★' : '☆'
											)
									);
							  } )
							: el(
									'div',
									{ className: 'aichi-palette-empty' },
									'表示できるスタンプがありません。'
							  ),
						! manage && renderClosetLink( 'closet-parts' )
					),
					! setPanel && roleSection === 'parts' && manage &&
						el(
							'div',
							{ className: 'aichi-palette-footer' },
							el(
								'button',
								{
									type: 'button',
									className: 'aichi-palette-done',
									onClick: function() {
										setManage( false );
										setCategory( 'お気に入り' );
										setSearch( '' );
										refreshFavoritesView();
									}
								},
								'お気に入りだけの表示へ戻る'
							)
						)
				)
		);
	}

	function mount() {
		if ( document.getElementById( 'aichi-palette-root' ) ) return;

		var root = document.createElement( 'div' );
		root.id = 'aichi-palette-root';
		document.body.appendChild( root );

		if ( wp.element.createRoot ) {
			wp.element.createRoot( root ).render( el( App ) );
		} else {
			wp.element.render( el( App ), root );
		}
		scheduleEditorPageSurfaceSync();
	}

	if ( typeof domReady === 'function' ) {
		domReady( function() {
			window.setTimeout( mount, 700 );
		} );
	} else {
		window.setTimeout( mount, 700 );
	}

} )( window.wp );
