<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://eqlwiki.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Maintenance+script</id>
	<title>EverQuest Legends Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://eqlwiki.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Maintenance+script"/>
	<link rel="alternate" type="text/html" href="https://eqlwiki.com/Special:Contributions/Maintenance_script"/>
	<updated>2026-09-02T15:01:00Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.45.3</generator>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168748</id>
		<title>MediaWiki:BlueprintLoader.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168748"/>
		<updated>2026-08-10T08:27:19Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Refresh Icon Finder static-cache client&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Editor Tools / Dynamic Blueprint Loader&lt;br /&gt;
 *&lt;br /&gt;
 * Source-editor behavior:&lt;br /&gt;
 * - On a new, truly empty page: show the Help:Contents Blueprint selector.&lt;br /&gt;
 * - On every source edit page: show the Icon Finder button.&lt;br /&gt;
 * - Heavy Icon Finder logic is lazy-loaded only when opened.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var BLUEPRINT_PAGE = &#039;Help:Contents&#039;;&lt;br /&gt;
	var TOOLS_ID = &#039;eql-editor-tools&#039;;&lt;br /&gt;
	var ICON_FINDER_PAGE = &#039;MediaWiki:IconFinder.js&#039;;&lt;br /&gt;
&lt;br /&gt;
	function htmlToText( html ) {&lt;br /&gt;
		var div = document.createElement( &#039;div&#039; );&lt;br /&gt;
		div.innerHTML = String( html || &#039;&#039; );&lt;br /&gt;
		return ( div.textContent || div.innerText || &#039;&#039; ).trim();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeLabel( heading ) {&lt;br /&gt;
		return heading&lt;br /&gt;
			.replace( /\s+Blueprints?\s*$/i, &#039;&#039; )&lt;br /&gt;
			.trim() + &#039; Blueprint&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getEditorContents( $textarea ) {&lt;br /&gt;
		try {&lt;br /&gt;
			return String( $textarea.textSelection( &#039;getContents&#039; ) || &#039;&#039; );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			return String( $textarea.val() || &#039;&#039; );&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setEditorContents( $textarea, text ) {&lt;br /&gt;
		try {&lt;br /&gt;
			$textarea.textSelection( &#039;setContents&#039;, text );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			$textarea.val( text );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea.trigger( &#039;input&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;change&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;focus&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function extractBlueprint( sectionText ) {&lt;br /&gt;
		var match;&lt;br /&gt;
		var blueprint;&lt;br /&gt;
&lt;br /&gt;
		sectionText = String( sectionText || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		match = sectionText.match(&lt;br /&gt;
			/&amp;lt;pre(?:\s[^&amp;gt;]*)?&amp;gt;([\s\S]*?)&amp;lt;\/pre&amp;gt;/i&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		if ( !match ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		blueprint = match[ 1 ];&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint.replace(&lt;br /&gt;
			/^\s*&amp;lt;nowiki&amp;gt;([\s\S]*?)&amp;lt;\/nowiki&amp;gt;\s*$/i,&lt;br /&gt;
			&#039;$1&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint&lt;br /&gt;
			.replace( /^\s*\n/, &#039;&#039; )&lt;br /&gt;
			.replace( /\s+$/, &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		return blueprint + &#039;\n&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintSections( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;tocdata&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var tocdata =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse.tocdata;&lt;br /&gt;
&lt;br /&gt;
			var sections =&lt;br /&gt;
				tocdata &amp;amp;&amp;amp;&lt;br /&gt;
				Array.isArray( tocdata.sections )&lt;br /&gt;
					? tocdata.sections&lt;br /&gt;
					: [];&lt;br /&gt;
&lt;br /&gt;
			if ( sections.length ) {&lt;br /&gt;
				return sections;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return api.get( {&lt;br /&gt;
				action: &#039;parse&#039;,&lt;br /&gt;
				page: BLUEPRINT_PAGE,&lt;br /&gt;
				prop: &#039;sections&#039;,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			} ).then( function ( fallbackData ) {&lt;br /&gt;
				return (&lt;br /&gt;
					fallbackData &amp;amp;&amp;amp;&lt;br /&gt;
					fallbackData.parse &amp;amp;&amp;amp;&lt;br /&gt;
					Array.isArray( fallbackData.parse.sections )&lt;br /&gt;
				)&lt;br /&gt;
					? fallbackData.parse.sections&lt;br /&gt;
					: [];&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeBlueprintSections( sections ) {&lt;br /&gt;
		return sections&lt;br /&gt;
			.map( function ( section ) {&lt;br /&gt;
				var heading = htmlToText( section.line || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
				return {&lt;br /&gt;
					heading: heading,&lt;br /&gt;
					label: makeLabel( heading ),&lt;br /&gt;
					index: String( section.index || &#039;&#039; ),&lt;br /&gt;
					anchor:&lt;br /&gt;
						section.linkAnchor ||&lt;br /&gt;
						section.anchor ||&lt;br /&gt;
						&#039;&#039;&lt;br /&gt;
				};&lt;br /&gt;
			} )&lt;br /&gt;
			.filter( function ( section ) {&lt;br /&gt;
				return (&lt;br /&gt;
					section.index &amp;amp;&amp;amp;&lt;br /&gt;
					/\bBlueprints?\s*$/i.test( section.heading )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintWikitext( api, sectionIndex ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;wikitext&#039;,&lt;br /&gt;
			section: sectionIndex,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var wikitext =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse&lt;br /&gt;
					? data.parse.wikitext&lt;br /&gt;
					: &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				wikitext &amp;amp;&amp;amp;&lt;br /&gt;
				typeof wikitext === &#039;object&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				Object.prototype.hasOwnProperty.call( wikitext, &#039;*&#039; )&lt;br /&gt;
			) {&lt;br /&gt;
				wikitext = wikitext[ &#039;*&#039; ];&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return String( wikitext || &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-editor-tools-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-editor-tools-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039;{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;margin:0 0 1rem 0;&#039; +&lt;br /&gt;
					&#039;padding:1rem 1.1rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,rgba(16,21,29,.97),rgba(9,13,19,.98));&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.38);&#039; +&lt;br /&gt;
					&#039;border-left:5px solid rgba(216,183,92,.82);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 9px 24px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.035);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(0,1fr) auto;&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:end;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-title,&#039; +&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .35rem 0;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;letter-spacing:.035em;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-description{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .7rem 0;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-controls{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;min-width:260px;&#039; +&lt;br /&gt;
					&#039;max-width:100%;&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .55rem;&#039; +&lt;br /&gt;
					&#039;background:#0b1017;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.36);&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#e2e2cf;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button{&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .85rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,#806d32,#594817);&#039; +&lt;br /&gt;
					&#039;border:1px solid #b69c4b;&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#fff5cc;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;cursor:pointer;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:hover:not(:disabled){&#039; +&lt;br /&gt;
					&#039;filter:brightness(1.08);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:disabled{&#039; +&lt;br /&gt;
					&#039;cursor:default;&#039; +&lt;br /&gt;
					&#039;opacity:.55;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-source{&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-status{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-top:.55rem;&#039; +&lt;br /&gt;
					&#039;min-height:1.2em;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:stretch;&#039; +&lt;br /&gt;
					&#039;justify-content:flex-end;&#039; +&lt;br /&gt;
					&#039;min-width:150px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-toggle{&#039; +&lt;br /&gt;
					&#039;white-space:nowrap;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host{&#039; +&lt;br /&gt;
					&#039;margin-top:1rem;&#039; +&lt;br /&gt;
					&#039;padding-top:1rem;&#039; +&lt;br /&gt;
					&#039;border-top:1px solid rgba(216,183,92,.22);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host[hidden]{display:none!important;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:760px){&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
						&#039;grid-template-columns:1fr;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
						&#039;width:100%;&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function loadIconFinderScript() {&lt;br /&gt;
		if ( window.EQLIconFinder ) {&lt;br /&gt;
			return Promise.resolve( window.EQLIconFinder );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( window.eqlIconFinderLoadingPromise ) {&lt;br /&gt;
			return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.eqlIconFinderLoadingPromise = new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var script = document.createElement( &#039;script&#039; );&lt;br /&gt;
&lt;br /&gt;
			script.src = mw.util.getUrl( ICON_FINDER_PAGE, {&lt;br /&gt;
				action: &#039;raw&#039;,&lt;br /&gt;
				ctype: &#039;text/javascript&#039;,&lt;br /&gt;
				v: &#039;9&#039;&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			script.async = true;&lt;br /&gt;
&lt;br /&gt;
			script.onload = function () {&lt;br /&gt;
				if ( window.EQLIconFinder ) {&lt;br /&gt;
					resolve( window.EQLIconFinder );&lt;br /&gt;
				} else {&lt;br /&gt;
					reject( new Error( &#039;IconFinder.js loaded without exposing EQLIconFinder.&#039; ) );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			script.onerror = function () {&lt;br /&gt;
				reject( new Error( &#039;Could not load IconFinder.js.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			document.head.appendChild( script );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildIconFinderLaunch( $row, $tools ) {&lt;br /&gt;
		var $launch = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-launch&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-title&#039;,&lt;br /&gt;
			text: &#039;ICON FINDER&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-toggle&#039;,&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Icon Finder&#039;,&lt;br /&gt;
			&#039;aria-expanded&#039;: &#039;false&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $host = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-host&#039;,&lt;br /&gt;
			hidden: true&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var opening = $host.prop( &#039;hidden&#039; );&lt;br /&gt;
&lt;br /&gt;
			if ( !opening ) {&lt;br /&gt;
				$host.prop( &#039;hidden&#039;, true );&lt;br /&gt;
				$button.attr( &#039;aria-expanded&#039;, &#039;false&#039; );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$button.text( &#039;Loading…&#039; );&lt;br /&gt;
&lt;br /&gt;
			loadIconFinderScript()&lt;br /&gt;
				.then( function ( finder ) {&lt;br /&gt;
					return finder.init( $host[ 0 ] );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$host.prop( &#039;hidden&#039;, false );&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$host&lt;br /&gt;
						.prop( &#039;hidden&#039;, false )&lt;br /&gt;
						.text( &#039;Icon Finder could not be loaded.&#039; );&lt;br /&gt;
&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.text( &#039;Icon Finder&#039; );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildBlueprintArea( $row, $textarea, api ) {&lt;br /&gt;
		var $area = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-area&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-title&#039;,&lt;br /&gt;
			text: &#039;CREATE FROM A BLUEPRINT&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-description&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;Load a standard page skeleton from Help:Contents, &#039; +&lt;br /&gt;
				&#039;or continue editing the current source below.&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $controls = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-controls&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $select = $( &#039;&amp;lt;select&amp;gt;&#039;, {&lt;br /&gt;
			&#039;aria-label&#039;: &#039;Select page blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
			value: &#039;&#039;,&lt;br /&gt;
			text: &#039;Loading blueprints...&#039;&lt;br /&gt;
		} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Load Blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $sourceLink = $( &#039;&amp;lt;a&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-source&#039;,&lt;br /&gt;
			href: mw.util.getUrl( BLUEPRINT_PAGE ),&lt;br /&gt;
			text: &#039;View Blueprints&#039;,&lt;br /&gt;
			target: &#039;_blank&#039;,&lt;br /&gt;
			rel: &#039;noopener&#039;&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $status = $( &#039;&amp;lt;span&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-status&#039;,&lt;br /&gt;
			&#039;aria-live&#039;: &#039;polite&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		fetchBlueprintSections( api )&lt;br /&gt;
			.then( function ( sections ) {&lt;br /&gt;
				var blueprints = normalizeBlueprintSections( sections );&lt;br /&gt;
&lt;br /&gt;
				$select.empty();&lt;br /&gt;
&lt;br /&gt;
				$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
					value: &#039;&#039;,&lt;br /&gt;
					text: blueprints.length ?&lt;br /&gt;
						&#039;Select a blueprint...&#039; :&lt;br /&gt;
						&#039;No blueprints found&#039;&lt;br /&gt;
				} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
				blueprints.forEach( function ( blueprint ) {&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: blueprint.index,&lt;br /&gt;
						text: blueprint.label&lt;br /&gt;
					} )&lt;br /&gt;
						.attr( &#039;data-anchor&#039;, blueprint.anchor )&lt;br /&gt;
						.appendTo( $select );&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
				$select.prop( &#039;disabled&#039;, !blueprints.length );&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				$select.empty().append(&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: &#039;&#039;,&lt;br /&gt;
						text: &#039;Could not load blueprints&#039;&lt;br /&gt;
					} )&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				$status.text(&lt;br /&gt;
					&#039;Could not read Blueprint headings from Help:Contents.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
		$select.on( &#039;change&#039;, function () {&lt;br /&gt;
			var $option = $select.find( &#039;option:selected&#039; );&lt;br /&gt;
			var anchor = $option.attr( &#039;data-anchor&#039; ) || &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
&lt;br /&gt;
			if ( anchor ) {&lt;br /&gt;
				$sourceLink.attr(&lt;br /&gt;
					&#039;href&#039;,&lt;br /&gt;
					mw.util.getUrl( BLUEPRINT_PAGE ) + &#039;#&#039; + anchor&lt;br /&gt;
				);&lt;br /&gt;
				$sourceLink.text( &#039;View This Blueprint&#039; );&lt;br /&gt;
			} else {&lt;br /&gt;
				$sourceLink.attr( &#039;href&#039;, mw.util.getUrl( BLUEPRINT_PAGE ) );&lt;br /&gt;
				$sourceLink.text( &#039;View Blueprints&#039; );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$status.text( &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var sectionIndex = $select.val();&lt;br /&gt;
			var existing;&lt;br /&gt;
&lt;br /&gt;
			if ( !sectionIndex ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			existing = getEditorContents( $textarea );&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				existing.trim() &amp;amp;&amp;amp;&lt;br /&gt;
				!window.confirm(&lt;br /&gt;
					&#039;The editor already contains text. Replace it with the selected blueprint?&#039;&lt;br /&gt;
				)&lt;br /&gt;
			) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$select.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$status.text( &#039;Loading current blueprint from Help:Contents...&#039; );&lt;br /&gt;
&lt;br /&gt;
			fetchBlueprintWikitext( api, sectionIndex )&lt;br /&gt;
				.then( function ( sectionText ) {&lt;br /&gt;
					var blueprint = extractBlueprint( sectionText );&lt;br /&gt;
&lt;br /&gt;
					if ( blueprint === null ) {&lt;br /&gt;
						throw new Error( &#039;No &amp;lt;pre&amp;gt; block found.&#039; );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					setEditorContents( $textarea, blueprint );&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Blueprint loaded. Edit the fields below, then publish normally.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Could not load this blueprint. Check Help:Contents and make sure the section contains a &amp;lt;pre&amp;gt; block.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$select.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function init() {&lt;br /&gt;
		var action = mw.config.get( &#039;wgAction&#039; );&lt;br /&gt;
		var $textarea;&lt;br /&gt;
		var $tools;&lt;br /&gt;
		var $row;&lt;br /&gt;
		var $editorHost;&lt;br /&gt;
		var api;&lt;br /&gt;
&lt;br /&gt;
		if ( [ &#039;edit&#039;, &#039;submit&#039; ].indexOf( action ) === -1 ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea = $( &#039;#wpTextbox1&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$textarea.length ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( document.getElementById( TOOLS_ID ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		addStyles();&lt;br /&gt;
&lt;br /&gt;
		$tools = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: TOOLS_ID&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$row = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-editor-tools-row&#039;&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		buildBlueprintArea( $row, $textarea, api );&lt;br /&gt;
&lt;br /&gt;
		buildIconFinderLaunch( $row, $tools );&lt;br /&gt;
&lt;br /&gt;
		$editorHost = $textarea.closest( &#039;.wikiEditor-ui&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$editorHost.length ) {&lt;br /&gt;
			$editorHost = $textarea;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$tools.insertBefore( $editorHost );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	mw.loader.using( [&lt;br /&gt;
		&#039;mediawiki.api&#039;,&lt;br /&gt;
		&#039;mediawiki.util&#039;,&lt;br /&gt;
		&#039;jquery.textSelection&#039;&lt;br /&gt;
	] ).then( function () {&lt;br /&gt;
		$( init );&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168747</id>
		<title>MediaWiki:IconFinder.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168747"/>
		<updated>2026-08-10T08:27:19Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Use static server cache for Icon Finder&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Icon Finder&lt;br /&gt;
 *&lt;br /&gt;
 * Lazy-loaded from MediaWiki:BlueprintLoader.js.&lt;br /&gt;
 *&lt;br /&gt;
 * Matching workflow:&lt;br /&gt;
 * 1. User pastes a screen capture containing JUST the icon.&lt;br /&gt;
 * 2. Read the current image list from [[Icon List]].&lt;br /&gt;
 * 3. Resolve only existing uploaded files through imageinfo.&lt;br /&gt;
 * 4. Build compact perceptual fingerprints in the browser.&lt;br /&gt;
 * 5. Cache that fingerprint index in IndexedDB keyed to the Icon List revision.&lt;br /&gt;
 * 6. Rank possible matches by perceptual/pixel similarity.&lt;br /&gt;
 *&lt;br /&gt;
 * No icon-catalog requests or image downloads happen until the user actually&lt;br /&gt;
 * pastes an image into the Icon Finder.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var ICON_LIST_PAGE = &#039;Icon List&#039;;&lt;br /&gt;
	var DB_NAME = &#039;eql-icon-finder&#039;;&lt;br /&gt;
	var DB_VERSION = 1;&lt;br /&gt;
	var STORE_NAME = &#039;indexes&#039;;&lt;br /&gt;
	var ALGO_VERSION = 6;&lt;br /&gt;
	var COLOR_GRID = 16;&lt;br /&gt;
	var IMAGEINFO_BATCH = 50;&lt;br /&gt;
	var DOWNLOAD_CONCURRENCY = 8;&lt;br /&gt;
	var RESULT_COUNT = 10;&lt;br /&gt;
&lt;br /&gt;
	var initializedHost = null;&lt;br /&gt;
	var memoryIndex = null;&lt;br /&gt;
	var currentPreviewUrl = null;&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-icon-finder-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;.eql-iconfinder-shell{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(230px,.62fr) minmax(0,1.38fr);&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:start;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste{&#039; +&lt;br /&gt;
					&#039;min-height:160px;&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;justify-content:center;&#039; +&lt;br /&gt;
					&#039;gap:.45rem;&#039; +&lt;br /&gt;
					&#039;padding:1rem;&#039; +&lt;br /&gt;
					&#039;background:#080d13;&#039; +&lt;br /&gt;
					&#039;border:2px dashed rgba(216,183,92,.45);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;color:#dfe7ef;&#039; +&lt;br /&gt;
					&#039;text-align:center;&#039; +&lt;br /&gt;
					&#039;cursor:paste;&#039; +&lt;br /&gt;
					&#039;outline:none;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste:focus{&#039; +&lt;br /&gt;
					&#039;border-color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 0 0 2px rgba(216,183,92,.13);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-instruction{&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-subinstruction{&#039; +&lt;br /&gt;
					&#039;color:#9faaba;&#039; +&lt;br /&gt;
					&#039;font-size:.88rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview{&#039; +&lt;br /&gt;
					&#039;display:none;&#039; +&lt;br /&gt;
					&#039;width:72px;&#039; +&lt;br /&gt;
					&#039;height:72px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;image-rendering:auto;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.14);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview.is-visible{display:block;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-status{&#039; +&lt;br /&gt;
					&#039;margin-top:.65rem;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.4;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .55rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-empty{&#039; +&lt;br /&gt;
					&#039;padding:.9rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.025);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.08);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
					&#039;color:#96a2b2;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-grid{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:54px minmax(0,1fr);&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;padding:.65rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.03);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.09);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result img{&#039; +&lt;br /&gt;
					&#039;width:52px;&#039; +&lt;br /&gt;
					&#039;height:52px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.1);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-name{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.18rem;&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.2;&#039; +&lt;br /&gt;
					&#039;word-break:break-word;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-score{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.28rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-size:.8rem;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-meta{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;color:#8f9bac;&#039; +&lt;br /&gt;
					&#039;font-size:.76rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.25;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-id{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin:.2rem 0 .35rem;&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:800;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy-row{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;gap:.3rem;&#039; +&lt;br /&gt;
					&#039;margin-top:.38rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy{&#039; +&lt;br /&gt;
					&#039;min-height:1.8rem!important;&#039; +&lt;br /&gt;
					&#039;padding:.15rem .45rem!important;&#039; +&lt;br /&gt;
					&#039;font-size:.72rem!important;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-note{&#039; +&lt;br /&gt;
					&#039;margin-top:.7rem;&#039; +&lt;br /&gt;
					&#039;color:#7f8b9b;&#039; +&lt;br /&gt;
					&#039;font-size:.78rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:850px){&#039; +&lt;br /&gt;
					&#039;.eql-iconfinder-shell{grid-template-columns:1fr;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function openDb() {&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var request;&lt;br /&gt;
&lt;br /&gt;
			if ( !window.indexedDB ) {&lt;br /&gt;
				reject( new Error( &#039;IndexedDB unavailable.&#039; ) );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			request = indexedDB.open( DB_NAME, DB_VERSION );&lt;br /&gt;
&lt;br /&gt;
			request.onupgradeneeded = function () {&lt;br /&gt;
				var db = request.result;&lt;br /&gt;
&lt;br /&gt;
				if ( !db.objectStoreNames.contains( STORE_NAME ) ) {&lt;br /&gt;
					db.createObjectStore( STORE_NAME, {&lt;br /&gt;
						keyPath: &#039;key&#039;&lt;br /&gt;
					} );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onsuccess = function () {&lt;br /&gt;
				resolve( request.result );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onerror = function () {&lt;br /&gt;
				reject( request.error || new Error( &#039;IndexedDB open failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbGet( key ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readonly&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var request = store.get( key );&lt;br /&gt;
&lt;br /&gt;
				request.onsuccess = function () {&lt;br /&gt;
					resolve( request.result || null );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				request.onerror = function () {&lt;br /&gt;
					reject( request.error || new Error( &#039;IndexedDB read failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbPutAndPrune( value ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readwrite&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var cursorRequest;&lt;br /&gt;
&lt;br /&gt;
				store.put( value );&lt;br /&gt;
&lt;br /&gt;
				cursorRequest = store.openCursor();&lt;br /&gt;
&lt;br /&gt;
				cursorRequest.onsuccess = function () {&lt;br /&gt;
					var cursor = cursorRequest.result;&lt;br /&gt;
&lt;br /&gt;
					if ( !cursor ) {&lt;br /&gt;
						return;&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					if ( cursor.key !== value.key ) {&lt;br /&gt;
						cursor.delete();&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					cursor.continue();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					resolve();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.onerror = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					reject( tx.error || new Error( &#039;IndexedDB write failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeFileTitle( title ) {&lt;br /&gt;
		title = String( title || &#039;&#039; ).trim();&lt;br /&gt;
&lt;br /&gt;
		if ( !title ) {&lt;br /&gt;
			return &#039;&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return /^File:/i.test( title )&lt;br /&gt;
			? title&lt;br /&gt;
			: &#039;File:&#039; + title;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchCatalog( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: ICON_LIST_PAGE,&lt;br /&gt;
			prop: &#039;images|revid&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var parse = data &amp;amp;&amp;amp; data.parse ? data.parse : {};&lt;br /&gt;
			var titles = Array.isArray( parse.images ) ? parse.images : [];&lt;br /&gt;
&lt;br /&gt;
			titles = titles&lt;br /&gt;
				.map( normalizeFileTitle )&lt;br /&gt;
				.filter( Boolean )&lt;br /&gt;
				.filter( function ( title, index, list ) {&lt;br /&gt;
					return list.indexOf( title ) === index;&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				revid: Number( parse.revid ) || 0,&lt;br /&gt;
				titles: titles&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function chunk( list, size ) {&lt;br /&gt;
		var out = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; list.length; i += size ) {&lt;br /&gt;
			out.push( list.slice( i, i + size ) );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchImageInfo( api, titles, setStatus ) {&lt;br /&gt;
		var batches = chunk( titles, IMAGEINFO_BATCH );&lt;br /&gt;
		var records = [];&lt;br /&gt;
		var chain = Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
		batches.forEach( function ( batch, batchIndex ) {&lt;br /&gt;
			chain = chain.then( function () {&lt;br /&gt;
				if ( setStatus ) {&lt;br /&gt;
					setStatus(&lt;br /&gt;
						&#039;Reading uploaded icon files… &#039; +&lt;br /&gt;
						Math.min( batchIndex * IMAGEINFO_BATCH, titles.length ) +&lt;br /&gt;
						&#039; / &#039; + titles.length&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return api.get( {&lt;br /&gt;
					action: &#039;query&#039;,&lt;br /&gt;
					prop: &#039;imageinfo&#039;,&lt;br /&gt;
					titles: batch.join( &#039;|&#039; ),&lt;br /&gt;
					iiprop: &#039;url|size|sha1&#039;,&lt;br /&gt;
					formatversion: 2&lt;br /&gt;
				} ).then( function ( data ) {&lt;br /&gt;
					var pages =&lt;br /&gt;
						data &amp;amp;&amp;amp;&lt;br /&gt;
						data.query &amp;amp;&amp;amp;&lt;br /&gt;
						Array.isArray( data.query.pages )&lt;br /&gt;
							? data.query.pages&lt;br /&gt;
							: [];&lt;br /&gt;
&lt;br /&gt;
					pages.forEach( function ( page ) {&lt;br /&gt;
						var info =&lt;br /&gt;
							page &amp;amp;&amp;amp;&lt;br /&gt;
							Array.isArray( page.imageinfo ) &amp;amp;&amp;amp;&lt;br /&gt;
							page.imageinfo.length&lt;br /&gt;
								? page.imageinfo[ 0 ]&lt;br /&gt;
								: null;&lt;br /&gt;
&lt;br /&gt;
						if ( !info || !info.url ) {&lt;br /&gt;
							return;&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						records.push( {&lt;br /&gt;
							title: normalizeFileTitle( page.title ),&lt;br /&gt;
							url: String( info.url ),&lt;br /&gt;
							sha1: String( info.sha1 || &#039;&#039; ),&lt;br /&gt;
							width: Number( info.width ) || 0,&lt;br /&gt;
							height: Number( info.height ) || 0&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return chain.then( function () {&lt;br /&gt;
			return records;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function blobToDrawable( blob ) {&lt;br /&gt;
		if ( window.createImageBitmap ) {&lt;br /&gt;
			return createImageBitmap( blob );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var url = URL.createObjectURL( blob );&lt;br /&gt;
			var img = new Image();&lt;br /&gt;
&lt;br /&gt;
			img.onload = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				resolve( img );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.onerror = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				reject( new Error( &#039;Image decode failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.src = url;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchDrawable( url ) {&lt;br /&gt;
		return fetch( url, {&lt;br /&gt;
			credentials: &#039;same-origin&#039;,&lt;br /&gt;
			cache: &#039;force-cache&#039;&lt;br /&gt;
		} )&lt;br /&gt;
			.then( function ( response ) {&lt;br /&gt;
				if ( !response.ok ) {&lt;br /&gt;
					throw new Error( &#039;HTTP &#039; + response.status );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return response.blob();&lt;br /&gt;
			} )&lt;br /&gt;
			.then( blobToDrawable );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function closeDrawable( drawable ) {&lt;br /&gt;
		if ( drawable &amp;amp;&amp;amp; typeof drawable.close === &#039;function&#039; ) {&lt;br /&gt;
			drawable.close();&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function drawScaled( drawable, width, height, cropFraction ) {&lt;br /&gt;
		var sourceCanvas = document.createElement( &#039;canvas&#039; );&lt;br /&gt;
		var sourceWidth =&lt;br /&gt;
			Number( drawable.width || drawable.naturalWidth ) || 1;&lt;br /&gt;
		var sourceHeight =&lt;br /&gt;
			Number( drawable.height || drawable.naturalHeight ) || 1;&lt;br /&gt;
		var sourceCtx;&lt;br /&gt;
		var sourceImage;&lt;br /&gt;
		var sourceData;&lt;br /&gt;
		var pixelCount = sourceWidth * sourceHeight;&lt;br /&gt;
		var transparentCount = 0;&lt;br /&gt;
		var mask = new Uint8Array( pixelCount );&lt;br /&gt;
		var foregroundCount = 0;&lt;br /&gt;
		var borderR = [];&lt;br /&gt;
		var borderG = [];&lt;br /&gt;
		var borderB = [];&lt;br /&gt;
		var bgR = 0;&lt;br /&gt;
		var bgG = 0;&lt;br /&gt;
		var bgB = 0;&lt;br /&gt;
		var bgLuma = 0;&lt;br /&gt;
		var borderSpread = [];&lt;br /&gt;
		var tolerance;&lt;br /&gt;
		var darkCutoff;&lt;br /&gt;
		var minX = sourceWidth;&lt;br /&gt;
		var minY = sourceHeight;&lt;br /&gt;
		var maxX = -1;&lt;br /&gt;
		var maxY = -1;&lt;br /&gt;
		var cleanCanvas;&lt;br /&gt;
		var cleanCtx;&lt;br /&gt;
		var cleanImage;&lt;br /&gt;
		var targetCanvas;&lt;br /&gt;
		var targetCtx;&lt;br /&gt;
		var usableWidth;&lt;br /&gt;
		var usableHeight;&lt;br /&gt;
		var scale;&lt;br /&gt;
		var drawWidth;&lt;br /&gt;
		var drawHeight;&lt;br /&gt;
		var drawX;&lt;br /&gt;
		var drawY;&lt;br /&gt;
		var i;&lt;br /&gt;
		var p;&lt;br /&gt;
		var x;&lt;br /&gt;
		var y;&lt;br /&gt;
		var r;&lt;br /&gt;
		var g;&lt;br /&gt;
		var b;&lt;br /&gt;
		var a;&lt;br /&gt;
		var luma;&lt;br /&gt;
		var chroma;&lt;br /&gt;
		var maxChannel;&lt;br /&gt;
		var minChannel;&lt;br /&gt;
		var colorDistance;&lt;br /&gt;
&lt;br /&gt;
		function median( values ) {&lt;br /&gt;
			var sorted;&lt;br /&gt;
&lt;br /&gt;
			if ( !values.length ) {&lt;br /&gt;
				return 0;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			sorted = values.slice().sort(&lt;br /&gt;
				function ( left, right ) {&lt;br /&gt;
					return left - right;&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			return sorted[&lt;br /&gt;
				Math.floor( sorted.length / 2 )&lt;br /&gt;
			];&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		function addBorderPixel( px, py ) {&lt;br /&gt;
			var index =&lt;br /&gt;
				( py * sourceWidth + px ) * 4;&lt;br /&gt;
&lt;br /&gt;
			if ( sourceData[ index + 3 ] &amp;lt; 16 ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			borderR.push( sourceData[ index ] );&lt;br /&gt;
			borderG.push( sourceData[ index + 1 ] );&lt;br /&gt;
			borderB.push( sourceData[ index + 2 ] );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		sourceCanvas.width = sourceWidth;&lt;br /&gt;
		sourceCanvas.height = sourceHeight;&lt;br /&gt;
&lt;br /&gt;
		sourceCtx = sourceCanvas.getContext(&lt;br /&gt;
			&#039;2d&#039;,&lt;br /&gt;
			{&lt;br /&gt;
				willReadFrequently: true&lt;br /&gt;
			}&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Preserve real PNG transparency.&lt;br /&gt;
		 */&lt;br /&gt;
		sourceCtx.clearRect(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			sourceWidth,&lt;br /&gt;
			sourceHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceCtx.drawImage(&lt;br /&gt;
			drawable,&lt;br /&gt;
			0,&lt;br /&gt;
			0&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceImage = sourceCtx.getImageData(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			sourceWidth,&lt;br /&gt;
			sourceHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceData = sourceImage.data;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; pixelCount; i++ ) {&lt;br /&gt;
			if ( sourceData[ i * 4 + 3 ] &amp;lt; 245 ) {&lt;br /&gt;
				transparentCount++;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			transparentCount &amp;gt;&lt;br /&gt;
			Math.max( 4, pixelCount * 0.01 )&lt;br /&gt;
		) {&lt;br /&gt;
			/*&lt;br /&gt;
			 * Uploaded transparent PNG:&lt;br /&gt;
			 * alpha is already an excellent mask.&lt;br /&gt;
			 */&lt;br /&gt;
			for ( i = 0; i &amp;lt; pixelCount; i++ ) {&lt;br /&gt;
				if (&lt;br /&gt;
					sourceData[ i * 4 + 3 ] &amp;gt; 24&lt;br /&gt;
				) {&lt;br /&gt;
					mask[ i ] = 1;&lt;br /&gt;
					foregroundCount++;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		} else {&lt;br /&gt;
			/*&lt;br /&gt;
			 * Pasted screenshot:&lt;br /&gt;
			 *&lt;br /&gt;
			 * Infer the UI background from a two-pixel&lt;br /&gt;
			 * border around the capture.&lt;br /&gt;
			 */&lt;br /&gt;
			for (&lt;br /&gt;
				x = 0;&lt;br /&gt;
				x &amp;lt; sourceWidth;&lt;br /&gt;
				x++&lt;br /&gt;
			) {&lt;br /&gt;
				for (&lt;br /&gt;
					y = 0;&lt;br /&gt;
					y &amp;lt; Math.min(&lt;br /&gt;
						2,&lt;br /&gt;
						sourceHeight&lt;br /&gt;
					);&lt;br /&gt;
					y++&lt;br /&gt;
				) {&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						sourceHeight - 1 - y&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				y = 0;&lt;br /&gt;
				y &amp;lt; sourceHeight;&lt;br /&gt;
				y++&lt;br /&gt;
			) {&lt;br /&gt;
				for (&lt;br /&gt;
					x = 0;&lt;br /&gt;
					x &amp;lt; Math.min(&lt;br /&gt;
						2,&lt;br /&gt;
						sourceWidth&lt;br /&gt;
					);&lt;br /&gt;
					x++&lt;br /&gt;
				) {&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						sourceWidth - 1 - x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			bgR = median( borderR );&lt;br /&gt;
			bgG = median( borderG );&lt;br /&gt;
			bgB = median( borderB );&lt;br /&gt;
&lt;br /&gt;
			bgLuma = luminance(&lt;br /&gt;
				bgR,&lt;br /&gt;
				bgG,&lt;br /&gt;
				bgB&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				i = 0;&lt;br /&gt;
				i &amp;lt; borderR.length;&lt;br /&gt;
				i++&lt;br /&gt;
			) {&lt;br /&gt;
				borderSpread.push(&lt;br /&gt;
					Math.sqrt(&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderR[ i ] - bgR,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderG[ i ] - bgG,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderB[ i ] - bgB,&lt;br /&gt;
							2&lt;br /&gt;
						)&lt;br /&gt;
					)&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * The old crop variants blindly trimmed the&lt;br /&gt;
			 * image square.&lt;br /&gt;
			 *&lt;br /&gt;
			 * Now they instead make background removal&lt;br /&gt;
			 * slightly more aggressive.&lt;br /&gt;
			 */&lt;br /&gt;
			tolerance = Math.max(&lt;br /&gt;
				20,&lt;br /&gt;
				Math.min(&lt;br /&gt;
					74,&lt;br /&gt;
					18 +&lt;br /&gt;
						median(&lt;br /&gt;
							borderSpread&lt;br /&gt;
						) * 2.5 +&lt;br /&gt;
						cropFraction * 80&lt;br /&gt;
				)&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			darkCutoff = Math.max(&lt;br /&gt;
				36,&lt;br /&gt;
				Math.min(&lt;br /&gt;
					88,&lt;br /&gt;
					bgLuma +&lt;br /&gt;
						22 +&lt;br /&gt;
						cropFraction * 100&lt;br /&gt;
				)&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				i = 0;&lt;br /&gt;
				i &amp;lt; pixelCount;&lt;br /&gt;
				i++&lt;br /&gt;
			) {&lt;br /&gt;
				p = i * 4;&lt;br /&gt;
&lt;br /&gt;
				r = sourceData[ p ];&lt;br /&gt;
				g = sourceData[ p + 1 ];&lt;br /&gt;
				b = sourceData[ p + 2 ];&lt;br /&gt;
				a = sourceData[ p + 3 ];&lt;br /&gt;
&lt;br /&gt;
				if ( a &amp;lt; 16 ) {&lt;br /&gt;
					continue;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				maxChannel = Math.max(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				minChannel = Math.min(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				chroma =&lt;br /&gt;
					maxChannel -&lt;br /&gt;
					minChannel;&lt;br /&gt;
&lt;br /&gt;
				luma = luminance(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				colorDistance =&lt;br /&gt;
					Math.sqrt(&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							r - bgR,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							g - bgG,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							b - bgB,&lt;br /&gt;
							2&lt;br /&gt;
						)&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				/*&lt;br /&gt;
				 * EQ&#039;s editor/game icon background is&lt;br /&gt;
				 * normally very dark.&lt;br /&gt;
				 *&lt;br /&gt;
				 * Discard dark neutral pixels, but allow&lt;br /&gt;
				 * strongly colored pixels to survive at&lt;br /&gt;
				 * lower brightness.&lt;br /&gt;
				 */&lt;br /&gt;
				if ( bgLuma &amp;lt; 95 ) {&lt;br /&gt;
					if (&lt;br /&gt;
						(&lt;br /&gt;
							luma &amp;gt;=&lt;br /&gt;
								darkCutoff &amp;amp;&amp;amp;&lt;br /&gt;
							colorDistance &amp;gt;=&lt;br /&gt;
								tolerance *&lt;br /&gt;
								0.65&lt;br /&gt;
						) ||&lt;br /&gt;
						(&lt;br /&gt;
							chroma &amp;gt;= 24 &amp;amp;&amp;amp;&lt;br /&gt;
							luma &amp;gt;=&lt;br /&gt;
								bgLuma +&lt;br /&gt;
								7 +&lt;br /&gt;
								cropFraction *&lt;br /&gt;
								30 &amp;amp;&amp;amp;&lt;br /&gt;
							colorDistance &amp;gt;=&lt;br /&gt;
								tolerance *&lt;br /&gt;
								0.55&lt;br /&gt;
						)&lt;br /&gt;
					) {&lt;br /&gt;
						mask[ i ] = 1;&lt;br /&gt;
						foregroundCount++;&lt;br /&gt;
					}&lt;br /&gt;
				} else if (&lt;br /&gt;
					colorDistance &amp;gt;=&lt;br /&gt;
					tolerance&lt;br /&gt;
				) {&lt;br /&gt;
					mask[ i ] = 1;&lt;br /&gt;
					foregroundCount++;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * Fail soft for an unusual image instead of&lt;br /&gt;
			 * producing an empty fingerprint.&lt;br /&gt;
			 */&lt;br /&gt;
			if (&lt;br /&gt;
				foregroundCount &amp;lt; 6 ||&lt;br /&gt;
				foregroundCount &amp;gt;&lt;br /&gt;
					pixelCount * 0.90&lt;br /&gt;
			) {&lt;br /&gt;
				mask.fill( 0 );&lt;br /&gt;
				foregroundCount = 0;&lt;br /&gt;
&lt;br /&gt;
				for (&lt;br /&gt;
					i = 0;&lt;br /&gt;
					i &amp;lt; pixelCount;&lt;br /&gt;
					i++&lt;br /&gt;
				) {&lt;br /&gt;
					if (&lt;br /&gt;
						sourceData[&lt;br /&gt;
							i * 4 + 3&lt;br /&gt;
						] &amp;gt; 24&lt;br /&gt;
					) {&lt;br /&gt;
						mask[ i ] = 1;&lt;br /&gt;
						foregroundCount++;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Find the actual sprite bounds.&lt;br /&gt;
		 */&lt;br /&gt;
		for (&lt;br /&gt;
			y = 0;&lt;br /&gt;
			y &amp;lt; sourceHeight;&lt;br /&gt;
			y++&lt;br /&gt;
		) {&lt;br /&gt;
			for (&lt;br /&gt;
				x = 0;&lt;br /&gt;
				x &amp;lt; sourceWidth;&lt;br /&gt;
				x++&lt;br /&gt;
			) {&lt;br /&gt;
				i =&lt;br /&gt;
					y * sourceWidth +&lt;br /&gt;
					x;&lt;br /&gt;
&lt;br /&gt;
				if ( !mask[ i ] ) {&lt;br /&gt;
					continue;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				minX = Math.min(&lt;br /&gt;
					minX,&lt;br /&gt;
					x&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				minY = Math.min(&lt;br /&gt;
					minY,&lt;br /&gt;
					y&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				maxX = Math.max(&lt;br /&gt;
					maxX,&lt;br /&gt;
					x&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				maxY = Math.max(&lt;br /&gt;
					maxY,&lt;br /&gt;
					y&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			maxX &amp;lt; minX ||&lt;br /&gt;
			maxY &amp;lt; minY&lt;br /&gt;
		) {&lt;br /&gt;
			minX = 0;&lt;br /&gt;
			minY = 0;&lt;br /&gt;
			maxX = sourceWidth - 1;&lt;br /&gt;
			maxY = sourceHeight - 1;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Create a transparent copy containing only&lt;br /&gt;
		 * detected foreground pixels.&lt;br /&gt;
		 */&lt;br /&gt;
		cleanCanvas =&lt;br /&gt;
			document.createElement(&lt;br /&gt;
				&#039;canvas&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		cleanCanvas.width =&lt;br /&gt;
			sourceWidth;&lt;br /&gt;
&lt;br /&gt;
		cleanCanvas.height =&lt;br /&gt;
			sourceHeight;&lt;br /&gt;
&lt;br /&gt;
		cleanCtx =&lt;br /&gt;
			cleanCanvas.getContext(&lt;br /&gt;
				&#039;2d&#039;,&lt;br /&gt;
				{&lt;br /&gt;
					willReadFrequently: true&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		cleanImage =&lt;br /&gt;
			cleanCtx.createImageData(&lt;br /&gt;
				sourceWidth,&lt;br /&gt;
				sourceHeight&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i &amp;lt; pixelCount;&lt;br /&gt;
			i++&lt;br /&gt;
		) {&lt;br /&gt;
			if ( !mask[ i ] ) {&lt;br /&gt;
				continue;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			p = i * 4;&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p ] =&lt;br /&gt;
				sourceData[ p ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 1 ] =&lt;br /&gt;
				sourceData[ p + 1 ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 2 ] =&lt;br /&gt;
				sourceData[ p + 2 ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 3 ] =&lt;br /&gt;
				255;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		cleanCtx.putImageData(&lt;br /&gt;
			cleanImage,&lt;br /&gt;
			0,&lt;br /&gt;
			0&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Normalize the detected sprite to the requested&lt;br /&gt;
		 * comparison size.&lt;br /&gt;
		 */&lt;br /&gt;
		targetCanvas =&lt;br /&gt;
			document.createElement(&lt;br /&gt;
				&#039;canvas&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		targetCanvas.width = width;&lt;br /&gt;
		targetCanvas.height = height;&lt;br /&gt;
&lt;br /&gt;
		targetCtx =&lt;br /&gt;
			targetCanvas.getContext(&lt;br /&gt;
				&#039;2d&#039;,&lt;br /&gt;
				{&lt;br /&gt;
					willReadFrequently: true&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.fillStyle = &#039;#000&#039;;&lt;br /&gt;
&lt;br /&gt;
		targetCtx.fillRect(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			width,&lt;br /&gt;
			height&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.imageSmoothingEnabled =&lt;br /&gt;
			true;&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			&#039;imageSmoothingQuality&#039; in&lt;br /&gt;
			targetCtx&lt;br /&gt;
		) {&lt;br /&gt;
			targetCtx.imageSmoothingQuality =&lt;br /&gt;
				&#039;high&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		usableWidth =&lt;br /&gt;
			Math.max(&lt;br /&gt;
				1,&lt;br /&gt;
				width - 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		usableHeight =&lt;br /&gt;
			Math.max(&lt;br /&gt;
				1,&lt;br /&gt;
				height - 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		scale = Math.min(&lt;br /&gt;
			usableWidth /&lt;br /&gt;
				Math.max(&lt;br /&gt;
					1,&lt;br /&gt;
					maxX - minX + 1&lt;br /&gt;
				),&lt;br /&gt;
			usableHeight /&lt;br /&gt;
				Math.max(&lt;br /&gt;
					1,&lt;br /&gt;
					maxY - minY + 1&lt;br /&gt;
				)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawWidth = Math.max(&lt;br /&gt;
			1,&lt;br /&gt;
			Math.round(&lt;br /&gt;
				(&lt;br /&gt;
					maxX -&lt;br /&gt;
					minX +&lt;br /&gt;
					1&lt;br /&gt;
				) * scale&lt;br /&gt;
			)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawHeight = Math.max(&lt;br /&gt;
			1,&lt;br /&gt;
			Math.round(&lt;br /&gt;
				(&lt;br /&gt;
					maxY -&lt;br /&gt;
					minY +&lt;br /&gt;
					1&lt;br /&gt;
				) * scale&lt;br /&gt;
			)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawX = Math.floor(&lt;br /&gt;
			(&lt;br /&gt;
				width -&lt;br /&gt;
				drawWidth&lt;br /&gt;
			) / 2&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawY = Math.floor(&lt;br /&gt;
			(&lt;br /&gt;
				height -&lt;br /&gt;
				drawHeight&lt;br /&gt;
			) / 2&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.drawImage(&lt;br /&gt;
			cleanCanvas,&lt;br /&gt;
			minX,&lt;br /&gt;
			minY,&lt;br /&gt;
			maxX - minX + 1,&lt;br /&gt;
			maxY - minY + 1,&lt;br /&gt;
			drawX,&lt;br /&gt;
			drawY,&lt;br /&gt;
			drawWidth,&lt;br /&gt;
			drawHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		return targetCtx.getImageData(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			width,&lt;br /&gt;
			height&lt;br /&gt;
		);&lt;br /&gt;
	}&lt;br /&gt;
	function luminance( r, g, b ) {&lt;br /&gt;
		return 0.2126 * r + 0.7152 * g + 0.0722 * b;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeDHash( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled( drawable, 9, 8, cropFraction );&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var hi = 0;&lt;br /&gt;
		var lo = 0;&lt;br /&gt;
		var bit = 0;&lt;br /&gt;
		var y;&lt;br /&gt;
		var x;&lt;br /&gt;
		var left;&lt;br /&gt;
		var right;&lt;br /&gt;
		var li;&lt;br /&gt;
		var ri;&lt;br /&gt;
&lt;br /&gt;
		for ( y = 0; y &amp;lt; 8; y++ ) {&lt;br /&gt;
			for ( x = 0; x &amp;lt; 8; x++ ) {&lt;br /&gt;
				li = ( y * 9 + x ) * 4;&lt;br /&gt;
				ri = ( y * 9 + x + 1 ) * 4;&lt;br /&gt;
&lt;br /&gt;
				left = luminance(&lt;br /&gt;
					data[ li ],&lt;br /&gt;
					data[ li + 1 ],&lt;br /&gt;
					data[ li + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				right = luminance(&lt;br /&gt;
					data[ ri ],&lt;br /&gt;
					data[ ri + 1 ],&lt;br /&gt;
					data[ ri + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				if ( left &amp;gt; right ) {&lt;br /&gt;
					if ( bit &amp;lt; 32 ) {&lt;br /&gt;
						lo = ( lo | ( 1 &amp;lt;&amp;lt; bit ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					} else {&lt;br /&gt;
						hi = ( hi | ( 1 &amp;lt;&amp;lt; ( bit - 32 ) ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				bit++;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hi: hi &amp;gt;&amp;gt;&amp;gt; 0,&lt;br /&gt;
			lo: lo &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeColorGrid( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled(&lt;br /&gt;
			drawable,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			cropFraction&lt;br /&gt;
		);&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var out = new Uint8Array( COLOR_GRID * COLOR_GRID * 3 );&lt;br /&gt;
		var source = 0;&lt;br /&gt;
		var target = 0;&lt;br /&gt;
&lt;br /&gt;
		while ( source &amp;lt; data.length ) {&lt;br /&gt;
			out[ target++ ] = data[ source ];&lt;br /&gt;
			out[ target++ ] = data[ source + 1 ];&lt;br /&gt;
			out[ target++ ] = data[ source + 2 ];&lt;br /&gt;
			source += 4;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDrawable( drawable, cropFraction ) {&lt;br /&gt;
		var hash = computeDHash( drawable, cropFraction );&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hashHi: hash.hi,&lt;br /&gt;
			hashLo: hash.lo,&lt;br /&gt;
			rgb: computeColorGrid( drawable, cropFraction )&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function popcount32( value ) {&lt;br /&gt;
		value = value &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
		value = value - ( ( value &amp;gt;&amp;gt;&amp;gt; 1 ) &amp;amp; 0x55555555 );&lt;br /&gt;
		value = ( value &amp;amp; 0x33333333 ) + ( ( value &amp;gt;&amp;gt;&amp;gt; 2 ) &amp;amp; 0x33333333 );&lt;br /&gt;
		return (&lt;br /&gt;
			(&lt;br /&gt;
				(&lt;br /&gt;
					value + ( value &amp;gt;&amp;gt;&amp;gt; 4 )&lt;br /&gt;
				) &amp;amp; 0x0F0F0F0F&lt;br /&gt;
			) * 0x01010101&lt;br /&gt;
		) &amp;gt;&amp;gt;&amp;gt; 24;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDistance( a, b ) {&lt;br /&gt;
		var hashBits =&lt;br /&gt;
			popcount32(&lt;br /&gt;
				(&lt;br /&gt;
					a.hashHi ^&lt;br /&gt;
					b.hashHi&lt;br /&gt;
				) &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
			) +&lt;br /&gt;
			popcount32(&lt;br /&gt;
				(&lt;br /&gt;
					a.hashLo ^&lt;br /&gt;
					b.hashLo&lt;br /&gt;
				) &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		var hashDistance =&lt;br /&gt;
			hashBits / 64;&lt;br /&gt;
&lt;br /&gt;
		var length = Math.min(&lt;br /&gt;
			a.rgb.length,&lt;br /&gt;
			b.rgb.length&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		var histogramA =&lt;br /&gt;
			new Uint16Array( 64 );&lt;br /&gt;
&lt;br /&gt;
		var histogramB =&lt;br /&gt;
			new Uint16Array( 64 );&lt;br /&gt;
&lt;br /&gt;
		var foregroundA = 0;&lt;br /&gt;
		var foregroundB = 0;&lt;br /&gt;
		var intersection = 0;&lt;br /&gt;
		var union = 0;&lt;br /&gt;
		var colorSum = 0;&lt;br /&gt;
		var i;&lt;br /&gt;
		var ar;&lt;br /&gt;
		var ag;&lt;br /&gt;
		var ab;&lt;br /&gt;
		var br;&lt;br /&gt;
		var bg;&lt;br /&gt;
		var bb;&lt;br /&gt;
		var aForeground;&lt;br /&gt;
		var bForeground;&lt;br /&gt;
		var dr;&lt;br /&gt;
		var dg;&lt;br /&gt;
		var db;&lt;br /&gt;
		var binA;&lt;br /&gt;
		var binB;&lt;br /&gt;
		var shapeDistance;&lt;br /&gt;
		var colorDistance;&lt;br /&gt;
		var histogramDistance = 0;&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i + 2 &amp;lt; length;&lt;br /&gt;
			i += 3&lt;br /&gt;
		) {&lt;br /&gt;
			ar = a.rgb[ i ];&lt;br /&gt;
			ag = a.rgb[ i + 1 ];&lt;br /&gt;
			ab = a.rgb[ i + 2 ];&lt;br /&gt;
&lt;br /&gt;
			br = b.rgb[ i ];&lt;br /&gt;
			bg = b.rgb[ i + 1 ];&lt;br /&gt;
			bb = b.rgb[ i + 2 ];&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * Background is normalized to black by&lt;br /&gt;
			 * drawScaled().&lt;br /&gt;
			 */&lt;br /&gt;
			aForeground =&lt;br /&gt;
				Math.max(&lt;br /&gt;
					ar,&lt;br /&gt;
					ag,&lt;br /&gt;
					ab&lt;br /&gt;
				) &amp;gt; 8;&lt;br /&gt;
&lt;br /&gt;
			bForeground =&lt;br /&gt;
				Math.max(&lt;br /&gt;
					br,&lt;br /&gt;
					bg,&lt;br /&gt;
					bb&lt;br /&gt;
				) &amp;gt; 8;&lt;br /&gt;
&lt;br /&gt;
			if ( aForeground ) {&lt;br /&gt;
				foregroundA++;&lt;br /&gt;
&lt;br /&gt;
				binA =&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							ar &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 4&lt;br /&gt;
					) |&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							ag &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 2&lt;br /&gt;
					) |&lt;br /&gt;
					Math.min(&lt;br /&gt;
						3,&lt;br /&gt;
						ab &amp;gt;&amp;gt; 6&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				histogramA[ binA ]++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if ( bForeground ) {&lt;br /&gt;
				foregroundB++;&lt;br /&gt;
&lt;br /&gt;
				binB =&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							br &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 4&lt;br /&gt;
					) |&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							bg &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 2&lt;br /&gt;
					) |&lt;br /&gt;
					Math.min(&lt;br /&gt;
						3,&lt;br /&gt;
						bb &amp;gt;&amp;gt; 6&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				histogramB[ binB ]++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				aForeground ||&lt;br /&gt;
				bForeground&lt;br /&gt;
			) {&lt;br /&gt;
				union++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				!aForeground ||&lt;br /&gt;
				!bForeground&lt;br /&gt;
			) {&lt;br /&gt;
				continue;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			intersection++;&lt;br /&gt;
&lt;br /&gt;
			dr = ar - br;&lt;br /&gt;
			dg = ag - bg;&lt;br /&gt;
			db = ab - bb;&lt;br /&gt;
&lt;br /&gt;
			colorSum +=&lt;br /&gt;
				Math.sqrt(&lt;br /&gt;
					dr * dr +&lt;br /&gt;
					dg * dg +&lt;br /&gt;
					db * db&lt;br /&gt;
				) /&lt;br /&gt;
				441.67295593;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		shapeDistance =&lt;br /&gt;
			union&lt;br /&gt;
				? 1 -&lt;br /&gt;
					intersection /&lt;br /&gt;
						union&lt;br /&gt;
				: 1;&lt;br /&gt;
&lt;br /&gt;
		colorDistance =&lt;br /&gt;
			intersection&lt;br /&gt;
				? colorSum /&lt;br /&gt;
					intersection&lt;br /&gt;
				: 1;&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i &amp;lt; 64;&lt;br /&gt;
			i++&lt;br /&gt;
		) {&lt;br /&gt;
			histogramDistance +=&lt;br /&gt;
				Math.abs(&lt;br /&gt;
					histogramA[ i ] /&lt;br /&gt;
						Math.max(&lt;br /&gt;
							1,&lt;br /&gt;
							foregroundA&lt;br /&gt;
						) -&lt;br /&gt;
					histogramB[ i ] /&lt;br /&gt;
						Math.max(&lt;br /&gt;
							1,&lt;br /&gt;
							foregroundB&lt;br /&gt;
						)&lt;br /&gt;
				);&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Normalized histogram L1 range is 0..2.&lt;br /&gt;
		 */&lt;br /&gt;
		histogramDistance =&lt;br /&gt;
			Math.min(&lt;br /&gt;
				1,&lt;br /&gt;
				histogramDistance / 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Foreground color is deliberately the largest&lt;br /&gt;
		 * contributor now.&lt;br /&gt;
		 */&lt;br /&gt;
		return Math.min(&lt;br /&gt;
			1,&lt;br /&gt;
			0.50 * colorDistance +&lt;br /&gt;
				0.25 * shapeDistance +&lt;br /&gt;
				0.15 * histogramDistance +&lt;br /&gt;
				0.10 * hashDistance&lt;br /&gt;
		);&lt;br /&gt;
	}&lt;br /&gt;
	function mapLimit( list, limit, worker ) {&lt;br /&gt;
		var nextIndex = 0;&lt;br /&gt;
		var workers = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		function runWorker() {&lt;br /&gt;
			var index;&lt;br /&gt;
&lt;br /&gt;
			if ( nextIndex &amp;gt;= list.length ) {&lt;br /&gt;
				return Promise.resolve();&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			index = nextIndex++;&lt;br /&gt;
			return Promise.resolve( worker( list[ index ], index ) )&lt;br /&gt;
				.then( runWorker );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; Math.min( limit, list.length ); i++ ) {&lt;br /&gt;
			workers.push( runWorker() );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return Promise.all( workers );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildFingerprintIndex( records, setStatus ) {&lt;br /&gt;
		var complete = 0;&lt;br /&gt;
		var successful = [];&lt;br /&gt;
		var grouped = Object.create( null );&lt;br /&gt;
		var groups = [];&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * If several icon filenames are byte-identical, imageinfo gives them&lt;br /&gt;
		 * the same SHA-1. Fingerprint that image only once and fan the result&lt;br /&gt;
		 * back out to every filename. This can substantially reduce the first&lt;br /&gt;
		 * build&#039;s static-file traffic without changing the visible results.&lt;br /&gt;
		 */&lt;br /&gt;
		records.forEach( function ( record ) {&lt;br /&gt;
			var key = record.sha1 || record.url;&lt;br /&gt;
&lt;br /&gt;
			if ( !grouped[ key ] ) {&lt;br /&gt;
				grouped[ key ] = {&lt;br /&gt;
					key: key,&lt;br /&gt;
					representative: record,&lt;br /&gt;
					records: []&lt;br /&gt;
				};&lt;br /&gt;
				groups.push( grouped[ key ] );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			grouped[ key ].records.push( record );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return mapLimit(&lt;br /&gt;
			groups,&lt;br /&gt;
			DOWNLOAD_CONCURRENCY,&lt;br /&gt;
			function ( group ) {&lt;br /&gt;
				return fetchDrawable( group.representative.url )&lt;br /&gt;
					.then( function ( drawable ) {&lt;br /&gt;
						var fp = fingerprintDrawable( drawable, 0 );&lt;br /&gt;
&lt;br /&gt;
						closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
						group.records.forEach( function ( record ) {&lt;br /&gt;
							successful.push( {&lt;br /&gt;
								title: record.title,&lt;br /&gt;
								url: record.url,&lt;br /&gt;
								sha1: record.sha1,&lt;br /&gt;
								width: record.width,&lt;br /&gt;
								height: record.height,&lt;br /&gt;
								hashHi: fp.hashHi,&lt;br /&gt;
								hashLo: fp.hashLo,&lt;br /&gt;
								rgb: fp.rgb&lt;br /&gt;
							} );&lt;br /&gt;
						} );&lt;br /&gt;
					} )&lt;br /&gt;
					.catch( function () {&lt;br /&gt;
						/*&lt;br /&gt;
						 * A missing/corrupt/non-raster file should not prevent&lt;br /&gt;
						 * the rest of the icon index from becoming usable.&lt;br /&gt;
						 */&lt;br /&gt;
					} )&lt;br /&gt;
					.then( function () {&lt;br /&gt;
						complete++;&lt;br /&gt;
&lt;br /&gt;
						if (&lt;br /&gt;
							setStatus &amp;amp;&amp;amp;&lt;br /&gt;
							(&lt;br /&gt;
								complete === groups.length ||&lt;br /&gt;
								complete % 25 === 0&lt;br /&gt;
							)&lt;br /&gt;
						) {&lt;br /&gt;
							setStatus(&lt;br /&gt;
								&#039;Building pixel index… &#039; +&lt;br /&gt;
									complete + &#039; / &#039; + groups.length +&lt;br /&gt;
									&#039; unique images (&#039; +&lt;br /&gt;
									records.length + &#039; filenames)&#039;&lt;br /&gt;
							);&lt;br /&gt;
						}&lt;br /&gt;
					} );&lt;br /&gt;
			}&lt;br /&gt;
		).then( function () {&lt;br /&gt;
			return successful;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cacheKey( revid ) {&lt;br /&gt;
		return [&lt;br /&gt;
			&#039;icon-list&#039;,&lt;br /&gt;
			ALGO_VERSION,&lt;br /&gt;
			revid&lt;br /&gt;
		].join( &#039;:&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function decodeServerRgb( base64 ) {&lt;br /&gt;
		var binary = window.atob( String( base64 || &#039;&#039; ) );&lt;br /&gt;
		var bytes = new Uint8Array( binary.length );&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; binary.length; i++ ) {&lt;br /&gt;
			bytes[ i ] = binary.charCodeAt( i ) &amp;amp; 0xFF;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return bytes;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function decodeServerRecords( records ) {&lt;br /&gt;
		return ( Array.isArray( records ) ? records : [] )&lt;br /&gt;
			.map( function ( record ) {&lt;br /&gt;
				return {&lt;br /&gt;
					title: String( record.title || &#039;&#039; ),&lt;br /&gt;
					url: String( record.url || &#039;&#039; ),&lt;br /&gt;
					sha1: String( record.sha1 || &#039;&#039; ),&lt;br /&gt;
					width: Number( record.width ) || 0,&lt;br /&gt;
					height: Number( record.height ) || 0,&lt;br /&gt;
					hashHi: Number( record.hashHi ) &amp;gt;&amp;gt;&amp;gt; 0,&lt;br /&gt;
					hashLo: Number( record.hashLo ) &amp;gt;&amp;gt;&amp;gt; 0,&lt;br /&gt;
					rgb: decodeServerRgb( record.rgb )&lt;br /&gt;
				};&lt;br /&gt;
			} )&lt;br /&gt;
			.filter( function ( record ) {&lt;br /&gt;
				return (&lt;br /&gt;
					record.title &amp;amp;&amp;amp;&lt;br /&gt;
					record.url &amp;amp;&amp;amp;&lt;br /&gt;
					record.rgb.length&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchServerIconIndexMeta( api ) {&lt;br /&gt;
		return window.fetch(&lt;br /&gt;
			&#039;/static/eql-icon-index/meta.json?ts=&#039; + Date.now(),&lt;br /&gt;
			{&lt;br /&gt;
				credentials: &#039;same-origin&#039;,&lt;br /&gt;
				cache: &#039;no-store&#039;&lt;br /&gt;
			}&lt;br /&gt;
		).then( function ( response ) {&lt;br /&gt;
			if ( !response.ok ) {&lt;br /&gt;
				return null;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return response.json();&lt;br /&gt;
		} ).catch( function () {&lt;br /&gt;
			return null;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchServerIconIndex( api, generation ) {&lt;br /&gt;
		return window.fetch(&lt;br /&gt;
			&#039;/static/eql-icon-index/index-&#039; +&lt;br /&gt;
				encodeURIComponent( String( generation || &#039;&#039; ) ) +&lt;br /&gt;
				&#039;.json&#039;,&lt;br /&gt;
			{&lt;br /&gt;
				credentials: &#039;same-origin&#039;,&lt;br /&gt;
				cache: &#039;force-cache&#039;&lt;br /&gt;
			}&lt;br /&gt;
		).then( function ( response ) {&lt;br /&gt;
			if ( !response.ok ) {&lt;br /&gt;
				return null;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return response.json();&lt;br /&gt;
		} ).catch( function () {&lt;br /&gt;
			return null;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
	function serverCacheKey( generation ) {&lt;br /&gt;
		return [&lt;br /&gt;
			&#039;server&#039;,&lt;br /&gt;
			ALGO_VERSION,&lt;br /&gt;
			String( generation || &#039;&#039; )&lt;br /&gt;
		].join( &#039;:&#039; );&lt;br /&gt;
	}&lt;br /&gt;
	function getOrBuildIndex( api, setStatus ) {&lt;br /&gt;
		if ( memoryIndex ) {&lt;br /&gt;
			return Promise.resolve( memoryIndex );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		setStatus( &#039;Checking server icon index…&#039; );&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Preferred path:&lt;br /&gt;
		 * 1. Ask for tiny server metadata.&lt;br /&gt;
		 * 2. Reuse IndexedDB if this immutable generation is already local.&lt;br /&gt;
		 * 3. Otherwise download the precomputed server fingerprints once.&lt;br /&gt;
		 *&lt;br /&gt;
		 * Any server-cache error falls through to the original fully&lt;br /&gt;
		 * client-side builder, preserving continuity.&lt;br /&gt;
		 */&lt;br /&gt;
		return fetchServerIconIndexMeta( api )&lt;br /&gt;
			.catch( function () {&lt;br /&gt;
				return null;&lt;br /&gt;
			} )&lt;br /&gt;
			.then( function ( meta ) {&lt;br /&gt;
				var key;&lt;br /&gt;
&lt;br /&gt;
				if (&lt;br /&gt;
					!meta ||&lt;br /&gt;
					!meta.ready ||&lt;br /&gt;
					Number( meta.algorithm ) !== ALGO_VERSION ||&lt;br /&gt;
					!meta.generation&lt;br /&gt;
				) {&lt;br /&gt;
					return null;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				key = serverCacheKey( meta.generation );&lt;br /&gt;
&lt;br /&gt;
				return idbGet( key )&lt;br /&gt;
					.catch( function () {&lt;br /&gt;
						return null;&lt;br /&gt;
					} )&lt;br /&gt;
					.then( function ( cached ) {&lt;br /&gt;
						if (&lt;br /&gt;
							cached &amp;amp;&amp;amp;&lt;br /&gt;
							Array.isArray( cached.records ) &amp;amp;&amp;amp;&lt;br /&gt;
							cached.records.length&lt;br /&gt;
						) {&lt;br /&gt;
							memoryIndex = cached.records;&lt;br /&gt;
&lt;br /&gt;
							setStatus(&lt;br /&gt;
								&#039;Icon index ready: &#039; +&lt;br /&gt;
									memoryIndex.length +&lt;br /&gt;
									&#039; icons (server generation cached locally).&#039;&lt;br /&gt;
							);&lt;br /&gt;
&lt;br /&gt;
							return memoryIndex;&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Downloading precomputed server icon index…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						return fetchServerIconIndex(&lt;br /&gt;
							api,&lt;br /&gt;
							String( meta.generation )&lt;br /&gt;
						).then( function ( payload ) {&lt;br /&gt;
							var records;&lt;br /&gt;
&lt;br /&gt;
							if (&lt;br /&gt;
								!payload ||&lt;br /&gt;
								!payload.ready ||&lt;br /&gt;
								String( payload.generation || &#039;&#039; ) !==&lt;br /&gt;
									String( meta.generation )&lt;br /&gt;
							) {&lt;br /&gt;
								return null;&lt;br /&gt;
							}&lt;br /&gt;
&lt;br /&gt;
							records = decodeServerRecords(&lt;br /&gt;
								payload.records&lt;br /&gt;
							);&lt;br /&gt;
&lt;br /&gt;
							if ( !records.length ) {&lt;br /&gt;
								return null;&lt;br /&gt;
							}&lt;br /&gt;
&lt;br /&gt;
							memoryIndex = records;&lt;br /&gt;
&lt;br /&gt;
							setStatus(&lt;br /&gt;
								&#039;Icon index ready: &#039; +&lt;br /&gt;
									records.length +&lt;br /&gt;
									&#039; icons (server cache).&#039;&lt;br /&gt;
							);&lt;br /&gt;
&lt;br /&gt;
							return idbPutAndPrune( {&lt;br /&gt;
								key: key,&lt;br /&gt;
								generation: String( meta.generation ),&lt;br /&gt;
								revid: Number( meta.iconListRevision ) || 0,&lt;br /&gt;
								algorithm: ALGO_VERSION,&lt;br /&gt;
								builtAt: Date.now(),&lt;br /&gt;
								records: records&lt;br /&gt;
							} )&lt;br /&gt;
								.catch( function () {&lt;br /&gt;
									/* Browser storage is optional. */&lt;br /&gt;
								} )&lt;br /&gt;
								.then( function () {&lt;br /&gt;
									return records;&lt;br /&gt;
								} );&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
			} )&lt;br /&gt;
			.then( function ( serverRecords ) {&lt;br /&gt;
				if ( serverRecords &amp;amp;&amp;amp; serverRecords.length ) {&lt;br /&gt;
					return serverRecords;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				/*&lt;br /&gt;
				 * Backwards-compatible fallback. If the extension is disabled,&lt;br /&gt;
				 * the job queue has not run yet, GD is unavailable, or the&lt;br /&gt;
				 * server cache is stale, the working browser implementation&lt;br /&gt;
				 * behaves exactly as it did before.&lt;br /&gt;
				 */&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Server icon index unavailable; checking browser cache…&#039;&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				return fetchCatalog( api ).then( function ( catalog ) {&lt;br /&gt;
					var key = cacheKey( catalog.revid );&lt;br /&gt;
&lt;br /&gt;
					return idbGet( key )&lt;br /&gt;
						.catch( function () {&lt;br /&gt;
							return null;&lt;br /&gt;
						} )&lt;br /&gt;
						.then( function ( cached ) {&lt;br /&gt;
							if (&lt;br /&gt;
								cached &amp;amp;&amp;amp;&lt;br /&gt;
								Array.isArray( cached.records ) &amp;amp;&amp;amp;&lt;br /&gt;
								cached.records.length&lt;br /&gt;
							) {&lt;br /&gt;
								memoryIndex = cached.records;&lt;br /&gt;
								setStatus(&lt;br /&gt;
									&#039;Icon index ready: &#039; +&lt;br /&gt;
										memoryIndex.length +&lt;br /&gt;
										&#039; uploaded icons (cached locally).&#039;&lt;br /&gt;
								);&lt;br /&gt;
								return memoryIndex;&lt;br /&gt;
							}&lt;br /&gt;
&lt;br /&gt;
							return fetchImageInfo(&lt;br /&gt;
								api,&lt;br /&gt;
								catalog.titles,&lt;br /&gt;
								setStatus&lt;br /&gt;
							).then( function ( imageRecords ) {&lt;br /&gt;
								setStatus(&lt;br /&gt;
									&#039;Found &#039; + imageRecords.length +&lt;br /&gt;
										&#039; uploaded icons. Building pixel index…&#039;&lt;br /&gt;
								);&lt;br /&gt;
&lt;br /&gt;
								return buildFingerprintIndex(&lt;br /&gt;
									imageRecords,&lt;br /&gt;
									setStatus&lt;br /&gt;
								);&lt;br /&gt;
							} ).then( function ( records ) {&lt;br /&gt;
								memoryIndex = records;&lt;br /&gt;
&lt;br /&gt;
								setStatus(&lt;br /&gt;
									&#039;Icon index ready: &#039; +&lt;br /&gt;
										records.length +&lt;br /&gt;
										&#039; uploaded icons.&#039;&lt;br /&gt;
								);&lt;br /&gt;
&lt;br /&gt;
								return idbPutAndPrune( {&lt;br /&gt;
									key: key,&lt;br /&gt;
									revid: catalog.revid,&lt;br /&gt;
									algorithm: ALGO_VERSION,&lt;br /&gt;
									builtAt: Date.now(),&lt;br /&gt;
									records: records&lt;br /&gt;
								} )&lt;br /&gt;
									.catch( function () {&lt;br /&gt;
										/* Browser storage is optional. */&lt;br /&gt;
									} )&lt;br /&gt;
									.then( function () {&lt;br /&gt;
										return records;&lt;br /&gt;
									} );&lt;br /&gt;
							} );&lt;br /&gt;
						} );&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
	function fingerprintPastedImage( drawable ) {&lt;br /&gt;
		/*&lt;br /&gt;
		 * Small centered crop variants make the matcher forgiving of a&lt;br /&gt;
		 * 1-2 pixel screen-capture border while still preferring an exact crop.&lt;br /&gt;
		 */&lt;br /&gt;
		return [&lt;br /&gt;
			fingerprintDrawable( drawable, 0 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.035 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.07 )&lt;br /&gt;
		];&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function rankMatches( targetFingerprints, records ) {&lt;br /&gt;
		var ranked = records.map( function ( record ) {&lt;br /&gt;
			var candidate = {&lt;br /&gt;
				hashHi: record.hashHi,&lt;br /&gt;
				hashLo: record.hashLo,&lt;br /&gt;
				rgb: record.rgb&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			var distance = Infinity;&lt;br /&gt;
&lt;br /&gt;
			targetFingerprints.forEach( function ( target ) {&lt;br /&gt;
				distance = Math.min(&lt;br /&gt;
					distance,&lt;br /&gt;
					fingerprintDistance( target, candidate )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				record: record,&lt;br /&gt;
				distance: distance,&lt;br /&gt;
				similarity: Math.max(&lt;br /&gt;
					0,&lt;br /&gt;
					Math.min( 100, ( 1 - distance ) * 100 )&lt;br /&gt;
				)&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		ranked.sort( function ( a, b ) {&lt;br /&gt;
			return a.distance - b.distance;&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return ranked.slice( 0, RESULT_COUNT );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cleanDisplayTitle( title ) {&lt;br /&gt;
		return String( title || &#039;&#039; ).replace( /^File:/i, &#039;&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function iconMetadata( title ) {&lt;br /&gt;
		var clean = cleanDisplayTitle( title );&lt;br /&gt;
		var itemMatch = clean.match( /^Item\s+(\d+)\.png$/i );&lt;br /&gt;
		var spellMatch = clean.match( /^Spellicon_([^.]*)\.png$/i );&lt;br /&gt;
		var id = &#039;&#039;;&lt;br /&gt;
		var kind = &#039;Icon file&#039;;&lt;br /&gt;
		var parameter = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		if ( itemMatch ) {&lt;br /&gt;
			id = itemMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Item icon&#039;;&lt;br /&gt;
			parameter = &#039;|lucy_img_ID = &#039; + id;&lt;br /&gt;
		} else if ( spellMatch ) {&lt;br /&gt;
			id = spellMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Spell icon&#039;;&lt;br /&gt;
			parameter = &#039;|spellicon = &#039; + id;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			kind: kind,&lt;br /&gt;
			id: id,&lt;br /&gt;
			filename: clean,&lt;br /&gt;
			parameter: parameter,&lt;br /&gt;
			rawWiki: &#039;[[File:&#039; + clean + &#039;]]&#039;,&lt;br /&gt;
			staticWiki: &#039;[[File:&#039; + clean + &#039;|32x32px]]&#039;,&lt;br /&gt;
			scaledWiki: &#039;[[File:&#039; + clean + &#039;|frameless|upright=1.5]]&#039;&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function copyText( text, button ) {&lt;br /&gt;
		var original = button.textContent;&lt;br /&gt;
&lt;br /&gt;
		function done() {&lt;br /&gt;
			button.textContent = &#039;Copied&#039;;&lt;br /&gt;
			window.setTimeout( function () {&lt;br /&gt;
				button.textContent = original;&lt;br /&gt;
			}, 1000 );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			navigator.clipboard &amp;amp;&amp;amp;&lt;br /&gt;
			typeof navigator.clipboard.writeText === &#039;function&#039;&lt;br /&gt;
		) {&lt;br /&gt;
			navigator.clipboard.writeText( text )&lt;br /&gt;
				.then( done )&lt;br /&gt;
				.catch( function () {&lt;br /&gt;
					window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
				} );&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeCopyButton( label, value ) {&lt;br /&gt;
		var button = document.createElement( &#039;button&#039; );&lt;br /&gt;
&lt;br /&gt;
		button.className = &#039;eql-iconfinder-copy&#039;;&lt;br /&gt;
		button.type = &#039;button&#039;;&lt;br /&gt;
		button.textContent = label;&lt;br /&gt;
&lt;br /&gt;
		button.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			copyText( value, button );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return button;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function renderMatches( container, matches ) {&lt;br /&gt;
		container.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
		var grid = document.createElement( &#039;div&#039; );&lt;br /&gt;
		grid.className = &#039;eql-iconfinder-results-grid&#039;;&lt;br /&gt;
		container.appendChild( grid );&lt;br /&gt;
&lt;br /&gt;
		matches.forEach( function ( match ) {&lt;br /&gt;
			var record = match.record;&lt;br /&gt;
			var meta = iconMetadata( record.title );&lt;br /&gt;
			var card = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var img = document.createElement( &#039;img&#039; );&lt;br /&gt;
			var textWrap = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var link = document.createElement( &#039;a&#039; );&lt;br /&gt;
			var score = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var iconId = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var metadata = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var copyRow = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
			card.className = &#039;eql-iconfinder-result&#039;;&lt;br /&gt;
&lt;br /&gt;
			img.src = record.url;&lt;br /&gt;
			img.alt = meta.filename;&lt;br /&gt;
			img.loading = &#039;lazy&#039;;&lt;br /&gt;
&lt;br /&gt;
			link.className = &#039;eql-iconfinder-result-name&#039;;&lt;br /&gt;
			link.href = mw.util.getUrl( record.title );&lt;br /&gt;
			link.target = &#039;_blank&#039;;&lt;br /&gt;
			link.rel = &#039;noopener&#039;;&lt;br /&gt;
			link.textContent = meta.filename;&lt;br /&gt;
&lt;br /&gt;
			score.className = &#039;eql-iconfinder-result-score&#039;;&lt;br /&gt;
			score.textContent =&lt;br /&gt;
				&#039;Match score &#039; + match.similarity.toFixed( 1 );&lt;br /&gt;
&lt;br /&gt;
			iconId.className = &#039;eql-iconfinder-result-id&#039;;&lt;br /&gt;
			iconId.textContent = meta.id&lt;br /&gt;
				? &#039;Icon &#039; + meta.id&lt;br /&gt;
				: meta.filename;&lt;br /&gt;
&lt;br /&gt;
			metadata.className = &#039;eql-iconfinder-result-meta&#039;;&lt;br /&gt;
			metadata.textContent = meta.kind;&lt;br /&gt;
&lt;br /&gt;
			copyRow.className = &#039;eql-iconfinder-copy-row&#039;;&lt;br /&gt;
&lt;br /&gt;
			if ( meta.id ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy ID&#039;, meta.id )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if ( meta.parameter ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy Parameter&#039;, meta.parameter )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Raw Wikicode&#039;, meta.rawWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;32×32&#039;, meta.staticWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Scaled&#039;, meta.scaledWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			textWrap.appendChild( link );&lt;br /&gt;
			textWrap.appendChild( iconId );&lt;br /&gt;
			textWrap.appendChild( score );&lt;br /&gt;
			textWrap.appendChild( metadata );&lt;br /&gt;
			textWrap.appendChild( copyRow );&lt;br /&gt;
&lt;br /&gt;
			card.appendChild( img );&lt;br /&gt;
			card.appendChild( textWrap );&lt;br /&gt;
			grid.appendChild( card );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		var note = document.createElement( &#039;div&#039; );&lt;br /&gt;
		note.className = &#039;eql-iconfinder-note&#039;;&lt;br /&gt;
		note.textContent =&lt;br /&gt;
			&#039;The tool only identifies likely matches and copies values/wikicode to the clipboard; it never changes the editor. &#039; +&lt;br /&gt;
			&#039;For the best result, capture only the square icon with as little surrounding UI as possible.&#039;;&lt;br /&gt;
		container.appendChild( note );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function imageFromClipboardEvent( event ) {&lt;br /&gt;
		var clipboard = event.clipboardData;&lt;br /&gt;
		var items;&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		if ( !clipboard || !clipboard.items ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		items = clipboard.items;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; items.length; i++ ) {&lt;br /&gt;
			if (&lt;br /&gt;
				items[ i ].kind === &#039;file&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				/^image\//i.test( items[ i ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				return items[ i ].getAsFile();&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return null;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setPreview( img, blob ) {&lt;br /&gt;
		if ( currentPreviewUrl ) {&lt;br /&gt;
			URL.revokeObjectURL( currentPreviewUrl );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		currentPreviewUrl = URL.createObjectURL( blob );&lt;br /&gt;
		img.src = currentPreviewUrl;&lt;br /&gt;
		img.classList.add( &#039;is-visible&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function processBlob( blob, preview, status, results, api ) {&lt;br /&gt;
		var setStatus = function ( text ) {&lt;br /&gt;
			status.textContent = text;&lt;br /&gt;
		};&lt;br /&gt;
&lt;br /&gt;
		setPreview( preview, blob );&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Analyzing pasted icon…&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		return blobToDrawable( blob )&lt;br /&gt;
			.then( function ( drawable ) {&lt;br /&gt;
				var targets = fingerprintPastedImage( drawable );&lt;br /&gt;
				closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
				return getOrBuildIndex( api, setStatus )&lt;br /&gt;
					.then( function ( index ) {&lt;br /&gt;
						var matches;&lt;br /&gt;
&lt;br /&gt;
						if ( !index.length ) {&lt;br /&gt;
							throw new Error( &#039;No uploaded icons were found.&#039; );&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Comparing against &#039; +&lt;br /&gt;
								index.length +&lt;br /&gt;
								&#039; uploaded icons…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						/*&lt;br /&gt;
						 * Yield once so the status message can paint before&lt;br /&gt;
						 * ranking several thousand in-memory fingerprints.&lt;br /&gt;
						 */&lt;br /&gt;
						return new Promise( function ( resolve ) {&lt;br /&gt;
							window.setTimeout( function () {&lt;br /&gt;
								matches = rankMatches( targets, index );&lt;br /&gt;
								resolve( matches );&lt;br /&gt;
							}, 0 );&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
			} )&lt;br /&gt;
			.then( function ( matches ) {&lt;br /&gt;
				renderMatches( results, matches );&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Done. Showing the &#039; +&lt;br /&gt;
						matches.length +&lt;br /&gt;
						&#039; closest matches.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				results.innerHTML =&lt;br /&gt;
					&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
						&#039;The icon could not be matched.&#039; +&lt;br /&gt;
					&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Icon Finder error: &#039; +&lt;br /&gt;
						( error &amp;amp;&amp;amp; error.message ? error.message : &#039;Unknown error&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildUi( host ) {&lt;br /&gt;
		var api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		host.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		var shell = document.createElement( &#039;div&#039; );&lt;br /&gt;
		shell.className = &#039;eql-iconfinder-shell&#039;;&lt;br /&gt;
&lt;br /&gt;
		var left = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var paste = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var instruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var subinstruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var preview = document.createElement( &#039;img&#039; );&lt;br /&gt;
		var status = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		paste.className = &#039;eql-iconfinder-paste&#039;;&lt;br /&gt;
		paste.tabIndex = 0;&lt;br /&gt;
		paste.setAttribute( &#039;role&#039;, &#039;button&#039; );&lt;br /&gt;
		paste.setAttribute(&lt;br /&gt;
			&#039;aria-label&#039;,&lt;br /&gt;
			&#039;Paste a screen capture of an EverQuest icon&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		instruction.className = &#039;eql-iconfinder-instruction&#039;;&lt;br /&gt;
		instruction.textContent =&lt;br /&gt;
			&#039;Screen capture and paste JUST the Icon&#039;;&lt;br /&gt;
&lt;br /&gt;
		subinstruction.className = &#039;eql-iconfinder-subinstruction&#039;;&lt;br /&gt;
		subinstruction.textContent =&lt;br /&gt;
			&#039;Click this box, then press Ctrl+V. You can also drag an image here.&#039;;&lt;br /&gt;
&lt;br /&gt;
		preview.className = &#039;eql-iconfinder-preview&#039;;&lt;br /&gt;
		preview.alt = &#039;Pasted icon preview&#039;;&lt;br /&gt;
&lt;br /&gt;
		status.className = &#039;eql-iconfinder-status&#039;;&lt;br /&gt;
		status.setAttribute( &#039;aria-live&#039;, &#039;polite&#039; );&lt;br /&gt;
		status.textContent =&lt;br /&gt;
			&#039;Nothing is loaded from Icon List until you paste an image.&#039;;&lt;br /&gt;
&lt;br /&gt;
		paste.appendChild( instruction );&lt;br /&gt;
		paste.appendChild( subinstruction );&lt;br /&gt;
		paste.appendChild( preview );&lt;br /&gt;
&lt;br /&gt;
		left.appendChild( paste );&lt;br /&gt;
		left.appendChild( status );&lt;br /&gt;
&lt;br /&gt;
		var right = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var resultsTitle = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var results = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		resultsTitle.className = &#039;eql-iconfinder-results-title&#039;;&lt;br /&gt;
		resultsTitle.textContent = &#039;Possible Matches&#039;;&lt;br /&gt;
&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Paste an icon to search the uploaded images on Icon List.&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		right.appendChild( resultsTitle );&lt;br /&gt;
		right.appendChild( results );&lt;br /&gt;
&lt;br /&gt;
		shell.appendChild( left );&lt;br /&gt;
		shell.appendChild( right );&lt;br /&gt;
		host.appendChild( shell );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;paste&#039;, function ( event ) {&lt;br /&gt;
			var file = imageFromClipboardEvent( event );&lt;br /&gt;
&lt;br /&gt;
			if ( !file ) {&lt;br /&gt;
				status.textContent =&lt;br /&gt;
					&#039;Clipboard does not contain an image. Screen capture the icon, then paste again.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				file,&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;dragover&#039;, function ( event ) {&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;drop&#039;, function ( event ) {&lt;br /&gt;
			var files;&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
			files = event.dataTransfer &amp;amp;&amp;amp; event.dataTransfer.files;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				!files ||&lt;br /&gt;
				!files.length ||&lt;br /&gt;
				!/^image\//i.test( files[ 0 ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				status.textContent = &#039;Drop an image file here.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				files[ 0 ],&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Focus the paste target immediately when the panel first opens so&lt;br /&gt;
		 * the normal workflow is: click Icon Finder -&amp;gt; Ctrl+V.&lt;br /&gt;
		 */&lt;br /&gt;
		window.setTimeout( function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		}, 0 );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	window.EQLIconFinder = {&lt;br /&gt;
		init: function ( host ) {&lt;br /&gt;
			if ( !host ) {&lt;br /&gt;
				return Promise.reject(&lt;br /&gt;
					new Error( &#039;Icon Finder host element is missing.&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			addStyles();&lt;br /&gt;
&lt;br /&gt;
			if ( initializedHost !== host ) {&lt;br /&gt;
				buildUi( host );&lt;br /&gt;
				initializedHost = host;&lt;br /&gt;
			} else {&lt;br /&gt;
				var paste = host.querySelector( &#039;.eql-iconfinder-paste&#039; );&lt;br /&gt;
&lt;br /&gt;
				if ( paste ) {&lt;br /&gt;
					window.setTimeout( function () {&lt;br /&gt;
						paste.focus();&lt;br /&gt;
					}, 0 );&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return Promise.resolve();&lt;br /&gt;
		}&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168740</id>
		<title>MediaWiki:BlueprintLoader.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168740"/>
		<updated>2026-08-10T06:28:54Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Refresh Icon Finder server-cache client&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Editor Tools / Dynamic Blueprint Loader&lt;br /&gt;
 *&lt;br /&gt;
 * Source-editor behavior:&lt;br /&gt;
 * - On a new, truly empty page: show the Help:Contents Blueprint selector.&lt;br /&gt;
 * - On every source edit page: show the Icon Finder button.&lt;br /&gt;
 * - Heavy Icon Finder logic is lazy-loaded only when opened.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var BLUEPRINT_PAGE = &#039;Help:Contents&#039;;&lt;br /&gt;
	var TOOLS_ID = &#039;eql-editor-tools&#039;;&lt;br /&gt;
	var ICON_FINDER_PAGE = &#039;MediaWiki:IconFinder.js&#039;;&lt;br /&gt;
&lt;br /&gt;
	function htmlToText( html ) {&lt;br /&gt;
		var div = document.createElement( &#039;div&#039; );&lt;br /&gt;
		div.innerHTML = String( html || &#039;&#039; );&lt;br /&gt;
		return ( div.textContent || div.innerText || &#039;&#039; ).trim();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeLabel( heading ) {&lt;br /&gt;
		return heading&lt;br /&gt;
			.replace( /\s+Blueprints?\s*$/i, &#039;&#039; )&lt;br /&gt;
			.trim() + &#039; Blueprint&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getEditorContents( $textarea ) {&lt;br /&gt;
		try {&lt;br /&gt;
			return String( $textarea.textSelection( &#039;getContents&#039; ) || &#039;&#039; );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			return String( $textarea.val() || &#039;&#039; );&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setEditorContents( $textarea, text ) {&lt;br /&gt;
		try {&lt;br /&gt;
			$textarea.textSelection( &#039;setContents&#039;, text );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			$textarea.val( text );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea.trigger( &#039;input&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;change&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;focus&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function extractBlueprint( sectionText ) {&lt;br /&gt;
		var match;&lt;br /&gt;
		var blueprint;&lt;br /&gt;
&lt;br /&gt;
		sectionText = String( sectionText || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		match = sectionText.match(&lt;br /&gt;
			/&amp;lt;pre(?:\s[^&amp;gt;]*)?&amp;gt;([\s\S]*?)&amp;lt;\/pre&amp;gt;/i&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		if ( !match ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		blueprint = match[ 1 ];&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint.replace(&lt;br /&gt;
			/^\s*&amp;lt;nowiki&amp;gt;([\s\S]*?)&amp;lt;\/nowiki&amp;gt;\s*$/i,&lt;br /&gt;
			&#039;$1&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint&lt;br /&gt;
			.replace( /^\s*\n/, &#039;&#039; )&lt;br /&gt;
			.replace( /\s+$/, &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		return blueprint + &#039;\n&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintSections( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;tocdata&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var tocdata =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse.tocdata;&lt;br /&gt;
&lt;br /&gt;
			var sections =&lt;br /&gt;
				tocdata &amp;amp;&amp;amp;&lt;br /&gt;
				Array.isArray( tocdata.sections )&lt;br /&gt;
					? tocdata.sections&lt;br /&gt;
					: [];&lt;br /&gt;
&lt;br /&gt;
			if ( sections.length ) {&lt;br /&gt;
				return sections;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return api.get( {&lt;br /&gt;
				action: &#039;parse&#039;,&lt;br /&gt;
				page: BLUEPRINT_PAGE,&lt;br /&gt;
				prop: &#039;sections&#039;,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			} ).then( function ( fallbackData ) {&lt;br /&gt;
				return (&lt;br /&gt;
					fallbackData &amp;amp;&amp;amp;&lt;br /&gt;
					fallbackData.parse &amp;amp;&amp;amp;&lt;br /&gt;
					Array.isArray( fallbackData.parse.sections )&lt;br /&gt;
				)&lt;br /&gt;
					? fallbackData.parse.sections&lt;br /&gt;
					: [];&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeBlueprintSections( sections ) {&lt;br /&gt;
		return sections&lt;br /&gt;
			.map( function ( section ) {&lt;br /&gt;
				var heading = htmlToText( section.line || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
				return {&lt;br /&gt;
					heading: heading,&lt;br /&gt;
					label: makeLabel( heading ),&lt;br /&gt;
					index: String( section.index || &#039;&#039; ),&lt;br /&gt;
					anchor:&lt;br /&gt;
						section.linkAnchor ||&lt;br /&gt;
						section.anchor ||&lt;br /&gt;
						&#039;&#039;&lt;br /&gt;
				};&lt;br /&gt;
			} )&lt;br /&gt;
			.filter( function ( section ) {&lt;br /&gt;
				return (&lt;br /&gt;
					section.index &amp;amp;&amp;amp;&lt;br /&gt;
					/\bBlueprints?\s*$/i.test( section.heading )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintWikitext( api, sectionIndex ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;wikitext&#039;,&lt;br /&gt;
			section: sectionIndex,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var wikitext =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse&lt;br /&gt;
					? data.parse.wikitext&lt;br /&gt;
					: &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				wikitext &amp;amp;&amp;amp;&lt;br /&gt;
				typeof wikitext === &#039;object&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				Object.prototype.hasOwnProperty.call( wikitext, &#039;*&#039; )&lt;br /&gt;
			) {&lt;br /&gt;
				wikitext = wikitext[ &#039;*&#039; ];&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return String( wikitext || &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-editor-tools-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-editor-tools-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039;{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;margin:0 0 1rem 0;&#039; +&lt;br /&gt;
					&#039;padding:1rem 1.1rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,rgba(16,21,29,.97),rgba(9,13,19,.98));&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.38);&#039; +&lt;br /&gt;
					&#039;border-left:5px solid rgba(216,183,92,.82);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 9px 24px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.035);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(0,1fr) auto;&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:end;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-title,&#039; +&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .35rem 0;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;letter-spacing:.035em;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-description{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .7rem 0;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-controls{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;min-width:260px;&#039; +&lt;br /&gt;
					&#039;max-width:100%;&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .55rem;&#039; +&lt;br /&gt;
					&#039;background:#0b1017;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.36);&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#e2e2cf;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button{&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .85rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,#806d32,#594817);&#039; +&lt;br /&gt;
					&#039;border:1px solid #b69c4b;&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#fff5cc;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;cursor:pointer;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:hover:not(:disabled){&#039; +&lt;br /&gt;
					&#039;filter:brightness(1.08);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:disabled{&#039; +&lt;br /&gt;
					&#039;cursor:default;&#039; +&lt;br /&gt;
					&#039;opacity:.55;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-source{&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-status{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-top:.55rem;&#039; +&lt;br /&gt;
					&#039;min-height:1.2em;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:stretch;&#039; +&lt;br /&gt;
					&#039;justify-content:flex-end;&#039; +&lt;br /&gt;
					&#039;min-width:150px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-toggle{&#039; +&lt;br /&gt;
					&#039;white-space:nowrap;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host{&#039; +&lt;br /&gt;
					&#039;margin-top:1rem;&#039; +&lt;br /&gt;
					&#039;padding-top:1rem;&#039; +&lt;br /&gt;
					&#039;border-top:1px solid rgba(216,183,92,.22);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host[hidden]{display:none!important;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:760px){&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
						&#039;grid-template-columns:1fr;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
						&#039;width:100%;&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function loadIconFinderScript() {&lt;br /&gt;
		if ( window.EQLIconFinder ) {&lt;br /&gt;
			return Promise.resolve( window.EQLIconFinder );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( window.eqlIconFinderLoadingPromise ) {&lt;br /&gt;
			return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.eqlIconFinderLoadingPromise = new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var script = document.createElement( &#039;script&#039; );&lt;br /&gt;
&lt;br /&gt;
			script.src = mw.util.getUrl( ICON_FINDER_PAGE, {&lt;br /&gt;
				action: &#039;raw&#039;,&lt;br /&gt;
				ctype: &#039;text/javascript&#039;,&lt;br /&gt;
				v: &#039;8&#039;&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			script.async = true;&lt;br /&gt;
&lt;br /&gt;
			script.onload = function () {&lt;br /&gt;
				if ( window.EQLIconFinder ) {&lt;br /&gt;
					resolve( window.EQLIconFinder );&lt;br /&gt;
				} else {&lt;br /&gt;
					reject( new Error( &#039;IconFinder.js loaded without exposing EQLIconFinder.&#039; ) );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			script.onerror = function () {&lt;br /&gt;
				reject( new Error( &#039;Could not load IconFinder.js.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			document.head.appendChild( script );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildIconFinderLaunch( $row, $tools ) {&lt;br /&gt;
		var $launch = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-launch&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-title&#039;,&lt;br /&gt;
			text: &#039;ICON FINDER&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-toggle&#039;,&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Icon Finder&#039;,&lt;br /&gt;
			&#039;aria-expanded&#039;: &#039;false&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $host = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-host&#039;,&lt;br /&gt;
			hidden: true&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var opening = $host.prop( &#039;hidden&#039; );&lt;br /&gt;
&lt;br /&gt;
			if ( !opening ) {&lt;br /&gt;
				$host.prop( &#039;hidden&#039;, true );&lt;br /&gt;
				$button.attr( &#039;aria-expanded&#039;, &#039;false&#039; );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$button.text( &#039;Loading…&#039; );&lt;br /&gt;
&lt;br /&gt;
			loadIconFinderScript()&lt;br /&gt;
				.then( function ( finder ) {&lt;br /&gt;
					return finder.init( $host[ 0 ] );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$host.prop( &#039;hidden&#039;, false );&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$host&lt;br /&gt;
						.prop( &#039;hidden&#039;, false )&lt;br /&gt;
						.text( &#039;Icon Finder could not be loaded.&#039; );&lt;br /&gt;
&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.text( &#039;Icon Finder&#039; );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildBlueprintArea( $row, $textarea, api ) {&lt;br /&gt;
		var $area = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-area&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-title&#039;,&lt;br /&gt;
			text: &#039;CREATE FROM A BLUEPRINT&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-description&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;Load a standard page skeleton from Help:Contents, &#039; +&lt;br /&gt;
				&#039;or continue editing the current source below.&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $controls = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-controls&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $select = $( &#039;&amp;lt;select&amp;gt;&#039;, {&lt;br /&gt;
			&#039;aria-label&#039;: &#039;Select page blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
			value: &#039;&#039;,&lt;br /&gt;
			text: &#039;Loading blueprints...&#039;&lt;br /&gt;
		} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Load Blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $sourceLink = $( &#039;&amp;lt;a&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-source&#039;,&lt;br /&gt;
			href: mw.util.getUrl( BLUEPRINT_PAGE ),&lt;br /&gt;
			text: &#039;View Blueprints&#039;,&lt;br /&gt;
			target: &#039;_blank&#039;,&lt;br /&gt;
			rel: &#039;noopener&#039;&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $status = $( &#039;&amp;lt;span&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-status&#039;,&lt;br /&gt;
			&#039;aria-live&#039;: &#039;polite&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		fetchBlueprintSections( api )&lt;br /&gt;
			.then( function ( sections ) {&lt;br /&gt;
				var blueprints = normalizeBlueprintSections( sections );&lt;br /&gt;
&lt;br /&gt;
				$select.empty();&lt;br /&gt;
&lt;br /&gt;
				$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
					value: &#039;&#039;,&lt;br /&gt;
					text: blueprints.length ?&lt;br /&gt;
						&#039;Select a blueprint...&#039; :&lt;br /&gt;
						&#039;No blueprints found&#039;&lt;br /&gt;
				} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
				blueprints.forEach( function ( blueprint ) {&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: blueprint.index,&lt;br /&gt;
						text: blueprint.label&lt;br /&gt;
					} )&lt;br /&gt;
						.attr( &#039;data-anchor&#039;, blueprint.anchor )&lt;br /&gt;
						.appendTo( $select );&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
				$select.prop( &#039;disabled&#039;, !blueprints.length );&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				$select.empty().append(&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: &#039;&#039;,&lt;br /&gt;
						text: &#039;Could not load blueprints&#039;&lt;br /&gt;
					} )&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				$status.text(&lt;br /&gt;
					&#039;Could not read Blueprint headings from Help:Contents.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
		$select.on( &#039;change&#039;, function () {&lt;br /&gt;
			var $option = $select.find( &#039;option:selected&#039; );&lt;br /&gt;
			var anchor = $option.attr( &#039;data-anchor&#039; ) || &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
&lt;br /&gt;
			if ( anchor ) {&lt;br /&gt;
				$sourceLink.attr(&lt;br /&gt;
					&#039;href&#039;,&lt;br /&gt;
					mw.util.getUrl( BLUEPRINT_PAGE ) + &#039;#&#039; + anchor&lt;br /&gt;
				);&lt;br /&gt;
				$sourceLink.text( &#039;View This Blueprint&#039; );&lt;br /&gt;
			} else {&lt;br /&gt;
				$sourceLink.attr( &#039;href&#039;, mw.util.getUrl( BLUEPRINT_PAGE ) );&lt;br /&gt;
				$sourceLink.text( &#039;View Blueprints&#039; );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$status.text( &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var sectionIndex = $select.val();&lt;br /&gt;
			var existing;&lt;br /&gt;
&lt;br /&gt;
			if ( !sectionIndex ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			existing = getEditorContents( $textarea );&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				existing.trim() &amp;amp;&amp;amp;&lt;br /&gt;
				!window.confirm(&lt;br /&gt;
					&#039;The editor already contains text. Replace it with the selected blueprint?&#039;&lt;br /&gt;
				)&lt;br /&gt;
			) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$select.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$status.text( &#039;Loading current blueprint from Help:Contents...&#039; );&lt;br /&gt;
&lt;br /&gt;
			fetchBlueprintWikitext( api, sectionIndex )&lt;br /&gt;
				.then( function ( sectionText ) {&lt;br /&gt;
					var blueprint = extractBlueprint( sectionText );&lt;br /&gt;
&lt;br /&gt;
					if ( blueprint === null ) {&lt;br /&gt;
						throw new Error( &#039;No &amp;lt;pre&amp;gt; block found.&#039; );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					setEditorContents( $textarea, blueprint );&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Blueprint loaded. Edit the fields below, then publish normally.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Could not load this blueprint. Check Help:Contents and make sure the section contains a &amp;lt;pre&amp;gt; block.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$select.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function init() {&lt;br /&gt;
		var action = mw.config.get( &#039;wgAction&#039; );&lt;br /&gt;
		var $textarea;&lt;br /&gt;
		var $tools;&lt;br /&gt;
		var $row;&lt;br /&gt;
		var $editorHost;&lt;br /&gt;
		var api;&lt;br /&gt;
&lt;br /&gt;
		if ( [ &#039;edit&#039;, &#039;submit&#039; ].indexOf( action ) === -1 ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea = $( &#039;#wpTextbox1&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$textarea.length ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( document.getElementById( TOOLS_ID ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		addStyles();&lt;br /&gt;
&lt;br /&gt;
		$tools = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: TOOLS_ID&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$row = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-editor-tools-row&#039;&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		buildBlueprintArea( $row, $textarea, api );&lt;br /&gt;
&lt;br /&gt;
		buildIconFinderLaunch( $row, $tools );&lt;br /&gt;
&lt;br /&gt;
		$editorHost = $textarea.closest( &#039;.wikiEditor-ui&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$editorHost.length ) {&lt;br /&gt;
			$editorHost = $textarea;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$tools.insertBefore( $editorHost );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	mw.loader.using( [&lt;br /&gt;
		&#039;mediawiki.api&#039;,&lt;br /&gt;
		&#039;mediawiki.util&#039;,&lt;br /&gt;
		&#039;jquery.textSelection&#039;&lt;br /&gt;
	] ).then( function () {&lt;br /&gt;
		$( init );&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168739</id>
		<title>MediaWiki:IconFinder.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168739"/>
		<updated>2026-08-10T06:28:54Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Use EQLIconIndex server cache with browser fallback&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Icon Finder&lt;br /&gt;
 *&lt;br /&gt;
 * Lazy-loaded from MediaWiki:BlueprintLoader.js.&lt;br /&gt;
 *&lt;br /&gt;
 * Matching workflow:&lt;br /&gt;
 * 1. User pastes a screen capture containing JUST the icon.&lt;br /&gt;
 * 2. Read the current image list from [[Icon List]].&lt;br /&gt;
 * 3. Resolve only existing uploaded files through imageinfo.&lt;br /&gt;
 * 4. Build compact perceptual fingerprints in the browser.&lt;br /&gt;
 * 5. Cache that fingerprint index in IndexedDB keyed to the Icon List revision.&lt;br /&gt;
 * 6. Rank possible matches by perceptual/pixel similarity.&lt;br /&gt;
 *&lt;br /&gt;
 * No icon-catalog requests or image downloads happen until the user actually&lt;br /&gt;
 * pastes an image into the Icon Finder.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var ICON_LIST_PAGE = &#039;Icon List&#039;;&lt;br /&gt;
	var DB_NAME = &#039;eql-icon-finder&#039;;&lt;br /&gt;
	var DB_VERSION = 1;&lt;br /&gt;
	var STORE_NAME = &#039;indexes&#039;;&lt;br /&gt;
	var ALGO_VERSION = 6;&lt;br /&gt;
	var COLOR_GRID = 16;&lt;br /&gt;
	var IMAGEINFO_BATCH = 50;&lt;br /&gt;
	var DOWNLOAD_CONCURRENCY = 8;&lt;br /&gt;
	var RESULT_COUNT = 10;&lt;br /&gt;
&lt;br /&gt;
	var initializedHost = null;&lt;br /&gt;
	var memoryIndex = null;&lt;br /&gt;
	var currentPreviewUrl = null;&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-icon-finder-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;.eql-iconfinder-shell{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(230px,.62fr) minmax(0,1.38fr);&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:start;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste{&#039; +&lt;br /&gt;
					&#039;min-height:160px;&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;justify-content:center;&#039; +&lt;br /&gt;
					&#039;gap:.45rem;&#039; +&lt;br /&gt;
					&#039;padding:1rem;&#039; +&lt;br /&gt;
					&#039;background:#080d13;&#039; +&lt;br /&gt;
					&#039;border:2px dashed rgba(216,183,92,.45);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;color:#dfe7ef;&#039; +&lt;br /&gt;
					&#039;text-align:center;&#039; +&lt;br /&gt;
					&#039;cursor:paste;&#039; +&lt;br /&gt;
					&#039;outline:none;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste:focus{&#039; +&lt;br /&gt;
					&#039;border-color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 0 0 2px rgba(216,183,92,.13);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-instruction{&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-subinstruction{&#039; +&lt;br /&gt;
					&#039;color:#9faaba;&#039; +&lt;br /&gt;
					&#039;font-size:.88rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview{&#039; +&lt;br /&gt;
					&#039;display:none;&#039; +&lt;br /&gt;
					&#039;width:72px;&#039; +&lt;br /&gt;
					&#039;height:72px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;image-rendering:auto;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.14);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview.is-visible{display:block;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-status{&#039; +&lt;br /&gt;
					&#039;margin-top:.65rem;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.4;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .55rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-empty{&#039; +&lt;br /&gt;
					&#039;padding:.9rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.025);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.08);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
					&#039;color:#96a2b2;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-grid{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:54px minmax(0,1fr);&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;padding:.65rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.03);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.09);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result img{&#039; +&lt;br /&gt;
					&#039;width:52px;&#039; +&lt;br /&gt;
					&#039;height:52px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.1);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-name{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.18rem;&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.2;&#039; +&lt;br /&gt;
					&#039;word-break:break-word;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-score{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.28rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-size:.8rem;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-meta{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;color:#8f9bac;&#039; +&lt;br /&gt;
					&#039;font-size:.76rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.25;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-id{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin:.2rem 0 .35rem;&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:800;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy-row{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;gap:.3rem;&#039; +&lt;br /&gt;
					&#039;margin-top:.38rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy{&#039; +&lt;br /&gt;
					&#039;min-height:1.8rem!important;&#039; +&lt;br /&gt;
					&#039;padding:.15rem .45rem!important;&#039; +&lt;br /&gt;
					&#039;font-size:.72rem!important;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-note{&#039; +&lt;br /&gt;
					&#039;margin-top:.7rem;&#039; +&lt;br /&gt;
					&#039;color:#7f8b9b;&#039; +&lt;br /&gt;
					&#039;font-size:.78rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:850px){&#039; +&lt;br /&gt;
					&#039;.eql-iconfinder-shell{grid-template-columns:1fr;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function openDb() {&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var request;&lt;br /&gt;
&lt;br /&gt;
			if ( !window.indexedDB ) {&lt;br /&gt;
				reject( new Error( &#039;IndexedDB unavailable.&#039; ) );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			request = indexedDB.open( DB_NAME, DB_VERSION );&lt;br /&gt;
&lt;br /&gt;
			request.onupgradeneeded = function () {&lt;br /&gt;
				var db = request.result;&lt;br /&gt;
&lt;br /&gt;
				if ( !db.objectStoreNames.contains( STORE_NAME ) ) {&lt;br /&gt;
					db.createObjectStore( STORE_NAME, {&lt;br /&gt;
						keyPath: &#039;key&#039;&lt;br /&gt;
					} );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onsuccess = function () {&lt;br /&gt;
				resolve( request.result );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onerror = function () {&lt;br /&gt;
				reject( request.error || new Error( &#039;IndexedDB open failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbGet( key ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readonly&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var request = store.get( key );&lt;br /&gt;
&lt;br /&gt;
				request.onsuccess = function () {&lt;br /&gt;
					resolve( request.result || null );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				request.onerror = function () {&lt;br /&gt;
					reject( request.error || new Error( &#039;IndexedDB read failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbPutAndPrune( value ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readwrite&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var cursorRequest;&lt;br /&gt;
&lt;br /&gt;
				store.put( value );&lt;br /&gt;
&lt;br /&gt;
				cursorRequest = store.openCursor();&lt;br /&gt;
&lt;br /&gt;
				cursorRequest.onsuccess = function () {&lt;br /&gt;
					var cursor = cursorRequest.result;&lt;br /&gt;
&lt;br /&gt;
					if ( !cursor ) {&lt;br /&gt;
						return;&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					if ( cursor.key !== value.key ) {&lt;br /&gt;
						cursor.delete();&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					cursor.continue();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					resolve();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.onerror = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					reject( tx.error || new Error( &#039;IndexedDB write failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeFileTitle( title ) {&lt;br /&gt;
		title = String( title || &#039;&#039; ).trim();&lt;br /&gt;
&lt;br /&gt;
		if ( !title ) {&lt;br /&gt;
			return &#039;&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return /^File:/i.test( title )&lt;br /&gt;
			? title&lt;br /&gt;
			: &#039;File:&#039; + title;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchCatalog( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: ICON_LIST_PAGE,&lt;br /&gt;
			prop: &#039;images|revid&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var parse = data &amp;amp;&amp;amp; data.parse ? data.parse : {};&lt;br /&gt;
			var titles = Array.isArray( parse.images ) ? parse.images : [];&lt;br /&gt;
&lt;br /&gt;
			titles = titles&lt;br /&gt;
				.map( normalizeFileTitle )&lt;br /&gt;
				.filter( Boolean )&lt;br /&gt;
				.filter( function ( title, index, list ) {&lt;br /&gt;
					return list.indexOf( title ) === index;&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				revid: Number( parse.revid ) || 0,&lt;br /&gt;
				titles: titles&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function chunk( list, size ) {&lt;br /&gt;
		var out = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; list.length; i += size ) {&lt;br /&gt;
			out.push( list.slice( i, i + size ) );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchImageInfo( api, titles, setStatus ) {&lt;br /&gt;
		var batches = chunk( titles, IMAGEINFO_BATCH );&lt;br /&gt;
		var records = [];&lt;br /&gt;
		var chain = Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
		batches.forEach( function ( batch, batchIndex ) {&lt;br /&gt;
			chain = chain.then( function () {&lt;br /&gt;
				if ( setStatus ) {&lt;br /&gt;
					setStatus(&lt;br /&gt;
						&#039;Reading uploaded icon files… &#039; +&lt;br /&gt;
						Math.min( batchIndex * IMAGEINFO_BATCH, titles.length ) +&lt;br /&gt;
						&#039; / &#039; + titles.length&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return api.get( {&lt;br /&gt;
					action: &#039;query&#039;,&lt;br /&gt;
					prop: &#039;imageinfo&#039;,&lt;br /&gt;
					titles: batch.join( &#039;|&#039; ),&lt;br /&gt;
					iiprop: &#039;url|size|sha1&#039;,&lt;br /&gt;
					formatversion: 2&lt;br /&gt;
				} ).then( function ( data ) {&lt;br /&gt;
					var pages =&lt;br /&gt;
						data &amp;amp;&amp;amp;&lt;br /&gt;
						data.query &amp;amp;&amp;amp;&lt;br /&gt;
						Array.isArray( data.query.pages )&lt;br /&gt;
							? data.query.pages&lt;br /&gt;
							: [];&lt;br /&gt;
&lt;br /&gt;
					pages.forEach( function ( page ) {&lt;br /&gt;
						var info =&lt;br /&gt;
							page &amp;amp;&amp;amp;&lt;br /&gt;
							Array.isArray( page.imageinfo ) &amp;amp;&amp;amp;&lt;br /&gt;
							page.imageinfo.length&lt;br /&gt;
								? page.imageinfo[ 0 ]&lt;br /&gt;
								: null;&lt;br /&gt;
&lt;br /&gt;
						if ( !info || !info.url ) {&lt;br /&gt;
							return;&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						records.push( {&lt;br /&gt;
							title: normalizeFileTitle( page.title ),&lt;br /&gt;
							url: String( info.url ),&lt;br /&gt;
							sha1: String( info.sha1 || &#039;&#039; ),&lt;br /&gt;
							width: Number( info.width ) || 0,&lt;br /&gt;
							height: Number( info.height ) || 0&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return chain.then( function () {&lt;br /&gt;
			return records;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function blobToDrawable( blob ) {&lt;br /&gt;
		if ( window.createImageBitmap ) {&lt;br /&gt;
			return createImageBitmap( blob );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var url = URL.createObjectURL( blob );&lt;br /&gt;
			var img = new Image();&lt;br /&gt;
&lt;br /&gt;
			img.onload = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				resolve( img );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.onerror = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				reject( new Error( &#039;Image decode failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.src = url;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchDrawable( url ) {&lt;br /&gt;
		return fetch( url, {&lt;br /&gt;
			credentials: &#039;same-origin&#039;,&lt;br /&gt;
			cache: &#039;force-cache&#039;&lt;br /&gt;
		} )&lt;br /&gt;
			.then( function ( response ) {&lt;br /&gt;
				if ( !response.ok ) {&lt;br /&gt;
					throw new Error( &#039;HTTP &#039; + response.status );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return response.blob();&lt;br /&gt;
			} )&lt;br /&gt;
			.then( blobToDrawable );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function closeDrawable( drawable ) {&lt;br /&gt;
		if ( drawable &amp;amp;&amp;amp; typeof drawable.close === &#039;function&#039; ) {&lt;br /&gt;
			drawable.close();&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function drawScaled( drawable, width, height, cropFraction ) {&lt;br /&gt;
		var sourceCanvas = document.createElement( &#039;canvas&#039; );&lt;br /&gt;
		var sourceWidth =&lt;br /&gt;
			Number( drawable.width || drawable.naturalWidth ) || 1;&lt;br /&gt;
		var sourceHeight =&lt;br /&gt;
			Number( drawable.height || drawable.naturalHeight ) || 1;&lt;br /&gt;
		var sourceCtx;&lt;br /&gt;
		var sourceImage;&lt;br /&gt;
		var sourceData;&lt;br /&gt;
		var pixelCount = sourceWidth * sourceHeight;&lt;br /&gt;
		var transparentCount = 0;&lt;br /&gt;
		var mask = new Uint8Array( pixelCount );&lt;br /&gt;
		var foregroundCount = 0;&lt;br /&gt;
		var borderR = [];&lt;br /&gt;
		var borderG = [];&lt;br /&gt;
		var borderB = [];&lt;br /&gt;
		var bgR = 0;&lt;br /&gt;
		var bgG = 0;&lt;br /&gt;
		var bgB = 0;&lt;br /&gt;
		var bgLuma = 0;&lt;br /&gt;
		var borderSpread = [];&lt;br /&gt;
		var tolerance;&lt;br /&gt;
		var darkCutoff;&lt;br /&gt;
		var minX = sourceWidth;&lt;br /&gt;
		var minY = sourceHeight;&lt;br /&gt;
		var maxX = -1;&lt;br /&gt;
		var maxY = -1;&lt;br /&gt;
		var cleanCanvas;&lt;br /&gt;
		var cleanCtx;&lt;br /&gt;
		var cleanImage;&lt;br /&gt;
		var targetCanvas;&lt;br /&gt;
		var targetCtx;&lt;br /&gt;
		var usableWidth;&lt;br /&gt;
		var usableHeight;&lt;br /&gt;
		var scale;&lt;br /&gt;
		var drawWidth;&lt;br /&gt;
		var drawHeight;&lt;br /&gt;
		var drawX;&lt;br /&gt;
		var drawY;&lt;br /&gt;
		var i;&lt;br /&gt;
		var p;&lt;br /&gt;
		var x;&lt;br /&gt;
		var y;&lt;br /&gt;
		var r;&lt;br /&gt;
		var g;&lt;br /&gt;
		var b;&lt;br /&gt;
		var a;&lt;br /&gt;
		var luma;&lt;br /&gt;
		var chroma;&lt;br /&gt;
		var maxChannel;&lt;br /&gt;
		var minChannel;&lt;br /&gt;
		var colorDistance;&lt;br /&gt;
&lt;br /&gt;
		function median( values ) {&lt;br /&gt;
			var sorted;&lt;br /&gt;
&lt;br /&gt;
			if ( !values.length ) {&lt;br /&gt;
				return 0;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			sorted = values.slice().sort(&lt;br /&gt;
				function ( left, right ) {&lt;br /&gt;
					return left - right;&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			return sorted[&lt;br /&gt;
				Math.floor( sorted.length / 2 )&lt;br /&gt;
			];&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		function addBorderPixel( px, py ) {&lt;br /&gt;
			var index =&lt;br /&gt;
				( py * sourceWidth + px ) * 4;&lt;br /&gt;
&lt;br /&gt;
			if ( sourceData[ index + 3 ] &amp;lt; 16 ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			borderR.push( sourceData[ index ] );&lt;br /&gt;
			borderG.push( sourceData[ index + 1 ] );&lt;br /&gt;
			borderB.push( sourceData[ index + 2 ] );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		sourceCanvas.width = sourceWidth;&lt;br /&gt;
		sourceCanvas.height = sourceHeight;&lt;br /&gt;
&lt;br /&gt;
		sourceCtx = sourceCanvas.getContext(&lt;br /&gt;
			&#039;2d&#039;,&lt;br /&gt;
			{&lt;br /&gt;
				willReadFrequently: true&lt;br /&gt;
			}&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Preserve real PNG transparency.&lt;br /&gt;
		 */&lt;br /&gt;
		sourceCtx.clearRect(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			sourceWidth,&lt;br /&gt;
			sourceHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceCtx.drawImage(&lt;br /&gt;
			drawable,&lt;br /&gt;
			0,&lt;br /&gt;
			0&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceImage = sourceCtx.getImageData(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			sourceWidth,&lt;br /&gt;
			sourceHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceData = sourceImage.data;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; pixelCount; i++ ) {&lt;br /&gt;
			if ( sourceData[ i * 4 + 3 ] &amp;lt; 245 ) {&lt;br /&gt;
				transparentCount++;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			transparentCount &amp;gt;&lt;br /&gt;
			Math.max( 4, pixelCount * 0.01 )&lt;br /&gt;
		) {&lt;br /&gt;
			/*&lt;br /&gt;
			 * Uploaded transparent PNG:&lt;br /&gt;
			 * alpha is already an excellent mask.&lt;br /&gt;
			 */&lt;br /&gt;
			for ( i = 0; i &amp;lt; pixelCount; i++ ) {&lt;br /&gt;
				if (&lt;br /&gt;
					sourceData[ i * 4 + 3 ] &amp;gt; 24&lt;br /&gt;
				) {&lt;br /&gt;
					mask[ i ] = 1;&lt;br /&gt;
					foregroundCount++;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		} else {&lt;br /&gt;
			/*&lt;br /&gt;
			 * Pasted screenshot:&lt;br /&gt;
			 *&lt;br /&gt;
			 * Infer the UI background from a two-pixel&lt;br /&gt;
			 * border around the capture.&lt;br /&gt;
			 */&lt;br /&gt;
			for (&lt;br /&gt;
				x = 0;&lt;br /&gt;
				x &amp;lt; sourceWidth;&lt;br /&gt;
				x++&lt;br /&gt;
			) {&lt;br /&gt;
				for (&lt;br /&gt;
					y = 0;&lt;br /&gt;
					y &amp;lt; Math.min(&lt;br /&gt;
						2,&lt;br /&gt;
						sourceHeight&lt;br /&gt;
					);&lt;br /&gt;
					y++&lt;br /&gt;
				) {&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						sourceHeight - 1 - y&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				y = 0;&lt;br /&gt;
				y &amp;lt; sourceHeight;&lt;br /&gt;
				y++&lt;br /&gt;
			) {&lt;br /&gt;
				for (&lt;br /&gt;
					x = 0;&lt;br /&gt;
					x &amp;lt; Math.min(&lt;br /&gt;
						2,&lt;br /&gt;
						sourceWidth&lt;br /&gt;
					);&lt;br /&gt;
					x++&lt;br /&gt;
				) {&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						sourceWidth - 1 - x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			bgR = median( borderR );&lt;br /&gt;
			bgG = median( borderG );&lt;br /&gt;
			bgB = median( borderB );&lt;br /&gt;
&lt;br /&gt;
			bgLuma = luminance(&lt;br /&gt;
				bgR,&lt;br /&gt;
				bgG,&lt;br /&gt;
				bgB&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				i = 0;&lt;br /&gt;
				i &amp;lt; borderR.length;&lt;br /&gt;
				i++&lt;br /&gt;
			) {&lt;br /&gt;
				borderSpread.push(&lt;br /&gt;
					Math.sqrt(&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderR[ i ] - bgR,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderG[ i ] - bgG,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderB[ i ] - bgB,&lt;br /&gt;
							2&lt;br /&gt;
						)&lt;br /&gt;
					)&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * The old crop variants blindly trimmed the&lt;br /&gt;
			 * image square.&lt;br /&gt;
			 *&lt;br /&gt;
			 * Now they instead make background removal&lt;br /&gt;
			 * slightly more aggressive.&lt;br /&gt;
			 */&lt;br /&gt;
			tolerance = Math.max(&lt;br /&gt;
				20,&lt;br /&gt;
				Math.min(&lt;br /&gt;
					74,&lt;br /&gt;
					18 +&lt;br /&gt;
						median(&lt;br /&gt;
							borderSpread&lt;br /&gt;
						) * 2.5 +&lt;br /&gt;
						cropFraction * 80&lt;br /&gt;
				)&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			darkCutoff = Math.max(&lt;br /&gt;
				36,&lt;br /&gt;
				Math.min(&lt;br /&gt;
					88,&lt;br /&gt;
					bgLuma +&lt;br /&gt;
						22 +&lt;br /&gt;
						cropFraction * 100&lt;br /&gt;
				)&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				i = 0;&lt;br /&gt;
				i &amp;lt; pixelCount;&lt;br /&gt;
				i++&lt;br /&gt;
			) {&lt;br /&gt;
				p = i * 4;&lt;br /&gt;
&lt;br /&gt;
				r = sourceData[ p ];&lt;br /&gt;
				g = sourceData[ p + 1 ];&lt;br /&gt;
				b = sourceData[ p + 2 ];&lt;br /&gt;
				a = sourceData[ p + 3 ];&lt;br /&gt;
&lt;br /&gt;
				if ( a &amp;lt; 16 ) {&lt;br /&gt;
					continue;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				maxChannel = Math.max(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				minChannel = Math.min(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				chroma =&lt;br /&gt;
					maxChannel -&lt;br /&gt;
					minChannel;&lt;br /&gt;
&lt;br /&gt;
				luma = luminance(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				colorDistance =&lt;br /&gt;
					Math.sqrt(&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							r - bgR,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							g - bgG,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							b - bgB,&lt;br /&gt;
							2&lt;br /&gt;
						)&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				/*&lt;br /&gt;
				 * EQ&#039;s editor/game icon background is&lt;br /&gt;
				 * normally very dark.&lt;br /&gt;
				 *&lt;br /&gt;
				 * Discard dark neutral pixels, but allow&lt;br /&gt;
				 * strongly colored pixels to survive at&lt;br /&gt;
				 * lower brightness.&lt;br /&gt;
				 */&lt;br /&gt;
				if ( bgLuma &amp;lt; 95 ) {&lt;br /&gt;
					if (&lt;br /&gt;
						(&lt;br /&gt;
							luma &amp;gt;=&lt;br /&gt;
								darkCutoff &amp;amp;&amp;amp;&lt;br /&gt;
							colorDistance &amp;gt;=&lt;br /&gt;
								tolerance *&lt;br /&gt;
								0.65&lt;br /&gt;
						) ||&lt;br /&gt;
						(&lt;br /&gt;
							chroma &amp;gt;= 24 &amp;amp;&amp;amp;&lt;br /&gt;
							luma &amp;gt;=&lt;br /&gt;
								bgLuma +&lt;br /&gt;
								7 +&lt;br /&gt;
								cropFraction *&lt;br /&gt;
								30 &amp;amp;&amp;amp;&lt;br /&gt;
							colorDistance &amp;gt;=&lt;br /&gt;
								tolerance *&lt;br /&gt;
								0.55&lt;br /&gt;
						)&lt;br /&gt;
					) {&lt;br /&gt;
						mask[ i ] = 1;&lt;br /&gt;
						foregroundCount++;&lt;br /&gt;
					}&lt;br /&gt;
				} else if (&lt;br /&gt;
					colorDistance &amp;gt;=&lt;br /&gt;
					tolerance&lt;br /&gt;
				) {&lt;br /&gt;
					mask[ i ] = 1;&lt;br /&gt;
					foregroundCount++;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * Fail soft for an unusual image instead of&lt;br /&gt;
			 * producing an empty fingerprint.&lt;br /&gt;
			 */&lt;br /&gt;
			if (&lt;br /&gt;
				foregroundCount &amp;lt; 6 ||&lt;br /&gt;
				foregroundCount &amp;gt;&lt;br /&gt;
					pixelCount * 0.90&lt;br /&gt;
			) {&lt;br /&gt;
				mask.fill( 0 );&lt;br /&gt;
				foregroundCount = 0;&lt;br /&gt;
&lt;br /&gt;
				for (&lt;br /&gt;
					i = 0;&lt;br /&gt;
					i &amp;lt; pixelCount;&lt;br /&gt;
					i++&lt;br /&gt;
				) {&lt;br /&gt;
					if (&lt;br /&gt;
						sourceData[&lt;br /&gt;
							i * 4 + 3&lt;br /&gt;
						] &amp;gt; 24&lt;br /&gt;
					) {&lt;br /&gt;
						mask[ i ] = 1;&lt;br /&gt;
						foregroundCount++;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Find the actual sprite bounds.&lt;br /&gt;
		 */&lt;br /&gt;
		for (&lt;br /&gt;
			y = 0;&lt;br /&gt;
			y &amp;lt; sourceHeight;&lt;br /&gt;
			y++&lt;br /&gt;
		) {&lt;br /&gt;
			for (&lt;br /&gt;
				x = 0;&lt;br /&gt;
				x &amp;lt; sourceWidth;&lt;br /&gt;
				x++&lt;br /&gt;
			) {&lt;br /&gt;
				i =&lt;br /&gt;
					y * sourceWidth +&lt;br /&gt;
					x;&lt;br /&gt;
&lt;br /&gt;
				if ( !mask[ i ] ) {&lt;br /&gt;
					continue;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				minX = Math.min(&lt;br /&gt;
					minX,&lt;br /&gt;
					x&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				minY = Math.min(&lt;br /&gt;
					minY,&lt;br /&gt;
					y&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				maxX = Math.max(&lt;br /&gt;
					maxX,&lt;br /&gt;
					x&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				maxY = Math.max(&lt;br /&gt;
					maxY,&lt;br /&gt;
					y&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			maxX &amp;lt; minX ||&lt;br /&gt;
			maxY &amp;lt; minY&lt;br /&gt;
		) {&lt;br /&gt;
			minX = 0;&lt;br /&gt;
			minY = 0;&lt;br /&gt;
			maxX = sourceWidth - 1;&lt;br /&gt;
			maxY = sourceHeight - 1;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Create a transparent copy containing only&lt;br /&gt;
		 * detected foreground pixels.&lt;br /&gt;
		 */&lt;br /&gt;
		cleanCanvas =&lt;br /&gt;
			document.createElement(&lt;br /&gt;
				&#039;canvas&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		cleanCanvas.width =&lt;br /&gt;
			sourceWidth;&lt;br /&gt;
&lt;br /&gt;
		cleanCanvas.height =&lt;br /&gt;
			sourceHeight;&lt;br /&gt;
&lt;br /&gt;
		cleanCtx =&lt;br /&gt;
			cleanCanvas.getContext(&lt;br /&gt;
				&#039;2d&#039;,&lt;br /&gt;
				{&lt;br /&gt;
					willReadFrequently: true&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		cleanImage =&lt;br /&gt;
			cleanCtx.createImageData(&lt;br /&gt;
				sourceWidth,&lt;br /&gt;
				sourceHeight&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i &amp;lt; pixelCount;&lt;br /&gt;
			i++&lt;br /&gt;
		) {&lt;br /&gt;
			if ( !mask[ i ] ) {&lt;br /&gt;
				continue;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			p = i * 4;&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p ] =&lt;br /&gt;
				sourceData[ p ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 1 ] =&lt;br /&gt;
				sourceData[ p + 1 ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 2 ] =&lt;br /&gt;
				sourceData[ p + 2 ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 3 ] =&lt;br /&gt;
				255;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		cleanCtx.putImageData(&lt;br /&gt;
			cleanImage,&lt;br /&gt;
			0,&lt;br /&gt;
			0&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Normalize the detected sprite to the requested&lt;br /&gt;
		 * comparison size.&lt;br /&gt;
		 */&lt;br /&gt;
		targetCanvas =&lt;br /&gt;
			document.createElement(&lt;br /&gt;
				&#039;canvas&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		targetCanvas.width = width;&lt;br /&gt;
		targetCanvas.height = height;&lt;br /&gt;
&lt;br /&gt;
		targetCtx =&lt;br /&gt;
			targetCanvas.getContext(&lt;br /&gt;
				&#039;2d&#039;,&lt;br /&gt;
				{&lt;br /&gt;
					willReadFrequently: true&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.fillStyle = &#039;#000&#039;;&lt;br /&gt;
&lt;br /&gt;
		targetCtx.fillRect(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			width,&lt;br /&gt;
			height&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.imageSmoothingEnabled =&lt;br /&gt;
			true;&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			&#039;imageSmoothingQuality&#039; in&lt;br /&gt;
			targetCtx&lt;br /&gt;
		) {&lt;br /&gt;
			targetCtx.imageSmoothingQuality =&lt;br /&gt;
				&#039;high&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		usableWidth =&lt;br /&gt;
			Math.max(&lt;br /&gt;
				1,&lt;br /&gt;
				width - 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		usableHeight =&lt;br /&gt;
			Math.max(&lt;br /&gt;
				1,&lt;br /&gt;
				height - 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		scale = Math.min(&lt;br /&gt;
			usableWidth /&lt;br /&gt;
				Math.max(&lt;br /&gt;
					1,&lt;br /&gt;
					maxX - minX + 1&lt;br /&gt;
				),&lt;br /&gt;
			usableHeight /&lt;br /&gt;
				Math.max(&lt;br /&gt;
					1,&lt;br /&gt;
					maxY - minY + 1&lt;br /&gt;
				)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawWidth = Math.max(&lt;br /&gt;
			1,&lt;br /&gt;
			Math.round(&lt;br /&gt;
				(&lt;br /&gt;
					maxX -&lt;br /&gt;
					minX +&lt;br /&gt;
					1&lt;br /&gt;
				) * scale&lt;br /&gt;
			)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawHeight = Math.max(&lt;br /&gt;
			1,&lt;br /&gt;
			Math.round(&lt;br /&gt;
				(&lt;br /&gt;
					maxY -&lt;br /&gt;
					minY +&lt;br /&gt;
					1&lt;br /&gt;
				) * scale&lt;br /&gt;
			)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawX = Math.floor(&lt;br /&gt;
			(&lt;br /&gt;
				width -&lt;br /&gt;
				drawWidth&lt;br /&gt;
			) / 2&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawY = Math.floor(&lt;br /&gt;
			(&lt;br /&gt;
				height -&lt;br /&gt;
				drawHeight&lt;br /&gt;
			) / 2&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.drawImage(&lt;br /&gt;
			cleanCanvas,&lt;br /&gt;
			minX,&lt;br /&gt;
			minY,&lt;br /&gt;
			maxX - minX + 1,&lt;br /&gt;
			maxY - minY + 1,&lt;br /&gt;
			drawX,&lt;br /&gt;
			drawY,&lt;br /&gt;
			drawWidth,&lt;br /&gt;
			drawHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		return targetCtx.getImageData(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			width,&lt;br /&gt;
			height&lt;br /&gt;
		);&lt;br /&gt;
	}&lt;br /&gt;
	function luminance( r, g, b ) {&lt;br /&gt;
		return 0.2126 * r + 0.7152 * g + 0.0722 * b;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeDHash( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled( drawable, 9, 8, cropFraction );&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var hi = 0;&lt;br /&gt;
		var lo = 0;&lt;br /&gt;
		var bit = 0;&lt;br /&gt;
		var y;&lt;br /&gt;
		var x;&lt;br /&gt;
		var left;&lt;br /&gt;
		var right;&lt;br /&gt;
		var li;&lt;br /&gt;
		var ri;&lt;br /&gt;
&lt;br /&gt;
		for ( y = 0; y &amp;lt; 8; y++ ) {&lt;br /&gt;
			for ( x = 0; x &amp;lt; 8; x++ ) {&lt;br /&gt;
				li = ( y * 9 + x ) * 4;&lt;br /&gt;
				ri = ( y * 9 + x + 1 ) * 4;&lt;br /&gt;
&lt;br /&gt;
				left = luminance(&lt;br /&gt;
					data[ li ],&lt;br /&gt;
					data[ li + 1 ],&lt;br /&gt;
					data[ li + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				right = luminance(&lt;br /&gt;
					data[ ri ],&lt;br /&gt;
					data[ ri + 1 ],&lt;br /&gt;
					data[ ri + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				if ( left &amp;gt; right ) {&lt;br /&gt;
					if ( bit &amp;lt; 32 ) {&lt;br /&gt;
						lo = ( lo | ( 1 &amp;lt;&amp;lt; bit ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					} else {&lt;br /&gt;
						hi = ( hi | ( 1 &amp;lt;&amp;lt; ( bit - 32 ) ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				bit++;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hi: hi &amp;gt;&amp;gt;&amp;gt; 0,&lt;br /&gt;
			lo: lo &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeColorGrid( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled(&lt;br /&gt;
			drawable,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			cropFraction&lt;br /&gt;
		);&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var out = new Uint8Array( COLOR_GRID * COLOR_GRID * 3 );&lt;br /&gt;
		var source = 0;&lt;br /&gt;
		var target = 0;&lt;br /&gt;
&lt;br /&gt;
		while ( source &amp;lt; data.length ) {&lt;br /&gt;
			out[ target++ ] = data[ source ];&lt;br /&gt;
			out[ target++ ] = data[ source + 1 ];&lt;br /&gt;
			out[ target++ ] = data[ source + 2 ];&lt;br /&gt;
			source += 4;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDrawable( drawable, cropFraction ) {&lt;br /&gt;
		var hash = computeDHash( drawable, cropFraction );&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hashHi: hash.hi,&lt;br /&gt;
			hashLo: hash.lo,&lt;br /&gt;
			rgb: computeColorGrid( drawable, cropFraction )&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function popcount32( value ) {&lt;br /&gt;
		value = value &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
		value = value - ( ( value &amp;gt;&amp;gt;&amp;gt; 1 ) &amp;amp; 0x55555555 );&lt;br /&gt;
		value = ( value &amp;amp; 0x33333333 ) + ( ( value &amp;gt;&amp;gt;&amp;gt; 2 ) &amp;amp; 0x33333333 );&lt;br /&gt;
		return (&lt;br /&gt;
			(&lt;br /&gt;
				(&lt;br /&gt;
					value + ( value &amp;gt;&amp;gt;&amp;gt; 4 )&lt;br /&gt;
				) &amp;amp; 0x0F0F0F0F&lt;br /&gt;
			) * 0x01010101&lt;br /&gt;
		) &amp;gt;&amp;gt;&amp;gt; 24;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDistance( a, b ) {&lt;br /&gt;
		var hashBits =&lt;br /&gt;
			popcount32(&lt;br /&gt;
				(&lt;br /&gt;
					a.hashHi ^&lt;br /&gt;
					b.hashHi&lt;br /&gt;
				) &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
			) +&lt;br /&gt;
			popcount32(&lt;br /&gt;
				(&lt;br /&gt;
					a.hashLo ^&lt;br /&gt;
					b.hashLo&lt;br /&gt;
				) &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		var hashDistance =&lt;br /&gt;
			hashBits / 64;&lt;br /&gt;
&lt;br /&gt;
		var length = Math.min(&lt;br /&gt;
			a.rgb.length,&lt;br /&gt;
			b.rgb.length&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		var histogramA =&lt;br /&gt;
			new Uint16Array( 64 );&lt;br /&gt;
&lt;br /&gt;
		var histogramB =&lt;br /&gt;
			new Uint16Array( 64 );&lt;br /&gt;
&lt;br /&gt;
		var foregroundA = 0;&lt;br /&gt;
		var foregroundB = 0;&lt;br /&gt;
		var intersection = 0;&lt;br /&gt;
		var union = 0;&lt;br /&gt;
		var colorSum = 0;&lt;br /&gt;
		var i;&lt;br /&gt;
		var ar;&lt;br /&gt;
		var ag;&lt;br /&gt;
		var ab;&lt;br /&gt;
		var br;&lt;br /&gt;
		var bg;&lt;br /&gt;
		var bb;&lt;br /&gt;
		var aForeground;&lt;br /&gt;
		var bForeground;&lt;br /&gt;
		var dr;&lt;br /&gt;
		var dg;&lt;br /&gt;
		var db;&lt;br /&gt;
		var binA;&lt;br /&gt;
		var binB;&lt;br /&gt;
		var shapeDistance;&lt;br /&gt;
		var colorDistance;&lt;br /&gt;
		var histogramDistance = 0;&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i + 2 &amp;lt; length;&lt;br /&gt;
			i += 3&lt;br /&gt;
		) {&lt;br /&gt;
			ar = a.rgb[ i ];&lt;br /&gt;
			ag = a.rgb[ i + 1 ];&lt;br /&gt;
			ab = a.rgb[ i + 2 ];&lt;br /&gt;
&lt;br /&gt;
			br = b.rgb[ i ];&lt;br /&gt;
			bg = b.rgb[ i + 1 ];&lt;br /&gt;
			bb = b.rgb[ i + 2 ];&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * Background is normalized to black by&lt;br /&gt;
			 * drawScaled().&lt;br /&gt;
			 */&lt;br /&gt;
			aForeground =&lt;br /&gt;
				Math.max(&lt;br /&gt;
					ar,&lt;br /&gt;
					ag,&lt;br /&gt;
					ab&lt;br /&gt;
				) &amp;gt; 8;&lt;br /&gt;
&lt;br /&gt;
			bForeground =&lt;br /&gt;
				Math.max(&lt;br /&gt;
					br,&lt;br /&gt;
					bg,&lt;br /&gt;
					bb&lt;br /&gt;
				) &amp;gt; 8;&lt;br /&gt;
&lt;br /&gt;
			if ( aForeground ) {&lt;br /&gt;
				foregroundA++;&lt;br /&gt;
&lt;br /&gt;
				binA =&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							ar &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 4&lt;br /&gt;
					) |&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							ag &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 2&lt;br /&gt;
					) |&lt;br /&gt;
					Math.min(&lt;br /&gt;
						3,&lt;br /&gt;
						ab &amp;gt;&amp;gt; 6&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				histogramA[ binA ]++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if ( bForeground ) {&lt;br /&gt;
				foregroundB++;&lt;br /&gt;
&lt;br /&gt;
				binB =&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							br &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 4&lt;br /&gt;
					) |&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							bg &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 2&lt;br /&gt;
					) |&lt;br /&gt;
					Math.min(&lt;br /&gt;
						3,&lt;br /&gt;
						bb &amp;gt;&amp;gt; 6&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				histogramB[ binB ]++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				aForeground ||&lt;br /&gt;
				bForeground&lt;br /&gt;
			) {&lt;br /&gt;
				union++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				!aForeground ||&lt;br /&gt;
				!bForeground&lt;br /&gt;
			) {&lt;br /&gt;
				continue;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			intersection++;&lt;br /&gt;
&lt;br /&gt;
			dr = ar - br;&lt;br /&gt;
			dg = ag - bg;&lt;br /&gt;
			db = ab - bb;&lt;br /&gt;
&lt;br /&gt;
			colorSum +=&lt;br /&gt;
				Math.sqrt(&lt;br /&gt;
					dr * dr +&lt;br /&gt;
					dg * dg +&lt;br /&gt;
					db * db&lt;br /&gt;
				) /&lt;br /&gt;
				441.67295593;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		shapeDistance =&lt;br /&gt;
			union&lt;br /&gt;
				? 1 -&lt;br /&gt;
					intersection /&lt;br /&gt;
						union&lt;br /&gt;
				: 1;&lt;br /&gt;
&lt;br /&gt;
		colorDistance =&lt;br /&gt;
			intersection&lt;br /&gt;
				? colorSum /&lt;br /&gt;
					intersection&lt;br /&gt;
				: 1;&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i &amp;lt; 64;&lt;br /&gt;
			i++&lt;br /&gt;
		) {&lt;br /&gt;
			histogramDistance +=&lt;br /&gt;
				Math.abs(&lt;br /&gt;
					histogramA[ i ] /&lt;br /&gt;
						Math.max(&lt;br /&gt;
							1,&lt;br /&gt;
							foregroundA&lt;br /&gt;
						) -&lt;br /&gt;
					histogramB[ i ] /&lt;br /&gt;
						Math.max(&lt;br /&gt;
							1,&lt;br /&gt;
							foregroundB&lt;br /&gt;
						)&lt;br /&gt;
				);&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Normalized histogram L1 range is 0..2.&lt;br /&gt;
		 */&lt;br /&gt;
		histogramDistance =&lt;br /&gt;
			Math.min(&lt;br /&gt;
				1,&lt;br /&gt;
				histogramDistance / 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Foreground color is deliberately the largest&lt;br /&gt;
		 * contributor now.&lt;br /&gt;
		 */&lt;br /&gt;
		return Math.min(&lt;br /&gt;
			1,&lt;br /&gt;
			0.50 * colorDistance +&lt;br /&gt;
				0.25 * shapeDistance +&lt;br /&gt;
				0.15 * histogramDistance +&lt;br /&gt;
				0.10 * hashDistance&lt;br /&gt;
		);&lt;br /&gt;
	}&lt;br /&gt;
	function mapLimit( list, limit, worker ) {&lt;br /&gt;
		var nextIndex = 0;&lt;br /&gt;
		var workers = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		function runWorker() {&lt;br /&gt;
			var index;&lt;br /&gt;
&lt;br /&gt;
			if ( nextIndex &amp;gt;= list.length ) {&lt;br /&gt;
				return Promise.resolve();&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			index = nextIndex++;&lt;br /&gt;
			return Promise.resolve( worker( list[ index ], index ) )&lt;br /&gt;
				.then( runWorker );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; Math.min( limit, list.length ); i++ ) {&lt;br /&gt;
			workers.push( runWorker() );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return Promise.all( workers );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildFingerprintIndex( records, setStatus ) {&lt;br /&gt;
		var complete = 0;&lt;br /&gt;
		var successful = [];&lt;br /&gt;
		var grouped = Object.create( null );&lt;br /&gt;
		var groups = [];&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * If several icon filenames are byte-identical, imageinfo gives them&lt;br /&gt;
		 * the same SHA-1. Fingerprint that image only once and fan the result&lt;br /&gt;
		 * back out to every filename. This can substantially reduce the first&lt;br /&gt;
		 * build&#039;s static-file traffic without changing the visible results.&lt;br /&gt;
		 */&lt;br /&gt;
		records.forEach( function ( record ) {&lt;br /&gt;
			var key = record.sha1 || record.url;&lt;br /&gt;
&lt;br /&gt;
			if ( !grouped[ key ] ) {&lt;br /&gt;
				grouped[ key ] = {&lt;br /&gt;
					key: key,&lt;br /&gt;
					representative: record,&lt;br /&gt;
					records: []&lt;br /&gt;
				};&lt;br /&gt;
				groups.push( grouped[ key ] );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			grouped[ key ].records.push( record );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return mapLimit(&lt;br /&gt;
			groups,&lt;br /&gt;
			DOWNLOAD_CONCURRENCY,&lt;br /&gt;
			function ( group ) {&lt;br /&gt;
				return fetchDrawable( group.representative.url )&lt;br /&gt;
					.then( function ( drawable ) {&lt;br /&gt;
						var fp = fingerprintDrawable( drawable, 0 );&lt;br /&gt;
&lt;br /&gt;
						closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
						group.records.forEach( function ( record ) {&lt;br /&gt;
							successful.push( {&lt;br /&gt;
								title: record.title,&lt;br /&gt;
								url: record.url,&lt;br /&gt;
								sha1: record.sha1,&lt;br /&gt;
								width: record.width,&lt;br /&gt;
								height: record.height,&lt;br /&gt;
								hashHi: fp.hashHi,&lt;br /&gt;
								hashLo: fp.hashLo,&lt;br /&gt;
								rgb: fp.rgb&lt;br /&gt;
							} );&lt;br /&gt;
						} );&lt;br /&gt;
					} )&lt;br /&gt;
					.catch( function () {&lt;br /&gt;
						/*&lt;br /&gt;
						 * A missing/corrupt/non-raster file should not prevent&lt;br /&gt;
						 * the rest of the icon index from becoming usable.&lt;br /&gt;
						 */&lt;br /&gt;
					} )&lt;br /&gt;
					.then( function () {&lt;br /&gt;
						complete++;&lt;br /&gt;
&lt;br /&gt;
						if (&lt;br /&gt;
							setStatus &amp;amp;&amp;amp;&lt;br /&gt;
							(&lt;br /&gt;
								complete === groups.length ||&lt;br /&gt;
								complete % 25 === 0&lt;br /&gt;
							)&lt;br /&gt;
						) {&lt;br /&gt;
							setStatus(&lt;br /&gt;
								&#039;Building pixel index… &#039; +&lt;br /&gt;
									complete + &#039; / &#039; + groups.length +&lt;br /&gt;
									&#039; unique images (&#039; +&lt;br /&gt;
									records.length + &#039; filenames)&#039;&lt;br /&gt;
							);&lt;br /&gt;
						}&lt;br /&gt;
					} );&lt;br /&gt;
			}&lt;br /&gt;
		).then( function () {&lt;br /&gt;
			return successful;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cacheKey( revid ) {&lt;br /&gt;
		return [&lt;br /&gt;
			&#039;icon-list&#039;,&lt;br /&gt;
			ALGO_VERSION,&lt;br /&gt;
			revid&lt;br /&gt;
		].join( &#039;:&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function decodeServerRgb( base64 ) {&lt;br /&gt;
		var binary = window.atob( String( base64 || &#039;&#039; ) );&lt;br /&gt;
		var bytes = new Uint8Array( binary.length );&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; binary.length; i++ ) {&lt;br /&gt;
			bytes[ i ] = binary.charCodeAt( i ) &amp;amp; 0xFF;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return bytes;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function decodeServerRecords( records ) {&lt;br /&gt;
		return ( Array.isArray( records ) ? records : [] )&lt;br /&gt;
			.map( function ( record ) {&lt;br /&gt;
				return {&lt;br /&gt;
					title: String( record.title || &#039;&#039; ),&lt;br /&gt;
					url: String( record.url || &#039;&#039; ),&lt;br /&gt;
					sha1: String( record.sha1 || &#039;&#039; ),&lt;br /&gt;
					width: Number( record.width ) || 0,&lt;br /&gt;
					height: Number( record.height ) || 0,&lt;br /&gt;
					hashHi: Number( record.hashHi ) &amp;gt;&amp;gt;&amp;gt; 0,&lt;br /&gt;
					hashLo: Number( record.hashLo ) &amp;gt;&amp;gt;&amp;gt; 0,&lt;br /&gt;
					rgb: decodeServerRgb( record.rgb )&lt;br /&gt;
				};&lt;br /&gt;
			} )&lt;br /&gt;
			.filter( function ( record ) {&lt;br /&gt;
				return (&lt;br /&gt;
					record.title &amp;amp;&amp;amp;&lt;br /&gt;
					record.url &amp;amp;&amp;amp;&lt;br /&gt;
					record.rgb.length&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchServerIconIndexMeta( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;eqliconindex&#039;,&lt;br /&gt;
			meta: 1,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			return (&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.eqliconindex&lt;br /&gt;
			)&lt;br /&gt;
				? data.eqliconindex&lt;br /&gt;
				: null;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchServerIconIndex( api, generation ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;eqliconindex&#039;,&lt;br /&gt;
			generation: generation,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			return (&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.eqliconindex&lt;br /&gt;
			)&lt;br /&gt;
				? data.eqliconindex&lt;br /&gt;
				: null;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function serverCacheKey( generation ) {&lt;br /&gt;
		return [&lt;br /&gt;
			&#039;server&#039;,&lt;br /&gt;
			ALGO_VERSION,&lt;br /&gt;
			String( generation || &#039;&#039; )&lt;br /&gt;
		].join( &#039;:&#039; );&lt;br /&gt;
	}&lt;br /&gt;
	function getOrBuildIndex( api, setStatus ) {&lt;br /&gt;
		if ( memoryIndex ) {&lt;br /&gt;
			return Promise.resolve( memoryIndex );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		setStatus( &#039;Checking server icon index…&#039; );&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Preferred path:&lt;br /&gt;
		 * 1. Ask for tiny server metadata.&lt;br /&gt;
		 * 2. Reuse IndexedDB if this immutable generation is already local.&lt;br /&gt;
		 * 3. Otherwise download the precomputed server fingerprints once.&lt;br /&gt;
		 *&lt;br /&gt;
		 * Any server-cache error falls through to the original fully&lt;br /&gt;
		 * client-side builder, preserving continuity.&lt;br /&gt;
		 */&lt;br /&gt;
		return fetchServerIconIndexMeta( api )&lt;br /&gt;
			.catch( function () {&lt;br /&gt;
				return null;&lt;br /&gt;
			} )&lt;br /&gt;
			.then( function ( meta ) {&lt;br /&gt;
				var key;&lt;br /&gt;
&lt;br /&gt;
				if (&lt;br /&gt;
					!meta ||&lt;br /&gt;
					!meta.ready ||&lt;br /&gt;
					Number( meta.algorithm ) !== ALGO_VERSION ||&lt;br /&gt;
					!meta.generation&lt;br /&gt;
				) {&lt;br /&gt;
					return null;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				key = serverCacheKey( meta.generation );&lt;br /&gt;
&lt;br /&gt;
				return idbGet( key )&lt;br /&gt;
					.catch( function () {&lt;br /&gt;
						return null;&lt;br /&gt;
					} )&lt;br /&gt;
					.then( function ( cached ) {&lt;br /&gt;
						if (&lt;br /&gt;
							cached &amp;amp;&amp;amp;&lt;br /&gt;
							Array.isArray( cached.records ) &amp;amp;&amp;amp;&lt;br /&gt;
							cached.records.length&lt;br /&gt;
						) {&lt;br /&gt;
							memoryIndex = cached.records;&lt;br /&gt;
&lt;br /&gt;
							setStatus(&lt;br /&gt;
								&#039;Icon index ready: &#039; +&lt;br /&gt;
									memoryIndex.length +&lt;br /&gt;
									&#039; icons (server generation cached locally).&#039;&lt;br /&gt;
							);&lt;br /&gt;
&lt;br /&gt;
							return memoryIndex;&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Downloading precomputed server icon index…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						return fetchServerIconIndex(&lt;br /&gt;
							api,&lt;br /&gt;
							String( meta.generation )&lt;br /&gt;
						).then( function ( payload ) {&lt;br /&gt;
							var records;&lt;br /&gt;
&lt;br /&gt;
							if (&lt;br /&gt;
								!payload ||&lt;br /&gt;
								!payload.ready ||&lt;br /&gt;
								String( payload.generation || &#039;&#039; ) !==&lt;br /&gt;
									String( meta.generation )&lt;br /&gt;
							) {&lt;br /&gt;
								return null;&lt;br /&gt;
							}&lt;br /&gt;
&lt;br /&gt;
							records = decodeServerRecords(&lt;br /&gt;
								payload.records&lt;br /&gt;
							);&lt;br /&gt;
&lt;br /&gt;
							if ( !records.length ) {&lt;br /&gt;
								return null;&lt;br /&gt;
							}&lt;br /&gt;
&lt;br /&gt;
							memoryIndex = records;&lt;br /&gt;
&lt;br /&gt;
							setStatus(&lt;br /&gt;
								&#039;Icon index ready: &#039; +&lt;br /&gt;
									records.length +&lt;br /&gt;
									&#039; icons (server cache).&#039;&lt;br /&gt;
							);&lt;br /&gt;
&lt;br /&gt;
							return idbPutAndPrune( {&lt;br /&gt;
								key: key,&lt;br /&gt;
								generation: String( meta.generation ),&lt;br /&gt;
								revid: Number( meta.iconListRevision ) || 0,&lt;br /&gt;
								algorithm: ALGO_VERSION,&lt;br /&gt;
								builtAt: Date.now(),&lt;br /&gt;
								records: records&lt;br /&gt;
							} )&lt;br /&gt;
								.catch( function () {&lt;br /&gt;
									/* Browser storage is optional. */&lt;br /&gt;
								} )&lt;br /&gt;
								.then( function () {&lt;br /&gt;
									return records;&lt;br /&gt;
								} );&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
			} )&lt;br /&gt;
			.then( function ( serverRecords ) {&lt;br /&gt;
				if ( serverRecords &amp;amp;&amp;amp; serverRecords.length ) {&lt;br /&gt;
					return serverRecords;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				/*&lt;br /&gt;
				 * Backwards-compatible fallback. If the extension is disabled,&lt;br /&gt;
				 * the job queue has not run yet, GD is unavailable, or the&lt;br /&gt;
				 * server cache is stale, the working browser implementation&lt;br /&gt;
				 * behaves exactly as it did before.&lt;br /&gt;
				 */&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Server icon index unavailable; checking browser cache…&#039;&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				return fetchCatalog( api ).then( function ( catalog ) {&lt;br /&gt;
					var key = cacheKey( catalog.revid );&lt;br /&gt;
&lt;br /&gt;
					return idbGet( key )&lt;br /&gt;
						.catch( function () {&lt;br /&gt;
							return null;&lt;br /&gt;
						} )&lt;br /&gt;
						.then( function ( cached ) {&lt;br /&gt;
							if (&lt;br /&gt;
								cached &amp;amp;&amp;amp;&lt;br /&gt;
								Array.isArray( cached.records ) &amp;amp;&amp;amp;&lt;br /&gt;
								cached.records.length&lt;br /&gt;
							) {&lt;br /&gt;
								memoryIndex = cached.records;&lt;br /&gt;
								setStatus(&lt;br /&gt;
									&#039;Icon index ready: &#039; +&lt;br /&gt;
										memoryIndex.length +&lt;br /&gt;
										&#039; uploaded icons (cached locally).&#039;&lt;br /&gt;
								);&lt;br /&gt;
								return memoryIndex;&lt;br /&gt;
							}&lt;br /&gt;
&lt;br /&gt;
							return fetchImageInfo(&lt;br /&gt;
								api,&lt;br /&gt;
								catalog.titles,&lt;br /&gt;
								setStatus&lt;br /&gt;
							).then( function ( imageRecords ) {&lt;br /&gt;
								setStatus(&lt;br /&gt;
									&#039;Found &#039; + imageRecords.length +&lt;br /&gt;
										&#039; uploaded icons. Building pixel index…&#039;&lt;br /&gt;
								);&lt;br /&gt;
&lt;br /&gt;
								return buildFingerprintIndex(&lt;br /&gt;
									imageRecords,&lt;br /&gt;
									setStatus&lt;br /&gt;
								);&lt;br /&gt;
							} ).then( function ( records ) {&lt;br /&gt;
								memoryIndex = records;&lt;br /&gt;
&lt;br /&gt;
								setStatus(&lt;br /&gt;
									&#039;Icon index ready: &#039; +&lt;br /&gt;
										records.length +&lt;br /&gt;
										&#039; uploaded icons.&#039;&lt;br /&gt;
								);&lt;br /&gt;
&lt;br /&gt;
								return idbPutAndPrune( {&lt;br /&gt;
									key: key,&lt;br /&gt;
									revid: catalog.revid,&lt;br /&gt;
									algorithm: ALGO_VERSION,&lt;br /&gt;
									builtAt: Date.now(),&lt;br /&gt;
									records: records&lt;br /&gt;
								} )&lt;br /&gt;
									.catch( function () {&lt;br /&gt;
										/* Browser storage is optional. */&lt;br /&gt;
									} )&lt;br /&gt;
									.then( function () {&lt;br /&gt;
										return records;&lt;br /&gt;
									} );&lt;br /&gt;
							} );&lt;br /&gt;
						} );&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
	function fingerprintPastedImage( drawable ) {&lt;br /&gt;
		/*&lt;br /&gt;
		 * Small centered crop variants make the matcher forgiving of a&lt;br /&gt;
		 * 1-2 pixel screen-capture border while still preferring an exact crop.&lt;br /&gt;
		 */&lt;br /&gt;
		return [&lt;br /&gt;
			fingerprintDrawable( drawable, 0 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.035 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.07 )&lt;br /&gt;
		];&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function rankMatches( targetFingerprints, records ) {&lt;br /&gt;
		var ranked = records.map( function ( record ) {&lt;br /&gt;
			var candidate = {&lt;br /&gt;
				hashHi: record.hashHi,&lt;br /&gt;
				hashLo: record.hashLo,&lt;br /&gt;
				rgb: record.rgb&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			var distance = Infinity;&lt;br /&gt;
&lt;br /&gt;
			targetFingerprints.forEach( function ( target ) {&lt;br /&gt;
				distance = Math.min(&lt;br /&gt;
					distance,&lt;br /&gt;
					fingerprintDistance( target, candidate )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				record: record,&lt;br /&gt;
				distance: distance,&lt;br /&gt;
				similarity: Math.max(&lt;br /&gt;
					0,&lt;br /&gt;
					Math.min( 100, ( 1 - distance ) * 100 )&lt;br /&gt;
				)&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		ranked.sort( function ( a, b ) {&lt;br /&gt;
			return a.distance - b.distance;&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return ranked.slice( 0, RESULT_COUNT );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cleanDisplayTitle( title ) {&lt;br /&gt;
		return String( title || &#039;&#039; ).replace( /^File:/i, &#039;&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function iconMetadata( title ) {&lt;br /&gt;
		var clean = cleanDisplayTitle( title );&lt;br /&gt;
		var itemMatch = clean.match( /^Item\s+(\d+)\.png$/i );&lt;br /&gt;
		var spellMatch = clean.match( /^Spellicon_([^.]*)\.png$/i );&lt;br /&gt;
		var id = &#039;&#039;;&lt;br /&gt;
		var kind = &#039;Icon file&#039;;&lt;br /&gt;
		var parameter = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		if ( itemMatch ) {&lt;br /&gt;
			id = itemMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Item icon&#039;;&lt;br /&gt;
			parameter = &#039;|lucy_img_ID = &#039; + id;&lt;br /&gt;
		} else if ( spellMatch ) {&lt;br /&gt;
			id = spellMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Spell icon&#039;;&lt;br /&gt;
			parameter = &#039;|spellicon = &#039; + id;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			kind: kind,&lt;br /&gt;
			id: id,&lt;br /&gt;
			filename: clean,&lt;br /&gt;
			parameter: parameter,&lt;br /&gt;
			rawWiki: &#039;[[File:&#039; + clean + &#039;]]&#039;,&lt;br /&gt;
			staticWiki: &#039;[[File:&#039; + clean + &#039;|32x32px]]&#039;,&lt;br /&gt;
			scaledWiki: &#039;[[File:&#039; + clean + &#039;|frameless|upright=1.5]]&#039;&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function copyText( text, button ) {&lt;br /&gt;
		var original = button.textContent;&lt;br /&gt;
&lt;br /&gt;
		function done() {&lt;br /&gt;
			button.textContent = &#039;Copied&#039;;&lt;br /&gt;
			window.setTimeout( function () {&lt;br /&gt;
				button.textContent = original;&lt;br /&gt;
			}, 1000 );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			navigator.clipboard &amp;amp;&amp;amp;&lt;br /&gt;
			typeof navigator.clipboard.writeText === &#039;function&#039;&lt;br /&gt;
		) {&lt;br /&gt;
			navigator.clipboard.writeText( text )&lt;br /&gt;
				.then( done )&lt;br /&gt;
				.catch( function () {&lt;br /&gt;
					window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
				} );&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeCopyButton( label, value ) {&lt;br /&gt;
		var button = document.createElement( &#039;button&#039; );&lt;br /&gt;
&lt;br /&gt;
		button.className = &#039;eql-iconfinder-copy&#039;;&lt;br /&gt;
		button.type = &#039;button&#039;;&lt;br /&gt;
		button.textContent = label;&lt;br /&gt;
&lt;br /&gt;
		button.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			copyText( value, button );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return button;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function renderMatches( container, matches ) {&lt;br /&gt;
		container.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
		var grid = document.createElement( &#039;div&#039; );&lt;br /&gt;
		grid.className = &#039;eql-iconfinder-results-grid&#039;;&lt;br /&gt;
		container.appendChild( grid );&lt;br /&gt;
&lt;br /&gt;
		matches.forEach( function ( match ) {&lt;br /&gt;
			var record = match.record;&lt;br /&gt;
			var meta = iconMetadata( record.title );&lt;br /&gt;
			var card = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var img = document.createElement( &#039;img&#039; );&lt;br /&gt;
			var textWrap = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var link = document.createElement( &#039;a&#039; );&lt;br /&gt;
			var score = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var iconId = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var metadata = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var copyRow = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
			card.className = &#039;eql-iconfinder-result&#039;;&lt;br /&gt;
&lt;br /&gt;
			img.src = record.url;&lt;br /&gt;
			img.alt = meta.filename;&lt;br /&gt;
			img.loading = &#039;lazy&#039;;&lt;br /&gt;
&lt;br /&gt;
			link.className = &#039;eql-iconfinder-result-name&#039;;&lt;br /&gt;
			link.href = mw.util.getUrl( record.title );&lt;br /&gt;
			link.target = &#039;_blank&#039;;&lt;br /&gt;
			link.rel = &#039;noopener&#039;;&lt;br /&gt;
			link.textContent = meta.filename;&lt;br /&gt;
&lt;br /&gt;
			score.className = &#039;eql-iconfinder-result-score&#039;;&lt;br /&gt;
			score.textContent =&lt;br /&gt;
				&#039;Match score &#039; + match.similarity.toFixed( 1 );&lt;br /&gt;
&lt;br /&gt;
			iconId.className = &#039;eql-iconfinder-result-id&#039;;&lt;br /&gt;
			iconId.textContent = meta.id&lt;br /&gt;
				? &#039;Icon &#039; + meta.id&lt;br /&gt;
				: meta.filename;&lt;br /&gt;
&lt;br /&gt;
			metadata.className = &#039;eql-iconfinder-result-meta&#039;;&lt;br /&gt;
			metadata.textContent = meta.kind;&lt;br /&gt;
&lt;br /&gt;
			copyRow.className = &#039;eql-iconfinder-copy-row&#039;;&lt;br /&gt;
&lt;br /&gt;
			if ( meta.id ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy ID&#039;, meta.id )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if ( meta.parameter ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy Parameter&#039;, meta.parameter )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Raw Wikicode&#039;, meta.rawWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;32×32&#039;, meta.staticWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Scaled&#039;, meta.scaledWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			textWrap.appendChild( link );&lt;br /&gt;
			textWrap.appendChild( iconId );&lt;br /&gt;
			textWrap.appendChild( score );&lt;br /&gt;
			textWrap.appendChild( metadata );&lt;br /&gt;
			textWrap.appendChild( copyRow );&lt;br /&gt;
&lt;br /&gt;
			card.appendChild( img );&lt;br /&gt;
			card.appendChild( textWrap );&lt;br /&gt;
			grid.appendChild( card );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		var note = document.createElement( &#039;div&#039; );&lt;br /&gt;
		note.className = &#039;eql-iconfinder-note&#039;;&lt;br /&gt;
		note.textContent =&lt;br /&gt;
			&#039;The tool only identifies likely matches and copies values/wikicode to the clipboard; it never changes the editor. &#039; +&lt;br /&gt;
			&#039;For the best result, capture only the square icon with as little surrounding UI as possible.&#039;;&lt;br /&gt;
		container.appendChild( note );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function imageFromClipboardEvent( event ) {&lt;br /&gt;
		var clipboard = event.clipboardData;&lt;br /&gt;
		var items;&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		if ( !clipboard || !clipboard.items ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		items = clipboard.items;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; items.length; i++ ) {&lt;br /&gt;
			if (&lt;br /&gt;
				items[ i ].kind === &#039;file&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				/^image\//i.test( items[ i ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				return items[ i ].getAsFile();&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return null;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setPreview( img, blob ) {&lt;br /&gt;
		if ( currentPreviewUrl ) {&lt;br /&gt;
			URL.revokeObjectURL( currentPreviewUrl );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		currentPreviewUrl = URL.createObjectURL( blob );&lt;br /&gt;
		img.src = currentPreviewUrl;&lt;br /&gt;
		img.classList.add( &#039;is-visible&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function processBlob( blob, preview, status, results, api ) {&lt;br /&gt;
		var setStatus = function ( text ) {&lt;br /&gt;
			status.textContent = text;&lt;br /&gt;
		};&lt;br /&gt;
&lt;br /&gt;
		setPreview( preview, blob );&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Analyzing pasted icon…&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		return blobToDrawable( blob )&lt;br /&gt;
			.then( function ( drawable ) {&lt;br /&gt;
				var targets = fingerprintPastedImage( drawable );&lt;br /&gt;
				closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
				return getOrBuildIndex( api, setStatus )&lt;br /&gt;
					.then( function ( index ) {&lt;br /&gt;
						var matches;&lt;br /&gt;
&lt;br /&gt;
						if ( !index.length ) {&lt;br /&gt;
							throw new Error( &#039;No uploaded icons were found.&#039; );&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Comparing against &#039; +&lt;br /&gt;
								index.length +&lt;br /&gt;
								&#039; uploaded icons…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						/*&lt;br /&gt;
						 * Yield once so the status message can paint before&lt;br /&gt;
						 * ranking several thousand in-memory fingerprints.&lt;br /&gt;
						 */&lt;br /&gt;
						return new Promise( function ( resolve ) {&lt;br /&gt;
							window.setTimeout( function () {&lt;br /&gt;
								matches = rankMatches( targets, index );&lt;br /&gt;
								resolve( matches );&lt;br /&gt;
							}, 0 );&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
			} )&lt;br /&gt;
			.then( function ( matches ) {&lt;br /&gt;
				renderMatches( results, matches );&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Done. Showing the &#039; +&lt;br /&gt;
						matches.length +&lt;br /&gt;
						&#039; closest matches.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				results.innerHTML =&lt;br /&gt;
					&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
						&#039;The icon could not be matched.&#039; +&lt;br /&gt;
					&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Icon Finder error: &#039; +&lt;br /&gt;
						( error &amp;amp;&amp;amp; error.message ? error.message : &#039;Unknown error&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildUi( host ) {&lt;br /&gt;
		var api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		host.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		var shell = document.createElement( &#039;div&#039; );&lt;br /&gt;
		shell.className = &#039;eql-iconfinder-shell&#039;;&lt;br /&gt;
&lt;br /&gt;
		var left = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var paste = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var instruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var subinstruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var preview = document.createElement( &#039;img&#039; );&lt;br /&gt;
		var status = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		paste.className = &#039;eql-iconfinder-paste&#039;;&lt;br /&gt;
		paste.tabIndex = 0;&lt;br /&gt;
		paste.setAttribute( &#039;role&#039;, &#039;button&#039; );&lt;br /&gt;
		paste.setAttribute(&lt;br /&gt;
			&#039;aria-label&#039;,&lt;br /&gt;
			&#039;Paste a screen capture of an EverQuest icon&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		instruction.className = &#039;eql-iconfinder-instruction&#039;;&lt;br /&gt;
		instruction.textContent =&lt;br /&gt;
			&#039;Screen capture and paste JUST the Icon&#039;;&lt;br /&gt;
&lt;br /&gt;
		subinstruction.className = &#039;eql-iconfinder-subinstruction&#039;;&lt;br /&gt;
		subinstruction.textContent =&lt;br /&gt;
			&#039;Click this box, then press Ctrl+V. You can also drag an image here.&#039;;&lt;br /&gt;
&lt;br /&gt;
		preview.className = &#039;eql-iconfinder-preview&#039;;&lt;br /&gt;
		preview.alt = &#039;Pasted icon preview&#039;;&lt;br /&gt;
&lt;br /&gt;
		status.className = &#039;eql-iconfinder-status&#039;;&lt;br /&gt;
		status.setAttribute( &#039;aria-live&#039;, &#039;polite&#039; );&lt;br /&gt;
		status.textContent =&lt;br /&gt;
			&#039;Nothing is loaded from Icon List until you paste an image.&#039;;&lt;br /&gt;
&lt;br /&gt;
		paste.appendChild( instruction );&lt;br /&gt;
		paste.appendChild( subinstruction );&lt;br /&gt;
		paste.appendChild( preview );&lt;br /&gt;
&lt;br /&gt;
		left.appendChild( paste );&lt;br /&gt;
		left.appendChild( status );&lt;br /&gt;
&lt;br /&gt;
		var right = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var resultsTitle = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var results = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		resultsTitle.className = &#039;eql-iconfinder-results-title&#039;;&lt;br /&gt;
		resultsTitle.textContent = &#039;Possible Matches&#039;;&lt;br /&gt;
&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Paste an icon to search the uploaded images on Icon List.&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		right.appendChild( resultsTitle );&lt;br /&gt;
		right.appendChild( results );&lt;br /&gt;
&lt;br /&gt;
		shell.appendChild( left );&lt;br /&gt;
		shell.appendChild( right );&lt;br /&gt;
		host.appendChild( shell );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;paste&#039;, function ( event ) {&lt;br /&gt;
			var file = imageFromClipboardEvent( event );&lt;br /&gt;
&lt;br /&gt;
			if ( !file ) {&lt;br /&gt;
				status.textContent =&lt;br /&gt;
					&#039;Clipboard does not contain an image. Screen capture the icon, then paste again.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				file,&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;dragover&#039;, function ( event ) {&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;drop&#039;, function ( event ) {&lt;br /&gt;
			var files;&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
			files = event.dataTransfer &amp;amp;&amp;amp; event.dataTransfer.files;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				!files ||&lt;br /&gt;
				!files.length ||&lt;br /&gt;
				!/^image\//i.test( files[ 0 ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				status.textContent = &#039;Drop an image file here.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				files[ 0 ],&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Focus the paste target immediately when the panel first opens so&lt;br /&gt;
		 * the normal workflow is: click Icon Finder -&amp;gt; Ctrl+V.&lt;br /&gt;
		 */&lt;br /&gt;
		window.setTimeout( function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		}, 0 );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	window.EQLIconFinder = {&lt;br /&gt;
		init: function ( host ) {&lt;br /&gt;
			if ( !host ) {&lt;br /&gt;
				return Promise.reject(&lt;br /&gt;
					new Error( &#039;Icon Finder host element is missing.&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			addStyles();&lt;br /&gt;
&lt;br /&gt;
			if ( initializedHost !== host ) {&lt;br /&gt;
				buildUi( host );&lt;br /&gt;
				initializedHost = host;&lt;br /&gt;
			} else {&lt;br /&gt;
				var paste = host.querySelector( &#039;.eql-iconfinder-paste&#039; );&lt;br /&gt;
&lt;br /&gt;
				if ( paste ) {&lt;br /&gt;
					window.setTimeout( function () {&lt;br /&gt;
						paste.focus();&lt;br /&gt;
					}, 0 );&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return Promise.resolve();&lt;br /&gt;
		}&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168735</id>
		<title>MediaWiki:BlueprintLoader.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168735"/>
		<updated>2026-08-10T06:01:08Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Refresh Icon Finder UI script&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Editor Tools / Dynamic Blueprint Loader&lt;br /&gt;
 *&lt;br /&gt;
 * Source-editor behavior:&lt;br /&gt;
 * - On a new, truly empty page: show the Help:Contents Blueprint selector.&lt;br /&gt;
 * - On every source edit page: show the Icon Finder button.&lt;br /&gt;
 * - Heavy Icon Finder logic is lazy-loaded only when opened.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var BLUEPRINT_PAGE = &#039;Help:Contents&#039;;&lt;br /&gt;
	var TOOLS_ID = &#039;eql-editor-tools&#039;;&lt;br /&gt;
	var ICON_FINDER_PAGE = &#039;MediaWiki:IconFinder.js&#039;;&lt;br /&gt;
&lt;br /&gt;
	function htmlToText( html ) {&lt;br /&gt;
		var div = document.createElement( &#039;div&#039; );&lt;br /&gt;
		div.innerHTML = String( html || &#039;&#039; );&lt;br /&gt;
		return ( div.textContent || div.innerText || &#039;&#039; ).trim();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeLabel( heading ) {&lt;br /&gt;
		return heading&lt;br /&gt;
			.replace( /\s+Blueprints?\s*$/i, &#039;&#039; )&lt;br /&gt;
			.trim() + &#039; Blueprint&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getEditorContents( $textarea ) {&lt;br /&gt;
		try {&lt;br /&gt;
			return String( $textarea.textSelection( &#039;getContents&#039; ) || &#039;&#039; );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			return String( $textarea.val() || &#039;&#039; );&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setEditorContents( $textarea, text ) {&lt;br /&gt;
		try {&lt;br /&gt;
			$textarea.textSelection( &#039;setContents&#039;, text );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			$textarea.val( text );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea.trigger( &#039;input&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;change&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;focus&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function extractBlueprint( sectionText ) {&lt;br /&gt;
		var match;&lt;br /&gt;
		var blueprint;&lt;br /&gt;
&lt;br /&gt;
		sectionText = String( sectionText || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		match = sectionText.match(&lt;br /&gt;
			/&amp;lt;pre(?:\s[^&amp;gt;]*)?&amp;gt;([\s\S]*?)&amp;lt;\/pre&amp;gt;/i&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		if ( !match ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		blueprint = match[ 1 ];&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint.replace(&lt;br /&gt;
			/^\s*&amp;lt;nowiki&amp;gt;([\s\S]*?)&amp;lt;\/nowiki&amp;gt;\s*$/i,&lt;br /&gt;
			&#039;$1&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint&lt;br /&gt;
			.replace( /^\s*\n/, &#039;&#039; )&lt;br /&gt;
			.replace( /\s+$/, &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		return blueprint + &#039;\n&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintSections( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;tocdata&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var tocdata =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse.tocdata;&lt;br /&gt;
&lt;br /&gt;
			var sections =&lt;br /&gt;
				tocdata &amp;amp;&amp;amp;&lt;br /&gt;
				Array.isArray( tocdata.sections )&lt;br /&gt;
					? tocdata.sections&lt;br /&gt;
					: [];&lt;br /&gt;
&lt;br /&gt;
			if ( sections.length ) {&lt;br /&gt;
				return sections;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return api.get( {&lt;br /&gt;
				action: &#039;parse&#039;,&lt;br /&gt;
				page: BLUEPRINT_PAGE,&lt;br /&gt;
				prop: &#039;sections&#039;,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			} ).then( function ( fallbackData ) {&lt;br /&gt;
				return (&lt;br /&gt;
					fallbackData &amp;amp;&amp;amp;&lt;br /&gt;
					fallbackData.parse &amp;amp;&amp;amp;&lt;br /&gt;
					Array.isArray( fallbackData.parse.sections )&lt;br /&gt;
				)&lt;br /&gt;
					? fallbackData.parse.sections&lt;br /&gt;
					: [];&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeBlueprintSections( sections ) {&lt;br /&gt;
		return sections&lt;br /&gt;
			.map( function ( section ) {&lt;br /&gt;
				var heading = htmlToText( section.line || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
				return {&lt;br /&gt;
					heading: heading,&lt;br /&gt;
					label: makeLabel( heading ),&lt;br /&gt;
					index: String( section.index || &#039;&#039; ),&lt;br /&gt;
					anchor:&lt;br /&gt;
						section.linkAnchor ||&lt;br /&gt;
						section.anchor ||&lt;br /&gt;
						&#039;&#039;&lt;br /&gt;
				};&lt;br /&gt;
			} )&lt;br /&gt;
			.filter( function ( section ) {&lt;br /&gt;
				return (&lt;br /&gt;
					section.index &amp;amp;&amp;amp;&lt;br /&gt;
					/\bBlueprints?\s*$/i.test( section.heading )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintWikitext( api, sectionIndex ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;wikitext&#039;,&lt;br /&gt;
			section: sectionIndex,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var wikitext =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse&lt;br /&gt;
					? data.parse.wikitext&lt;br /&gt;
					: &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				wikitext &amp;amp;&amp;amp;&lt;br /&gt;
				typeof wikitext === &#039;object&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				Object.prototype.hasOwnProperty.call( wikitext, &#039;*&#039; )&lt;br /&gt;
			) {&lt;br /&gt;
				wikitext = wikitext[ &#039;*&#039; ];&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return String( wikitext || &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-editor-tools-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-editor-tools-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039;{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;margin:0 0 1rem 0;&#039; +&lt;br /&gt;
					&#039;padding:1rem 1.1rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,rgba(16,21,29,.97),rgba(9,13,19,.98));&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.38);&#039; +&lt;br /&gt;
					&#039;border-left:5px solid rgba(216,183,92,.82);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 9px 24px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.035);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(0,1fr) auto;&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:end;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-title,&#039; +&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .35rem 0;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;letter-spacing:.035em;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-description{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .7rem 0;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-controls{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;min-width:260px;&#039; +&lt;br /&gt;
					&#039;max-width:100%;&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .55rem;&#039; +&lt;br /&gt;
					&#039;background:#0b1017;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.36);&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#e2e2cf;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button{&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .85rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,#806d32,#594817);&#039; +&lt;br /&gt;
					&#039;border:1px solid #b69c4b;&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#fff5cc;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;cursor:pointer;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:hover:not(:disabled){&#039; +&lt;br /&gt;
					&#039;filter:brightness(1.08);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:disabled{&#039; +&lt;br /&gt;
					&#039;cursor:default;&#039; +&lt;br /&gt;
					&#039;opacity:.55;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-source{&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-status{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-top:.55rem;&#039; +&lt;br /&gt;
					&#039;min-height:1.2em;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:stretch;&#039; +&lt;br /&gt;
					&#039;justify-content:flex-end;&#039; +&lt;br /&gt;
					&#039;min-width:150px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-toggle{&#039; +&lt;br /&gt;
					&#039;white-space:nowrap;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host{&#039; +&lt;br /&gt;
					&#039;margin-top:1rem;&#039; +&lt;br /&gt;
					&#039;padding-top:1rem;&#039; +&lt;br /&gt;
					&#039;border-top:1px solid rgba(216,183,92,.22);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host[hidden]{display:none!important;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:760px){&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
						&#039;grid-template-columns:1fr;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
						&#039;width:100%;&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function loadIconFinderScript() {&lt;br /&gt;
		if ( window.EQLIconFinder ) {&lt;br /&gt;
			return Promise.resolve( window.EQLIconFinder );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( window.eqlIconFinderLoadingPromise ) {&lt;br /&gt;
			return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.eqlIconFinderLoadingPromise = new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var script = document.createElement( &#039;script&#039; );&lt;br /&gt;
&lt;br /&gt;
			script.src = mw.util.getUrl( ICON_FINDER_PAGE, {&lt;br /&gt;
				action: &#039;raw&#039;,&lt;br /&gt;
				ctype: &#039;text/javascript&#039;,&lt;br /&gt;
				v: &#039;7&#039;&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			script.async = true;&lt;br /&gt;
&lt;br /&gt;
			script.onload = function () {&lt;br /&gt;
				if ( window.EQLIconFinder ) {&lt;br /&gt;
					resolve( window.EQLIconFinder );&lt;br /&gt;
				} else {&lt;br /&gt;
					reject( new Error( &#039;IconFinder.js loaded without exposing EQLIconFinder.&#039; ) );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			script.onerror = function () {&lt;br /&gt;
				reject( new Error( &#039;Could not load IconFinder.js.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			document.head.appendChild( script );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildIconFinderLaunch( $row, $tools ) {&lt;br /&gt;
		var $launch = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-launch&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-title&#039;,&lt;br /&gt;
			text: &#039;ICON FINDER&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-toggle&#039;,&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Icon Finder&#039;,&lt;br /&gt;
			&#039;aria-expanded&#039;: &#039;false&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $host = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-host&#039;,&lt;br /&gt;
			hidden: true&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var opening = $host.prop( &#039;hidden&#039; );&lt;br /&gt;
&lt;br /&gt;
			if ( !opening ) {&lt;br /&gt;
				$host.prop( &#039;hidden&#039;, true );&lt;br /&gt;
				$button.attr( &#039;aria-expanded&#039;, &#039;false&#039; );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$button.text( &#039;Loading…&#039; );&lt;br /&gt;
&lt;br /&gt;
			loadIconFinderScript()&lt;br /&gt;
				.then( function ( finder ) {&lt;br /&gt;
					return finder.init( $host[ 0 ] );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$host.prop( &#039;hidden&#039;, false );&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$host&lt;br /&gt;
						.prop( &#039;hidden&#039;, false )&lt;br /&gt;
						.text( &#039;Icon Finder could not be loaded.&#039; );&lt;br /&gt;
&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.text( &#039;Icon Finder&#039; );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildBlueprintArea( $row, $textarea, api ) {&lt;br /&gt;
		var $area = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-area&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-title&#039;,&lt;br /&gt;
			text: &#039;CREATE FROM A BLUEPRINT&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-description&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;Load a standard page skeleton from Help:Contents, &#039; +&lt;br /&gt;
				&#039;or continue editing the current source below.&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $controls = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-controls&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $select = $( &#039;&amp;lt;select&amp;gt;&#039;, {&lt;br /&gt;
			&#039;aria-label&#039;: &#039;Select page blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
			value: &#039;&#039;,&lt;br /&gt;
			text: &#039;Loading blueprints...&#039;&lt;br /&gt;
		} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Load Blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $sourceLink = $( &#039;&amp;lt;a&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-source&#039;,&lt;br /&gt;
			href: mw.util.getUrl( BLUEPRINT_PAGE ),&lt;br /&gt;
			text: &#039;View Blueprints&#039;,&lt;br /&gt;
			target: &#039;_blank&#039;,&lt;br /&gt;
			rel: &#039;noopener&#039;&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $status = $( &#039;&amp;lt;span&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-status&#039;,&lt;br /&gt;
			&#039;aria-live&#039;: &#039;polite&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		fetchBlueprintSections( api )&lt;br /&gt;
			.then( function ( sections ) {&lt;br /&gt;
				var blueprints = normalizeBlueprintSections( sections );&lt;br /&gt;
&lt;br /&gt;
				$select.empty();&lt;br /&gt;
&lt;br /&gt;
				$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
					value: &#039;&#039;,&lt;br /&gt;
					text: blueprints.length ?&lt;br /&gt;
						&#039;Select a blueprint...&#039; :&lt;br /&gt;
						&#039;No blueprints found&#039;&lt;br /&gt;
				} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
				blueprints.forEach( function ( blueprint ) {&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: blueprint.index,&lt;br /&gt;
						text: blueprint.label&lt;br /&gt;
					} )&lt;br /&gt;
						.attr( &#039;data-anchor&#039;, blueprint.anchor )&lt;br /&gt;
						.appendTo( $select );&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
				$select.prop( &#039;disabled&#039;, !blueprints.length );&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				$select.empty().append(&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: &#039;&#039;,&lt;br /&gt;
						text: &#039;Could not load blueprints&#039;&lt;br /&gt;
					} )&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				$status.text(&lt;br /&gt;
					&#039;Could not read Blueprint headings from Help:Contents.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
		$select.on( &#039;change&#039;, function () {&lt;br /&gt;
			var $option = $select.find( &#039;option:selected&#039; );&lt;br /&gt;
			var anchor = $option.attr( &#039;data-anchor&#039; ) || &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
&lt;br /&gt;
			if ( anchor ) {&lt;br /&gt;
				$sourceLink.attr(&lt;br /&gt;
					&#039;href&#039;,&lt;br /&gt;
					mw.util.getUrl( BLUEPRINT_PAGE ) + &#039;#&#039; + anchor&lt;br /&gt;
				);&lt;br /&gt;
				$sourceLink.text( &#039;View This Blueprint&#039; );&lt;br /&gt;
			} else {&lt;br /&gt;
				$sourceLink.attr( &#039;href&#039;, mw.util.getUrl( BLUEPRINT_PAGE ) );&lt;br /&gt;
				$sourceLink.text( &#039;View Blueprints&#039; );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$status.text( &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var sectionIndex = $select.val();&lt;br /&gt;
			var existing;&lt;br /&gt;
&lt;br /&gt;
			if ( !sectionIndex ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			existing = getEditorContents( $textarea );&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				existing.trim() &amp;amp;&amp;amp;&lt;br /&gt;
				!window.confirm(&lt;br /&gt;
					&#039;The editor already contains text. Replace it with the selected blueprint?&#039;&lt;br /&gt;
				)&lt;br /&gt;
			) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$select.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$status.text( &#039;Loading current blueprint from Help:Contents...&#039; );&lt;br /&gt;
&lt;br /&gt;
			fetchBlueprintWikitext( api, sectionIndex )&lt;br /&gt;
				.then( function ( sectionText ) {&lt;br /&gt;
					var blueprint = extractBlueprint( sectionText );&lt;br /&gt;
&lt;br /&gt;
					if ( blueprint === null ) {&lt;br /&gt;
						throw new Error( &#039;No &amp;lt;pre&amp;gt; block found.&#039; );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					setEditorContents( $textarea, blueprint );&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Blueprint loaded. Edit the fields below, then publish normally.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Could not load this blueprint. Check Help:Contents and make sure the section contains a &amp;lt;pre&amp;gt; block.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$select.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function init() {&lt;br /&gt;
		var action = mw.config.get( &#039;wgAction&#039; );&lt;br /&gt;
		var $textarea;&lt;br /&gt;
		var $tools;&lt;br /&gt;
		var $row;&lt;br /&gt;
		var $editorHost;&lt;br /&gt;
		var api;&lt;br /&gt;
&lt;br /&gt;
		if ( [ &#039;edit&#039;, &#039;submit&#039; ].indexOf( action ) === -1 ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea = $( &#039;#wpTextbox1&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$textarea.length ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( document.getElementById( TOOLS_ID ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		addStyles();&lt;br /&gt;
&lt;br /&gt;
		$tools = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: TOOLS_ID&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$row = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-editor-tools-row&#039;&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		buildBlueprintArea( $row, $textarea, api );&lt;br /&gt;
&lt;br /&gt;
		buildIconFinderLaunch( $row, $tools );&lt;br /&gt;
&lt;br /&gt;
		$editorHost = $textarea.closest( &#039;.wikiEditor-ui&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$editorHost.length ) {&lt;br /&gt;
			$editorHost = $textarea;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$tools.insertBefore( $editorHost );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	mw.loader.using( [&lt;br /&gt;
		&#039;mediawiki.api&#039;,&lt;br /&gt;
		&#039;mediawiki.util&#039;,&lt;br /&gt;
		&#039;jquery.textSelection&#039;&lt;br /&gt;
	] ).then( function () {&lt;br /&gt;
		$( init );&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168734</id>
		<title>MediaWiki:IconFinder.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168734"/>
		<updated>2026-08-10T06:01:07Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Remove duplicate Icon Finder results heading&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Icon Finder&lt;br /&gt;
 *&lt;br /&gt;
 * Lazy-loaded from MediaWiki:BlueprintLoader.js.&lt;br /&gt;
 *&lt;br /&gt;
 * Matching workflow:&lt;br /&gt;
 * 1. User pastes a screen capture containing JUST the icon.&lt;br /&gt;
 * 2. Read the current image list from [[Icon List]].&lt;br /&gt;
 * 3. Resolve only existing uploaded files through imageinfo.&lt;br /&gt;
 * 4. Build compact perceptual fingerprints in the browser.&lt;br /&gt;
 * 5. Cache that fingerprint index in IndexedDB keyed to the Icon List revision.&lt;br /&gt;
 * 6. Rank possible matches by perceptual/pixel similarity.&lt;br /&gt;
 *&lt;br /&gt;
 * No icon-catalog requests or image downloads happen until the user actually&lt;br /&gt;
 * pastes an image into the Icon Finder.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var ICON_LIST_PAGE = &#039;Icon List&#039;;&lt;br /&gt;
	var DB_NAME = &#039;eql-icon-finder&#039;;&lt;br /&gt;
	var DB_VERSION = 1;&lt;br /&gt;
	var STORE_NAME = &#039;indexes&#039;;&lt;br /&gt;
	var ALGO_VERSION = 6;&lt;br /&gt;
	var COLOR_GRID = 16;&lt;br /&gt;
	var IMAGEINFO_BATCH = 50;&lt;br /&gt;
	var DOWNLOAD_CONCURRENCY = 8;&lt;br /&gt;
	var RESULT_COUNT = 10;&lt;br /&gt;
&lt;br /&gt;
	var initializedHost = null;&lt;br /&gt;
	var memoryIndex = null;&lt;br /&gt;
	var currentPreviewUrl = null;&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-icon-finder-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;.eql-iconfinder-shell{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(230px,.62fr) minmax(0,1.38fr);&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:start;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste{&#039; +&lt;br /&gt;
					&#039;min-height:160px;&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;justify-content:center;&#039; +&lt;br /&gt;
					&#039;gap:.45rem;&#039; +&lt;br /&gt;
					&#039;padding:1rem;&#039; +&lt;br /&gt;
					&#039;background:#080d13;&#039; +&lt;br /&gt;
					&#039;border:2px dashed rgba(216,183,92,.45);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;color:#dfe7ef;&#039; +&lt;br /&gt;
					&#039;text-align:center;&#039; +&lt;br /&gt;
					&#039;cursor:paste;&#039; +&lt;br /&gt;
					&#039;outline:none;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste:focus{&#039; +&lt;br /&gt;
					&#039;border-color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 0 0 2px rgba(216,183,92,.13);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-instruction{&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-subinstruction{&#039; +&lt;br /&gt;
					&#039;color:#9faaba;&#039; +&lt;br /&gt;
					&#039;font-size:.88rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview{&#039; +&lt;br /&gt;
					&#039;display:none;&#039; +&lt;br /&gt;
					&#039;width:72px;&#039; +&lt;br /&gt;
					&#039;height:72px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;image-rendering:auto;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.14);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview.is-visible{display:block;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-status{&#039; +&lt;br /&gt;
					&#039;margin-top:.65rem;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.4;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .55rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-empty{&#039; +&lt;br /&gt;
					&#039;padding:.9rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.025);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.08);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
					&#039;color:#96a2b2;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-grid{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:54px minmax(0,1fr);&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;padding:.65rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.03);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.09);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result img{&#039; +&lt;br /&gt;
					&#039;width:52px;&#039; +&lt;br /&gt;
					&#039;height:52px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.1);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-name{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.18rem;&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.2;&#039; +&lt;br /&gt;
					&#039;word-break:break-word;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-score{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.28rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-size:.8rem;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-meta{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;color:#8f9bac;&#039; +&lt;br /&gt;
					&#039;font-size:.76rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.25;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-id{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin:.2rem 0 .35rem;&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:800;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy-row{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;gap:.3rem;&#039; +&lt;br /&gt;
					&#039;margin-top:.38rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy{&#039; +&lt;br /&gt;
					&#039;min-height:1.8rem!important;&#039; +&lt;br /&gt;
					&#039;padding:.15rem .45rem!important;&#039; +&lt;br /&gt;
					&#039;font-size:.72rem!important;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-note{&#039; +&lt;br /&gt;
					&#039;margin-top:.7rem;&#039; +&lt;br /&gt;
					&#039;color:#7f8b9b;&#039; +&lt;br /&gt;
					&#039;font-size:.78rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:850px){&#039; +&lt;br /&gt;
					&#039;.eql-iconfinder-shell{grid-template-columns:1fr;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function openDb() {&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var request;&lt;br /&gt;
&lt;br /&gt;
			if ( !window.indexedDB ) {&lt;br /&gt;
				reject( new Error( &#039;IndexedDB unavailable.&#039; ) );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			request = indexedDB.open( DB_NAME, DB_VERSION );&lt;br /&gt;
&lt;br /&gt;
			request.onupgradeneeded = function () {&lt;br /&gt;
				var db = request.result;&lt;br /&gt;
&lt;br /&gt;
				if ( !db.objectStoreNames.contains( STORE_NAME ) ) {&lt;br /&gt;
					db.createObjectStore( STORE_NAME, {&lt;br /&gt;
						keyPath: &#039;key&#039;&lt;br /&gt;
					} );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onsuccess = function () {&lt;br /&gt;
				resolve( request.result );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onerror = function () {&lt;br /&gt;
				reject( request.error || new Error( &#039;IndexedDB open failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbGet( key ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readonly&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var request = store.get( key );&lt;br /&gt;
&lt;br /&gt;
				request.onsuccess = function () {&lt;br /&gt;
					resolve( request.result || null );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				request.onerror = function () {&lt;br /&gt;
					reject( request.error || new Error( &#039;IndexedDB read failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbPutAndPrune( value ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readwrite&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var cursorRequest;&lt;br /&gt;
&lt;br /&gt;
				store.put( value );&lt;br /&gt;
&lt;br /&gt;
				cursorRequest = store.openCursor();&lt;br /&gt;
&lt;br /&gt;
				cursorRequest.onsuccess = function () {&lt;br /&gt;
					var cursor = cursorRequest.result;&lt;br /&gt;
&lt;br /&gt;
					if ( !cursor ) {&lt;br /&gt;
						return;&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					if ( cursor.key !== value.key ) {&lt;br /&gt;
						cursor.delete();&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					cursor.continue();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					resolve();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.onerror = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					reject( tx.error || new Error( &#039;IndexedDB write failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeFileTitle( title ) {&lt;br /&gt;
		title = String( title || &#039;&#039; ).trim();&lt;br /&gt;
&lt;br /&gt;
		if ( !title ) {&lt;br /&gt;
			return &#039;&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return /^File:/i.test( title )&lt;br /&gt;
			? title&lt;br /&gt;
			: &#039;File:&#039; + title;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchCatalog( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: ICON_LIST_PAGE,&lt;br /&gt;
			prop: &#039;images|revid&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var parse = data &amp;amp;&amp;amp; data.parse ? data.parse : {};&lt;br /&gt;
			var titles = Array.isArray( parse.images ) ? parse.images : [];&lt;br /&gt;
&lt;br /&gt;
			titles = titles&lt;br /&gt;
				.map( normalizeFileTitle )&lt;br /&gt;
				.filter( Boolean )&lt;br /&gt;
				.filter( function ( title, index, list ) {&lt;br /&gt;
					return list.indexOf( title ) === index;&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				revid: Number( parse.revid ) || 0,&lt;br /&gt;
				titles: titles&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function chunk( list, size ) {&lt;br /&gt;
		var out = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; list.length; i += size ) {&lt;br /&gt;
			out.push( list.slice( i, i + size ) );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchImageInfo( api, titles, setStatus ) {&lt;br /&gt;
		var batches = chunk( titles, IMAGEINFO_BATCH );&lt;br /&gt;
		var records = [];&lt;br /&gt;
		var chain = Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
		batches.forEach( function ( batch, batchIndex ) {&lt;br /&gt;
			chain = chain.then( function () {&lt;br /&gt;
				if ( setStatus ) {&lt;br /&gt;
					setStatus(&lt;br /&gt;
						&#039;Reading uploaded icon files… &#039; +&lt;br /&gt;
						Math.min( batchIndex * IMAGEINFO_BATCH, titles.length ) +&lt;br /&gt;
						&#039; / &#039; + titles.length&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return api.get( {&lt;br /&gt;
					action: &#039;query&#039;,&lt;br /&gt;
					prop: &#039;imageinfo&#039;,&lt;br /&gt;
					titles: batch.join( &#039;|&#039; ),&lt;br /&gt;
					iiprop: &#039;url|size|sha1&#039;,&lt;br /&gt;
					formatversion: 2&lt;br /&gt;
				} ).then( function ( data ) {&lt;br /&gt;
					var pages =&lt;br /&gt;
						data &amp;amp;&amp;amp;&lt;br /&gt;
						data.query &amp;amp;&amp;amp;&lt;br /&gt;
						Array.isArray( data.query.pages )&lt;br /&gt;
							? data.query.pages&lt;br /&gt;
							: [];&lt;br /&gt;
&lt;br /&gt;
					pages.forEach( function ( page ) {&lt;br /&gt;
						var info =&lt;br /&gt;
							page &amp;amp;&amp;amp;&lt;br /&gt;
							Array.isArray( page.imageinfo ) &amp;amp;&amp;amp;&lt;br /&gt;
							page.imageinfo.length&lt;br /&gt;
								? page.imageinfo[ 0 ]&lt;br /&gt;
								: null;&lt;br /&gt;
&lt;br /&gt;
						if ( !info || !info.url ) {&lt;br /&gt;
							return;&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						records.push( {&lt;br /&gt;
							title: normalizeFileTitle( page.title ),&lt;br /&gt;
							url: String( info.url ),&lt;br /&gt;
							sha1: String( info.sha1 || &#039;&#039; ),&lt;br /&gt;
							width: Number( info.width ) || 0,&lt;br /&gt;
							height: Number( info.height ) || 0&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return chain.then( function () {&lt;br /&gt;
			return records;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function blobToDrawable( blob ) {&lt;br /&gt;
		if ( window.createImageBitmap ) {&lt;br /&gt;
			return createImageBitmap( blob );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var url = URL.createObjectURL( blob );&lt;br /&gt;
			var img = new Image();&lt;br /&gt;
&lt;br /&gt;
			img.onload = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				resolve( img );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.onerror = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				reject( new Error( &#039;Image decode failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.src = url;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchDrawable( url ) {&lt;br /&gt;
		return fetch( url, {&lt;br /&gt;
			credentials: &#039;same-origin&#039;,&lt;br /&gt;
			cache: &#039;force-cache&#039;&lt;br /&gt;
		} )&lt;br /&gt;
			.then( function ( response ) {&lt;br /&gt;
				if ( !response.ok ) {&lt;br /&gt;
					throw new Error( &#039;HTTP &#039; + response.status );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return response.blob();&lt;br /&gt;
			} )&lt;br /&gt;
			.then( blobToDrawable );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function closeDrawable( drawable ) {&lt;br /&gt;
		if ( drawable &amp;amp;&amp;amp; typeof drawable.close === &#039;function&#039; ) {&lt;br /&gt;
			drawable.close();&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function drawScaled( drawable, width, height, cropFraction ) {&lt;br /&gt;
		var sourceCanvas = document.createElement( &#039;canvas&#039; );&lt;br /&gt;
		var sourceWidth =&lt;br /&gt;
			Number( drawable.width || drawable.naturalWidth ) || 1;&lt;br /&gt;
		var sourceHeight =&lt;br /&gt;
			Number( drawable.height || drawable.naturalHeight ) || 1;&lt;br /&gt;
		var sourceCtx;&lt;br /&gt;
		var sourceImage;&lt;br /&gt;
		var sourceData;&lt;br /&gt;
		var pixelCount = sourceWidth * sourceHeight;&lt;br /&gt;
		var transparentCount = 0;&lt;br /&gt;
		var mask = new Uint8Array( pixelCount );&lt;br /&gt;
		var foregroundCount = 0;&lt;br /&gt;
		var borderR = [];&lt;br /&gt;
		var borderG = [];&lt;br /&gt;
		var borderB = [];&lt;br /&gt;
		var bgR = 0;&lt;br /&gt;
		var bgG = 0;&lt;br /&gt;
		var bgB = 0;&lt;br /&gt;
		var bgLuma = 0;&lt;br /&gt;
		var borderSpread = [];&lt;br /&gt;
		var tolerance;&lt;br /&gt;
		var darkCutoff;&lt;br /&gt;
		var minX = sourceWidth;&lt;br /&gt;
		var minY = sourceHeight;&lt;br /&gt;
		var maxX = -1;&lt;br /&gt;
		var maxY = -1;&lt;br /&gt;
		var cleanCanvas;&lt;br /&gt;
		var cleanCtx;&lt;br /&gt;
		var cleanImage;&lt;br /&gt;
		var targetCanvas;&lt;br /&gt;
		var targetCtx;&lt;br /&gt;
		var usableWidth;&lt;br /&gt;
		var usableHeight;&lt;br /&gt;
		var scale;&lt;br /&gt;
		var drawWidth;&lt;br /&gt;
		var drawHeight;&lt;br /&gt;
		var drawX;&lt;br /&gt;
		var drawY;&lt;br /&gt;
		var i;&lt;br /&gt;
		var p;&lt;br /&gt;
		var x;&lt;br /&gt;
		var y;&lt;br /&gt;
		var r;&lt;br /&gt;
		var g;&lt;br /&gt;
		var b;&lt;br /&gt;
		var a;&lt;br /&gt;
		var luma;&lt;br /&gt;
		var chroma;&lt;br /&gt;
		var maxChannel;&lt;br /&gt;
		var minChannel;&lt;br /&gt;
		var colorDistance;&lt;br /&gt;
&lt;br /&gt;
		function median( values ) {&lt;br /&gt;
			var sorted;&lt;br /&gt;
&lt;br /&gt;
			if ( !values.length ) {&lt;br /&gt;
				return 0;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			sorted = values.slice().sort(&lt;br /&gt;
				function ( left, right ) {&lt;br /&gt;
					return left - right;&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			return sorted[&lt;br /&gt;
				Math.floor( sorted.length / 2 )&lt;br /&gt;
			];&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		function addBorderPixel( px, py ) {&lt;br /&gt;
			var index =&lt;br /&gt;
				( py * sourceWidth + px ) * 4;&lt;br /&gt;
&lt;br /&gt;
			if ( sourceData[ index + 3 ] &amp;lt; 16 ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			borderR.push( sourceData[ index ] );&lt;br /&gt;
			borderG.push( sourceData[ index + 1 ] );&lt;br /&gt;
			borderB.push( sourceData[ index + 2 ] );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		sourceCanvas.width = sourceWidth;&lt;br /&gt;
		sourceCanvas.height = sourceHeight;&lt;br /&gt;
&lt;br /&gt;
		sourceCtx = sourceCanvas.getContext(&lt;br /&gt;
			&#039;2d&#039;,&lt;br /&gt;
			{&lt;br /&gt;
				willReadFrequently: true&lt;br /&gt;
			}&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Preserve real PNG transparency.&lt;br /&gt;
		 */&lt;br /&gt;
		sourceCtx.clearRect(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			sourceWidth,&lt;br /&gt;
			sourceHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceCtx.drawImage(&lt;br /&gt;
			drawable,&lt;br /&gt;
			0,&lt;br /&gt;
			0&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceImage = sourceCtx.getImageData(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			sourceWidth,&lt;br /&gt;
			sourceHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceData = sourceImage.data;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; pixelCount; i++ ) {&lt;br /&gt;
			if ( sourceData[ i * 4 + 3 ] &amp;lt; 245 ) {&lt;br /&gt;
				transparentCount++;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			transparentCount &amp;gt;&lt;br /&gt;
			Math.max( 4, pixelCount * 0.01 )&lt;br /&gt;
		) {&lt;br /&gt;
			/*&lt;br /&gt;
			 * Uploaded transparent PNG:&lt;br /&gt;
			 * alpha is already an excellent mask.&lt;br /&gt;
			 */&lt;br /&gt;
			for ( i = 0; i &amp;lt; pixelCount; i++ ) {&lt;br /&gt;
				if (&lt;br /&gt;
					sourceData[ i * 4 + 3 ] &amp;gt; 24&lt;br /&gt;
				) {&lt;br /&gt;
					mask[ i ] = 1;&lt;br /&gt;
					foregroundCount++;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		} else {&lt;br /&gt;
			/*&lt;br /&gt;
			 * Pasted screenshot:&lt;br /&gt;
			 *&lt;br /&gt;
			 * Infer the UI background from a two-pixel&lt;br /&gt;
			 * border around the capture.&lt;br /&gt;
			 */&lt;br /&gt;
			for (&lt;br /&gt;
				x = 0;&lt;br /&gt;
				x &amp;lt; sourceWidth;&lt;br /&gt;
				x++&lt;br /&gt;
			) {&lt;br /&gt;
				for (&lt;br /&gt;
					y = 0;&lt;br /&gt;
					y &amp;lt; Math.min(&lt;br /&gt;
						2,&lt;br /&gt;
						sourceHeight&lt;br /&gt;
					);&lt;br /&gt;
					y++&lt;br /&gt;
				) {&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						sourceHeight - 1 - y&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				y = 0;&lt;br /&gt;
				y &amp;lt; sourceHeight;&lt;br /&gt;
				y++&lt;br /&gt;
			) {&lt;br /&gt;
				for (&lt;br /&gt;
					x = 0;&lt;br /&gt;
					x &amp;lt; Math.min(&lt;br /&gt;
						2,&lt;br /&gt;
						sourceWidth&lt;br /&gt;
					);&lt;br /&gt;
					x++&lt;br /&gt;
				) {&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						sourceWidth - 1 - x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			bgR = median( borderR );&lt;br /&gt;
			bgG = median( borderG );&lt;br /&gt;
			bgB = median( borderB );&lt;br /&gt;
&lt;br /&gt;
			bgLuma = luminance(&lt;br /&gt;
				bgR,&lt;br /&gt;
				bgG,&lt;br /&gt;
				bgB&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				i = 0;&lt;br /&gt;
				i &amp;lt; borderR.length;&lt;br /&gt;
				i++&lt;br /&gt;
			) {&lt;br /&gt;
				borderSpread.push(&lt;br /&gt;
					Math.sqrt(&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderR[ i ] - bgR,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderG[ i ] - bgG,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderB[ i ] - bgB,&lt;br /&gt;
							2&lt;br /&gt;
						)&lt;br /&gt;
					)&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * The old crop variants blindly trimmed the&lt;br /&gt;
			 * image square.&lt;br /&gt;
			 *&lt;br /&gt;
			 * Now they instead make background removal&lt;br /&gt;
			 * slightly more aggressive.&lt;br /&gt;
			 */&lt;br /&gt;
			tolerance = Math.max(&lt;br /&gt;
				20,&lt;br /&gt;
				Math.min(&lt;br /&gt;
					74,&lt;br /&gt;
					18 +&lt;br /&gt;
						median(&lt;br /&gt;
							borderSpread&lt;br /&gt;
						) * 2.5 +&lt;br /&gt;
						cropFraction * 80&lt;br /&gt;
				)&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			darkCutoff = Math.max(&lt;br /&gt;
				36,&lt;br /&gt;
				Math.min(&lt;br /&gt;
					88,&lt;br /&gt;
					bgLuma +&lt;br /&gt;
						22 +&lt;br /&gt;
						cropFraction * 100&lt;br /&gt;
				)&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				i = 0;&lt;br /&gt;
				i &amp;lt; pixelCount;&lt;br /&gt;
				i++&lt;br /&gt;
			) {&lt;br /&gt;
				p = i * 4;&lt;br /&gt;
&lt;br /&gt;
				r = sourceData[ p ];&lt;br /&gt;
				g = sourceData[ p + 1 ];&lt;br /&gt;
				b = sourceData[ p + 2 ];&lt;br /&gt;
				a = sourceData[ p + 3 ];&lt;br /&gt;
&lt;br /&gt;
				if ( a &amp;lt; 16 ) {&lt;br /&gt;
					continue;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				maxChannel = Math.max(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				minChannel = Math.min(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				chroma =&lt;br /&gt;
					maxChannel -&lt;br /&gt;
					minChannel;&lt;br /&gt;
&lt;br /&gt;
				luma = luminance(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				colorDistance =&lt;br /&gt;
					Math.sqrt(&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							r - bgR,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							g - bgG,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							b - bgB,&lt;br /&gt;
							2&lt;br /&gt;
						)&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				/*&lt;br /&gt;
				 * EQ&#039;s editor/game icon background is&lt;br /&gt;
				 * normally very dark.&lt;br /&gt;
				 *&lt;br /&gt;
				 * Discard dark neutral pixels, but allow&lt;br /&gt;
				 * strongly colored pixels to survive at&lt;br /&gt;
				 * lower brightness.&lt;br /&gt;
				 */&lt;br /&gt;
				if ( bgLuma &amp;lt; 95 ) {&lt;br /&gt;
					if (&lt;br /&gt;
						(&lt;br /&gt;
							luma &amp;gt;=&lt;br /&gt;
								darkCutoff &amp;amp;&amp;amp;&lt;br /&gt;
							colorDistance &amp;gt;=&lt;br /&gt;
								tolerance *&lt;br /&gt;
								0.65&lt;br /&gt;
						) ||&lt;br /&gt;
						(&lt;br /&gt;
							chroma &amp;gt;= 24 &amp;amp;&amp;amp;&lt;br /&gt;
							luma &amp;gt;=&lt;br /&gt;
								bgLuma +&lt;br /&gt;
								7 +&lt;br /&gt;
								cropFraction *&lt;br /&gt;
								30 &amp;amp;&amp;amp;&lt;br /&gt;
							colorDistance &amp;gt;=&lt;br /&gt;
								tolerance *&lt;br /&gt;
								0.55&lt;br /&gt;
						)&lt;br /&gt;
					) {&lt;br /&gt;
						mask[ i ] = 1;&lt;br /&gt;
						foregroundCount++;&lt;br /&gt;
					}&lt;br /&gt;
				} else if (&lt;br /&gt;
					colorDistance &amp;gt;=&lt;br /&gt;
					tolerance&lt;br /&gt;
				) {&lt;br /&gt;
					mask[ i ] = 1;&lt;br /&gt;
					foregroundCount++;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * Fail soft for an unusual image instead of&lt;br /&gt;
			 * producing an empty fingerprint.&lt;br /&gt;
			 */&lt;br /&gt;
			if (&lt;br /&gt;
				foregroundCount &amp;lt; 6 ||&lt;br /&gt;
				foregroundCount &amp;gt;&lt;br /&gt;
					pixelCount * 0.90&lt;br /&gt;
			) {&lt;br /&gt;
				mask.fill( 0 );&lt;br /&gt;
				foregroundCount = 0;&lt;br /&gt;
&lt;br /&gt;
				for (&lt;br /&gt;
					i = 0;&lt;br /&gt;
					i &amp;lt; pixelCount;&lt;br /&gt;
					i++&lt;br /&gt;
				) {&lt;br /&gt;
					if (&lt;br /&gt;
						sourceData[&lt;br /&gt;
							i * 4 + 3&lt;br /&gt;
						] &amp;gt; 24&lt;br /&gt;
					) {&lt;br /&gt;
						mask[ i ] = 1;&lt;br /&gt;
						foregroundCount++;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Find the actual sprite bounds.&lt;br /&gt;
		 */&lt;br /&gt;
		for (&lt;br /&gt;
			y = 0;&lt;br /&gt;
			y &amp;lt; sourceHeight;&lt;br /&gt;
			y++&lt;br /&gt;
		) {&lt;br /&gt;
			for (&lt;br /&gt;
				x = 0;&lt;br /&gt;
				x &amp;lt; sourceWidth;&lt;br /&gt;
				x++&lt;br /&gt;
			) {&lt;br /&gt;
				i =&lt;br /&gt;
					y * sourceWidth +&lt;br /&gt;
					x;&lt;br /&gt;
&lt;br /&gt;
				if ( !mask[ i ] ) {&lt;br /&gt;
					continue;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				minX = Math.min(&lt;br /&gt;
					minX,&lt;br /&gt;
					x&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				minY = Math.min(&lt;br /&gt;
					minY,&lt;br /&gt;
					y&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				maxX = Math.max(&lt;br /&gt;
					maxX,&lt;br /&gt;
					x&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				maxY = Math.max(&lt;br /&gt;
					maxY,&lt;br /&gt;
					y&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			maxX &amp;lt; minX ||&lt;br /&gt;
			maxY &amp;lt; minY&lt;br /&gt;
		) {&lt;br /&gt;
			minX = 0;&lt;br /&gt;
			minY = 0;&lt;br /&gt;
			maxX = sourceWidth - 1;&lt;br /&gt;
			maxY = sourceHeight - 1;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Create a transparent copy containing only&lt;br /&gt;
		 * detected foreground pixels.&lt;br /&gt;
		 */&lt;br /&gt;
		cleanCanvas =&lt;br /&gt;
			document.createElement(&lt;br /&gt;
				&#039;canvas&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		cleanCanvas.width =&lt;br /&gt;
			sourceWidth;&lt;br /&gt;
&lt;br /&gt;
		cleanCanvas.height =&lt;br /&gt;
			sourceHeight;&lt;br /&gt;
&lt;br /&gt;
		cleanCtx =&lt;br /&gt;
			cleanCanvas.getContext(&lt;br /&gt;
				&#039;2d&#039;,&lt;br /&gt;
				{&lt;br /&gt;
					willReadFrequently: true&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		cleanImage =&lt;br /&gt;
			cleanCtx.createImageData(&lt;br /&gt;
				sourceWidth,&lt;br /&gt;
				sourceHeight&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i &amp;lt; pixelCount;&lt;br /&gt;
			i++&lt;br /&gt;
		) {&lt;br /&gt;
			if ( !mask[ i ] ) {&lt;br /&gt;
				continue;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			p = i * 4;&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p ] =&lt;br /&gt;
				sourceData[ p ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 1 ] =&lt;br /&gt;
				sourceData[ p + 1 ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 2 ] =&lt;br /&gt;
				sourceData[ p + 2 ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 3 ] =&lt;br /&gt;
				255;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		cleanCtx.putImageData(&lt;br /&gt;
			cleanImage,&lt;br /&gt;
			0,&lt;br /&gt;
			0&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Normalize the detected sprite to the requested&lt;br /&gt;
		 * comparison size.&lt;br /&gt;
		 */&lt;br /&gt;
		targetCanvas =&lt;br /&gt;
			document.createElement(&lt;br /&gt;
				&#039;canvas&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		targetCanvas.width = width;&lt;br /&gt;
		targetCanvas.height = height;&lt;br /&gt;
&lt;br /&gt;
		targetCtx =&lt;br /&gt;
			targetCanvas.getContext(&lt;br /&gt;
				&#039;2d&#039;,&lt;br /&gt;
				{&lt;br /&gt;
					willReadFrequently: true&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.fillStyle = &#039;#000&#039;;&lt;br /&gt;
&lt;br /&gt;
		targetCtx.fillRect(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			width,&lt;br /&gt;
			height&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.imageSmoothingEnabled =&lt;br /&gt;
			true;&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			&#039;imageSmoothingQuality&#039; in&lt;br /&gt;
			targetCtx&lt;br /&gt;
		) {&lt;br /&gt;
			targetCtx.imageSmoothingQuality =&lt;br /&gt;
				&#039;high&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		usableWidth =&lt;br /&gt;
			Math.max(&lt;br /&gt;
				1,&lt;br /&gt;
				width - 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		usableHeight =&lt;br /&gt;
			Math.max(&lt;br /&gt;
				1,&lt;br /&gt;
				height - 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		scale = Math.min(&lt;br /&gt;
			usableWidth /&lt;br /&gt;
				Math.max(&lt;br /&gt;
					1,&lt;br /&gt;
					maxX - minX + 1&lt;br /&gt;
				),&lt;br /&gt;
			usableHeight /&lt;br /&gt;
				Math.max(&lt;br /&gt;
					1,&lt;br /&gt;
					maxY - minY + 1&lt;br /&gt;
				)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawWidth = Math.max(&lt;br /&gt;
			1,&lt;br /&gt;
			Math.round(&lt;br /&gt;
				(&lt;br /&gt;
					maxX -&lt;br /&gt;
					minX +&lt;br /&gt;
					1&lt;br /&gt;
				) * scale&lt;br /&gt;
			)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawHeight = Math.max(&lt;br /&gt;
			1,&lt;br /&gt;
			Math.round(&lt;br /&gt;
				(&lt;br /&gt;
					maxY -&lt;br /&gt;
					minY +&lt;br /&gt;
					1&lt;br /&gt;
				) * scale&lt;br /&gt;
			)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawX = Math.floor(&lt;br /&gt;
			(&lt;br /&gt;
				width -&lt;br /&gt;
				drawWidth&lt;br /&gt;
			) / 2&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawY = Math.floor(&lt;br /&gt;
			(&lt;br /&gt;
				height -&lt;br /&gt;
				drawHeight&lt;br /&gt;
			) / 2&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.drawImage(&lt;br /&gt;
			cleanCanvas,&lt;br /&gt;
			minX,&lt;br /&gt;
			minY,&lt;br /&gt;
			maxX - minX + 1,&lt;br /&gt;
			maxY - minY + 1,&lt;br /&gt;
			drawX,&lt;br /&gt;
			drawY,&lt;br /&gt;
			drawWidth,&lt;br /&gt;
			drawHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		return targetCtx.getImageData(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			width,&lt;br /&gt;
			height&lt;br /&gt;
		);&lt;br /&gt;
	}&lt;br /&gt;
	function luminance( r, g, b ) {&lt;br /&gt;
		return 0.2126 * r + 0.7152 * g + 0.0722 * b;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeDHash( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled( drawable, 9, 8, cropFraction );&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var hi = 0;&lt;br /&gt;
		var lo = 0;&lt;br /&gt;
		var bit = 0;&lt;br /&gt;
		var y;&lt;br /&gt;
		var x;&lt;br /&gt;
		var left;&lt;br /&gt;
		var right;&lt;br /&gt;
		var li;&lt;br /&gt;
		var ri;&lt;br /&gt;
&lt;br /&gt;
		for ( y = 0; y &amp;lt; 8; y++ ) {&lt;br /&gt;
			for ( x = 0; x &amp;lt; 8; x++ ) {&lt;br /&gt;
				li = ( y * 9 + x ) * 4;&lt;br /&gt;
				ri = ( y * 9 + x + 1 ) * 4;&lt;br /&gt;
&lt;br /&gt;
				left = luminance(&lt;br /&gt;
					data[ li ],&lt;br /&gt;
					data[ li + 1 ],&lt;br /&gt;
					data[ li + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				right = luminance(&lt;br /&gt;
					data[ ri ],&lt;br /&gt;
					data[ ri + 1 ],&lt;br /&gt;
					data[ ri + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				if ( left &amp;gt; right ) {&lt;br /&gt;
					if ( bit &amp;lt; 32 ) {&lt;br /&gt;
						lo = ( lo | ( 1 &amp;lt;&amp;lt; bit ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					} else {&lt;br /&gt;
						hi = ( hi | ( 1 &amp;lt;&amp;lt; ( bit - 32 ) ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				bit++;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hi: hi &amp;gt;&amp;gt;&amp;gt; 0,&lt;br /&gt;
			lo: lo &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeColorGrid( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled(&lt;br /&gt;
			drawable,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			cropFraction&lt;br /&gt;
		);&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var out = new Uint8Array( COLOR_GRID * COLOR_GRID * 3 );&lt;br /&gt;
		var source = 0;&lt;br /&gt;
		var target = 0;&lt;br /&gt;
&lt;br /&gt;
		while ( source &amp;lt; data.length ) {&lt;br /&gt;
			out[ target++ ] = data[ source ];&lt;br /&gt;
			out[ target++ ] = data[ source + 1 ];&lt;br /&gt;
			out[ target++ ] = data[ source + 2 ];&lt;br /&gt;
			source += 4;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDrawable( drawable, cropFraction ) {&lt;br /&gt;
		var hash = computeDHash( drawable, cropFraction );&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hashHi: hash.hi,&lt;br /&gt;
			hashLo: hash.lo,&lt;br /&gt;
			rgb: computeColorGrid( drawable, cropFraction )&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function popcount32( value ) {&lt;br /&gt;
		value = value &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
		value = value - ( ( value &amp;gt;&amp;gt;&amp;gt; 1 ) &amp;amp; 0x55555555 );&lt;br /&gt;
		value = ( value &amp;amp; 0x33333333 ) + ( ( value &amp;gt;&amp;gt;&amp;gt; 2 ) &amp;amp; 0x33333333 );&lt;br /&gt;
		return (&lt;br /&gt;
			(&lt;br /&gt;
				(&lt;br /&gt;
					value + ( value &amp;gt;&amp;gt;&amp;gt; 4 )&lt;br /&gt;
				) &amp;amp; 0x0F0F0F0F&lt;br /&gt;
			) * 0x01010101&lt;br /&gt;
		) &amp;gt;&amp;gt;&amp;gt; 24;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDistance( a, b ) {&lt;br /&gt;
		var hashBits =&lt;br /&gt;
			popcount32(&lt;br /&gt;
				(&lt;br /&gt;
					a.hashHi ^&lt;br /&gt;
					b.hashHi&lt;br /&gt;
				) &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
			) +&lt;br /&gt;
			popcount32(&lt;br /&gt;
				(&lt;br /&gt;
					a.hashLo ^&lt;br /&gt;
					b.hashLo&lt;br /&gt;
				) &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		var hashDistance =&lt;br /&gt;
			hashBits / 64;&lt;br /&gt;
&lt;br /&gt;
		var length = Math.min(&lt;br /&gt;
			a.rgb.length,&lt;br /&gt;
			b.rgb.length&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		var histogramA =&lt;br /&gt;
			new Uint16Array( 64 );&lt;br /&gt;
&lt;br /&gt;
		var histogramB =&lt;br /&gt;
			new Uint16Array( 64 );&lt;br /&gt;
&lt;br /&gt;
		var foregroundA = 0;&lt;br /&gt;
		var foregroundB = 0;&lt;br /&gt;
		var intersection = 0;&lt;br /&gt;
		var union = 0;&lt;br /&gt;
		var colorSum = 0;&lt;br /&gt;
		var i;&lt;br /&gt;
		var ar;&lt;br /&gt;
		var ag;&lt;br /&gt;
		var ab;&lt;br /&gt;
		var br;&lt;br /&gt;
		var bg;&lt;br /&gt;
		var bb;&lt;br /&gt;
		var aForeground;&lt;br /&gt;
		var bForeground;&lt;br /&gt;
		var dr;&lt;br /&gt;
		var dg;&lt;br /&gt;
		var db;&lt;br /&gt;
		var binA;&lt;br /&gt;
		var binB;&lt;br /&gt;
		var shapeDistance;&lt;br /&gt;
		var colorDistance;&lt;br /&gt;
		var histogramDistance = 0;&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i + 2 &amp;lt; length;&lt;br /&gt;
			i += 3&lt;br /&gt;
		) {&lt;br /&gt;
			ar = a.rgb[ i ];&lt;br /&gt;
			ag = a.rgb[ i + 1 ];&lt;br /&gt;
			ab = a.rgb[ i + 2 ];&lt;br /&gt;
&lt;br /&gt;
			br = b.rgb[ i ];&lt;br /&gt;
			bg = b.rgb[ i + 1 ];&lt;br /&gt;
			bb = b.rgb[ i + 2 ];&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * Background is normalized to black by&lt;br /&gt;
			 * drawScaled().&lt;br /&gt;
			 */&lt;br /&gt;
			aForeground =&lt;br /&gt;
				Math.max(&lt;br /&gt;
					ar,&lt;br /&gt;
					ag,&lt;br /&gt;
					ab&lt;br /&gt;
				) &amp;gt; 8;&lt;br /&gt;
&lt;br /&gt;
			bForeground =&lt;br /&gt;
				Math.max(&lt;br /&gt;
					br,&lt;br /&gt;
					bg,&lt;br /&gt;
					bb&lt;br /&gt;
				) &amp;gt; 8;&lt;br /&gt;
&lt;br /&gt;
			if ( aForeground ) {&lt;br /&gt;
				foregroundA++;&lt;br /&gt;
&lt;br /&gt;
				binA =&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							ar &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 4&lt;br /&gt;
					) |&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							ag &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 2&lt;br /&gt;
					) |&lt;br /&gt;
					Math.min(&lt;br /&gt;
						3,&lt;br /&gt;
						ab &amp;gt;&amp;gt; 6&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				histogramA[ binA ]++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if ( bForeground ) {&lt;br /&gt;
				foregroundB++;&lt;br /&gt;
&lt;br /&gt;
				binB =&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							br &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 4&lt;br /&gt;
					) |&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							bg &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 2&lt;br /&gt;
					) |&lt;br /&gt;
					Math.min(&lt;br /&gt;
						3,&lt;br /&gt;
						bb &amp;gt;&amp;gt; 6&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				histogramB[ binB ]++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				aForeground ||&lt;br /&gt;
				bForeground&lt;br /&gt;
			) {&lt;br /&gt;
				union++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				!aForeground ||&lt;br /&gt;
				!bForeground&lt;br /&gt;
			) {&lt;br /&gt;
				continue;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			intersection++;&lt;br /&gt;
&lt;br /&gt;
			dr = ar - br;&lt;br /&gt;
			dg = ag - bg;&lt;br /&gt;
			db = ab - bb;&lt;br /&gt;
&lt;br /&gt;
			colorSum +=&lt;br /&gt;
				Math.sqrt(&lt;br /&gt;
					dr * dr +&lt;br /&gt;
					dg * dg +&lt;br /&gt;
					db * db&lt;br /&gt;
				) /&lt;br /&gt;
				441.67295593;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		shapeDistance =&lt;br /&gt;
			union&lt;br /&gt;
				? 1 -&lt;br /&gt;
					intersection /&lt;br /&gt;
						union&lt;br /&gt;
				: 1;&lt;br /&gt;
&lt;br /&gt;
		colorDistance =&lt;br /&gt;
			intersection&lt;br /&gt;
				? colorSum /&lt;br /&gt;
					intersection&lt;br /&gt;
				: 1;&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i &amp;lt; 64;&lt;br /&gt;
			i++&lt;br /&gt;
		) {&lt;br /&gt;
			histogramDistance +=&lt;br /&gt;
				Math.abs(&lt;br /&gt;
					histogramA[ i ] /&lt;br /&gt;
						Math.max(&lt;br /&gt;
							1,&lt;br /&gt;
							foregroundA&lt;br /&gt;
						) -&lt;br /&gt;
					histogramB[ i ] /&lt;br /&gt;
						Math.max(&lt;br /&gt;
							1,&lt;br /&gt;
							foregroundB&lt;br /&gt;
						)&lt;br /&gt;
				);&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Normalized histogram L1 range is 0..2.&lt;br /&gt;
		 */&lt;br /&gt;
		histogramDistance =&lt;br /&gt;
			Math.min(&lt;br /&gt;
				1,&lt;br /&gt;
				histogramDistance / 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Foreground color is deliberately the largest&lt;br /&gt;
		 * contributor now.&lt;br /&gt;
		 */&lt;br /&gt;
		return Math.min(&lt;br /&gt;
			1,&lt;br /&gt;
			0.50 * colorDistance +&lt;br /&gt;
				0.25 * shapeDistance +&lt;br /&gt;
				0.15 * histogramDistance +&lt;br /&gt;
				0.10 * hashDistance&lt;br /&gt;
		);&lt;br /&gt;
	}&lt;br /&gt;
	function mapLimit( list, limit, worker ) {&lt;br /&gt;
		var nextIndex = 0;&lt;br /&gt;
		var workers = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		function runWorker() {&lt;br /&gt;
			var index;&lt;br /&gt;
&lt;br /&gt;
			if ( nextIndex &amp;gt;= list.length ) {&lt;br /&gt;
				return Promise.resolve();&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			index = nextIndex++;&lt;br /&gt;
			return Promise.resolve( worker( list[ index ], index ) )&lt;br /&gt;
				.then( runWorker );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; Math.min( limit, list.length ); i++ ) {&lt;br /&gt;
			workers.push( runWorker() );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return Promise.all( workers );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildFingerprintIndex( records, setStatus ) {&lt;br /&gt;
		var complete = 0;&lt;br /&gt;
		var successful = [];&lt;br /&gt;
		var grouped = Object.create( null );&lt;br /&gt;
		var groups = [];&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * If several icon filenames are byte-identical, imageinfo gives them&lt;br /&gt;
		 * the same SHA-1. Fingerprint that image only once and fan the result&lt;br /&gt;
		 * back out to every filename. This can substantially reduce the first&lt;br /&gt;
		 * build&#039;s static-file traffic without changing the visible results.&lt;br /&gt;
		 */&lt;br /&gt;
		records.forEach( function ( record ) {&lt;br /&gt;
			var key = record.sha1 || record.url;&lt;br /&gt;
&lt;br /&gt;
			if ( !grouped[ key ] ) {&lt;br /&gt;
				grouped[ key ] = {&lt;br /&gt;
					key: key,&lt;br /&gt;
					representative: record,&lt;br /&gt;
					records: []&lt;br /&gt;
				};&lt;br /&gt;
				groups.push( grouped[ key ] );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			grouped[ key ].records.push( record );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return mapLimit(&lt;br /&gt;
			groups,&lt;br /&gt;
			DOWNLOAD_CONCURRENCY,&lt;br /&gt;
			function ( group ) {&lt;br /&gt;
				return fetchDrawable( group.representative.url )&lt;br /&gt;
					.then( function ( drawable ) {&lt;br /&gt;
						var fp = fingerprintDrawable( drawable, 0 );&lt;br /&gt;
&lt;br /&gt;
						closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
						group.records.forEach( function ( record ) {&lt;br /&gt;
							successful.push( {&lt;br /&gt;
								title: record.title,&lt;br /&gt;
								url: record.url,&lt;br /&gt;
								sha1: record.sha1,&lt;br /&gt;
								width: record.width,&lt;br /&gt;
								height: record.height,&lt;br /&gt;
								hashHi: fp.hashHi,&lt;br /&gt;
								hashLo: fp.hashLo,&lt;br /&gt;
								rgb: fp.rgb&lt;br /&gt;
							} );&lt;br /&gt;
						} );&lt;br /&gt;
					} )&lt;br /&gt;
					.catch( function () {&lt;br /&gt;
						/*&lt;br /&gt;
						 * A missing/corrupt/non-raster file should not prevent&lt;br /&gt;
						 * the rest of the icon index from becoming usable.&lt;br /&gt;
						 */&lt;br /&gt;
					} )&lt;br /&gt;
					.then( function () {&lt;br /&gt;
						complete++;&lt;br /&gt;
&lt;br /&gt;
						if (&lt;br /&gt;
							setStatus &amp;amp;&amp;amp;&lt;br /&gt;
							(&lt;br /&gt;
								complete === groups.length ||&lt;br /&gt;
								complete % 25 === 0&lt;br /&gt;
							)&lt;br /&gt;
						) {&lt;br /&gt;
							setStatus(&lt;br /&gt;
								&#039;Building pixel index… &#039; +&lt;br /&gt;
									complete + &#039; / &#039; + groups.length +&lt;br /&gt;
									&#039; unique images (&#039; +&lt;br /&gt;
									records.length + &#039; filenames)&#039;&lt;br /&gt;
							);&lt;br /&gt;
						}&lt;br /&gt;
					} );&lt;br /&gt;
			}&lt;br /&gt;
		).then( function () {&lt;br /&gt;
			return successful;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cacheKey( revid ) {&lt;br /&gt;
		return [&lt;br /&gt;
			&#039;icon-list&#039;,&lt;br /&gt;
			ALGO_VERSION,&lt;br /&gt;
			revid&lt;br /&gt;
		].join( &#039;:&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getOrBuildIndex( api, setStatus ) {&lt;br /&gt;
		if ( memoryIndex ) {&lt;br /&gt;
			return Promise.resolve( memoryIndex );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		setStatus( &#039;Reading Icon List…&#039; );&lt;br /&gt;
&lt;br /&gt;
		return fetchCatalog( api ).then( function ( catalog ) {&lt;br /&gt;
			var key = cacheKey( catalog.revid );&lt;br /&gt;
&lt;br /&gt;
			return idbGet( key )&lt;br /&gt;
				.catch( function () {&lt;br /&gt;
					return null;&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function ( cached ) {&lt;br /&gt;
					if (&lt;br /&gt;
						cached &amp;amp;&amp;amp;&lt;br /&gt;
						Array.isArray( cached.records ) &amp;amp;&amp;amp;&lt;br /&gt;
						cached.records.length&lt;br /&gt;
					) {&lt;br /&gt;
						memoryIndex = cached.records;&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Icon index ready: &#039; +&lt;br /&gt;
								memoryIndex.length +&lt;br /&gt;
								&#039; uploaded icons (cached locally).&#039;&lt;br /&gt;
						);&lt;br /&gt;
						return memoryIndex;&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					return fetchImageInfo(&lt;br /&gt;
						api,&lt;br /&gt;
						catalog.titles,&lt;br /&gt;
						setStatus&lt;br /&gt;
					).then( function ( imageRecords ) {&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Found &#039; + imageRecords.length +&lt;br /&gt;
								&#039; uploaded icons. Building pixel index…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						return buildFingerprintIndex(&lt;br /&gt;
							imageRecords,&lt;br /&gt;
							setStatus&lt;br /&gt;
						);&lt;br /&gt;
					} ).then( function ( records ) {&lt;br /&gt;
						memoryIndex = records;&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Icon index ready: &#039; +&lt;br /&gt;
								records.length +&lt;br /&gt;
								&#039; uploaded icons.&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						return idbPutAndPrune( {&lt;br /&gt;
							key: key,&lt;br /&gt;
							revid: catalog.revid,&lt;br /&gt;
							algorithm: ALGO_VERSION,&lt;br /&gt;
							builtAt: Date.now(),&lt;br /&gt;
							records: records&lt;br /&gt;
						} )&lt;br /&gt;
							.catch( function () {&lt;br /&gt;
								/*&lt;br /&gt;
								 * Matching still works for this session even if&lt;br /&gt;
								 * the browser blocks persistent storage.&lt;br /&gt;
								 */&lt;br /&gt;
							} )&lt;br /&gt;
							.then( function () {&lt;br /&gt;
								return records;&lt;br /&gt;
							} );&lt;br /&gt;
					} );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintPastedImage( drawable ) {&lt;br /&gt;
		/*&lt;br /&gt;
		 * Small centered crop variants make the matcher forgiving of a&lt;br /&gt;
		 * 1-2 pixel screen-capture border while still preferring an exact crop.&lt;br /&gt;
		 */&lt;br /&gt;
		return [&lt;br /&gt;
			fingerprintDrawable( drawable, 0 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.035 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.07 )&lt;br /&gt;
		];&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function rankMatches( targetFingerprints, records ) {&lt;br /&gt;
		var ranked = records.map( function ( record ) {&lt;br /&gt;
			var candidate = {&lt;br /&gt;
				hashHi: record.hashHi,&lt;br /&gt;
				hashLo: record.hashLo,&lt;br /&gt;
				rgb: record.rgb&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			var distance = Infinity;&lt;br /&gt;
&lt;br /&gt;
			targetFingerprints.forEach( function ( target ) {&lt;br /&gt;
				distance = Math.min(&lt;br /&gt;
					distance,&lt;br /&gt;
					fingerprintDistance( target, candidate )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				record: record,&lt;br /&gt;
				distance: distance,&lt;br /&gt;
				similarity: Math.max(&lt;br /&gt;
					0,&lt;br /&gt;
					Math.min( 100, ( 1 - distance ) * 100 )&lt;br /&gt;
				)&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		ranked.sort( function ( a, b ) {&lt;br /&gt;
			return a.distance - b.distance;&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return ranked.slice( 0, RESULT_COUNT );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cleanDisplayTitle( title ) {&lt;br /&gt;
		return String( title || &#039;&#039; ).replace( /^File:/i, &#039;&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function iconMetadata( title ) {&lt;br /&gt;
		var clean = cleanDisplayTitle( title );&lt;br /&gt;
		var itemMatch = clean.match( /^Item\s+(\d+)\.png$/i );&lt;br /&gt;
		var spellMatch = clean.match( /^Spellicon_([^.]*)\.png$/i );&lt;br /&gt;
		var id = &#039;&#039;;&lt;br /&gt;
		var kind = &#039;Icon file&#039;;&lt;br /&gt;
		var parameter = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		if ( itemMatch ) {&lt;br /&gt;
			id = itemMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Item icon&#039;;&lt;br /&gt;
			parameter = &#039;|lucy_img_ID = &#039; + id;&lt;br /&gt;
		} else if ( spellMatch ) {&lt;br /&gt;
			id = spellMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Spell icon&#039;;&lt;br /&gt;
			parameter = &#039;|spellicon = &#039; + id;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			kind: kind,&lt;br /&gt;
			id: id,&lt;br /&gt;
			filename: clean,&lt;br /&gt;
			parameter: parameter,&lt;br /&gt;
			rawWiki: &#039;[[File:&#039; + clean + &#039;]]&#039;,&lt;br /&gt;
			staticWiki: &#039;[[File:&#039; + clean + &#039;|32x32px]]&#039;,&lt;br /&gt;
			scaledWiki: &#039;[[File:&#039; + clean + &#039;|frameless|upright=1.5]]&#039;&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function copyText( text, button ) {&lt;br /&gt;
		var original = button.textContent;&lt;br /&gt;
&lt;br /&gt;
		function done() {&lt;br /&gt;
			button.textContent = &#039;Copied&#039;;&lt;br /&gt;
			window.setTimeout( function () {&lt;br /&gt;
				button.textContent = original;&lt;br /&gt;
			}, 1000 );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			navigator.clipboard &amp;amp;&amp;amp;&lt;br /&gt;
			typeof navigator.clipboard.writeText === &#039;function&#039;&lt;br /&gt;
		) {&lt;br /&gt;
			navigator.clipboard.writeText( text )&lt;br /&gt;
				.then( done )&lt;br /&gt;
				.catch( function () {&lt;br /&gt;
					window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
				} );&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeCopyButton( label, value ) {&lt;br /&gt;
		var button = document.createElement( &#039;button&#039; );&lt;br /&gt;
&lt;br /&gt;
		button.className = &#039;eql-iconfinder-copy&#039;;&lt;br /&gt;
		button.type = &#039;button&#039;;&lt;br /&gt;
		button.textContent = label;&lt;br /&gt;
&lt;br /&gt;
		button.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			copyText( value, button );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return button;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function renderMatches( container, matches ) {&lt;br /&gt;
		container.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
		var grid = document.createElement( &#039;div&#039; );&lt;br /&gt;
		grid.className = &#039;eql-iconfinder-results-grid&#039;;&lt;br /&gt;
		container.appendChild( grid );&lt;br /&gt;
&lt;br /&gt;
		matches.forEach( function ( match ) {&lt;br /&gt;
			var record = match.record;&lt;br /&gt;
			var meta = iconMetadata( record.title );&lt;br /&gt;
			var card = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var img = document.createElement( &#039;img&#039; );&lt;br /&gt;
			var textWrap = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var link = document.createElement( &#039;a&#039; );&lt;br /&gt;
			var score = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var iconId = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var metadata = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var copyRow = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
			card.className = &#039;eql-iconfinder-result&#039;;&lt;br /&gt;
&lt;br /&gt;
			img.src = record.url;&lt;br /&gt;
			img.alt = meta.filename;&lt;br /&gt;
			img.loading = &#039;lazy&#039;;&lt;br /&gt;
&lt;br /&gt;
			link.className = &#039;eql-iconfinder-result-name&#039;;&lt;br /&gt;
			link.href = mw.util.getUrl( record.title );&lt;br /&gt;
			link.target = &#039;_blank&#039;;&lt;br /&gt;
			link.rel = &#039;noopener&#039;;&lt;br /&gt;
			link.textContent = meta.filename;&lt;br /&gt;
&lt;br /&gt;
			score.className = &#039;eql-iconfinder-result-score&#039;;&lt;br /&gt;
			score.textContent =&lt;br /&gt;
				&#039;Match score &#039; + match.similarity.toFixed( 1 );&lt;br /&gt;
&lt;br /&gt;
			iconId.className = &#039;eql-iconfinder-result-id&#039;;&lt;br /&gt;
			iconId.textContent = meta.id&lt;br /&gt;
				? &#039;Icon &#039; + meta.id&lt;br /&gt;
				: meta.filename;&lt;br /&gt;
&lt;br /&gt;
			metadata.className = &#039;eql-iconfinder-result-meta&#039;;&lt;br /&gt;
			metadata.textContent = meta.kind;&lt;br /&gt;
&lt;br /&gt;
			copyRow.className = &#039;eql-iconfinder-copy-row&#039;;&lt;br /&gt;
&lt;br /&gt;
			if ( meta.id ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy ID&#039;, meta.id )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if ( meta.parameter ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy Parameter&#039;, meta.parameter )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Raw Wikicode&#039;, meta.rawWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;32×32&#039;, meta.staticWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Scaled&#039;, meta.scaledWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			textWrap.appendChild( link );&lt;br /&gt;
			textWrap.appendChild( iconId );&lt;br /&gt;
			textWrap.appendChild( score );&lt;br /&gt;
			textWrap.appendChild( metadata );&lt;br /&gt;
			textWrap.appendChild( copyRow );&lt;br /&gt;
&lt;br /&gt;
			card.appendChild( img );&lt;br /&gt;
			card.appendChild( textWrap );&lt;br /&gt;
			grid.appendChild( card );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		var note = document.createElement( &#039;div&#039; );&lt;br /&gt;
		note.className = &#039;eql-iconfinder-note&#039;;&lt;br /&gt;
		note.textContent =&lt;br /&gt;
			&#039;The tool only identifies likely matches and copies values/wikicode to the clipboard; it never changes the editor. &#039; +&lt;br /&gt;
			&#039;For the best result, capture only the square icon with as little surrounding UI as possible.&#039;;&lt;br /&gt;
		container.appendChild( note );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function imageFromClipboardEvent( event ) {&lt;br /&gt;
		var clipboard = event.clipboardData;&lt;br /&gt;
		var items;&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		if ( !clipboard || !clipboard.items ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		items = clipboard.items;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; items.length; i++ ) {&lt;br /&gt;
			if (&lt;br /&gt;
				items[ i ].kind === &#039;file&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				/^image\//i.test( items[ i ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				return items[ i ].getAsFile();&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return null;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setPreview( img, blob ) {&lt;br /&gt;
		if ( currentPreviewUrl ) {&lt;br /&gt;
			URL.revokeObjectURL( currentPreviewUrl );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		currentPreviewUrl = URL.createObjectURL( blob );&lt;br /&gt;
		img.src = currentPreviewUrl;&lt;br /&gt;
		img.classList.add( &#039;is-visible&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function processBlob( blob, preview, status, results, api ) {&lt;br /&gt;
		var setStatus = function ( text ) {&lt;br /&gt;
			status.textContent = text;&lt;br /&gt;
		};&lt;br /&gt;
&lt;br /&gt;
		setPreview( preview, blob );&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Analyzing pasted icon…&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		return blobToDrawable( blob )&lt;br /&gt;
			.then( function ( drawable ) {&lt;br /&gt;
				var targets = fingerprintPastedImage( drawable );&lt;br /&gt;
				closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
				return getOrBuildIndex( api, setStatus )&lt;br /&gt;
					.then( function ( index ) {&lt;br /&gt;
						var matches;&lt;br /&gt;
&lt;br /&gt;
						if ( !index.length ) {&lt;br /&gt;
							throw new Error( &#039;No uploaded icons were found.&#039; );&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Comparing against &#039; +&lt;br /&gt;
								index.length +&lt;br /&gt;
								&#039; uploaded icons…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						/*&lt;br /&gt;
						 * Yield once so the status message can paint before&lt;br /&gt;
						 * ranking several thousand in-memory fingerprints.&lt;br /&gt;
						 */&lt;br /&gt;
						return new Promise( function ( resolve ) {&lt;br /&gt;
							window.setTimeout( function () {&lt;br /&gt;
								matches = rankMatches( targets, index );&lt;br /&gt;
								resolve( matches );&lt;br /&gt;
							}, 0 );&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
			} )&lt;br /&gt;
			.then( function ( matches ) {&lt;br /&gt;
				renderMatches( results, matches );&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Done. Showing the &#039; +&lt;br /&gt;
						matches.length +&lt;br /&gt;
						&#039; closest matches.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				results.innerHTML =&lt;br /&gt;
					&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
						&#039;The icon could not be matched.&#039; +&lt;br /&gt;
					&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Icon Finder error: &#039; +&lt;br /&gt;
						( error &amp;amp;&amp;amp; error.message ? error.message : &#039;Unknown error&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildUi( host ) {&lt;br /&gt;
		var api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		host.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		var shell = document.createElement( &#039;div&#039; );&lt;br /&gt;
		shell.className = &#039;eql-iconfinder-shell&#039;;&lt;br /&gt;
&lt;br /&gt;
		var left = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var paste = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var instruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var subinstruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var preview = document.createElement( &#039;img&#039; );&lt;br /&gt;
		var status = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		paste.className = &#039;eql-iconfinder-paste&#039;;&lt;br /&gt;
		paste.tabIndex = 0;&lt;br /&gt;
		paste.setAttribute( &#039;role&#039;, &#039;button&#039; );&lt;br /&gt;
		paste.setAttribute(&lt;br /&gt;
			&#039;aria-label&#039;,&lt;br /&gt;
			&#039;Paste a screen capture of an EverQuest icon&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		instruction.className = &#039;eql-iconfinder-instruction&#039;;&lt;br /&gt;
		instruction.textContent =&lt;br /&gt;
			&#039;Screen capture and paste JUST the Icon&#039;;&lt;br /&gt;
&lt;br /&gt;
		subinstruction.className = &#039;eql-iconfinder-subinstruction&#039;;&lt;br /&gt;
		subinstruction.textContent =&lt;br /&gt;
			&#039;Click this box, then press Ctrl+V. You can also drag an image here.&#039;;&lt;br /&gt;
&lt;br /&gt;
		preview.className = &#039;eql-iconfinder-preview&#039;;&lt;br /&gt;
		preview.alt = &#039;Pasted icon preview&#039;;&lt;br /&gt;
&lt;br /&gt;
		status.className = &#039;eql-iconfinder-status&#039;;&lt;br /&gt;
		status.setAttribute( &#039;aria-live&#039;, &#039;polite&#039; );&lt;br /&gt;
		status.textContent =&lt;br /&gt;
			&#039;Nothing is loaded from Icon List until you paste an image.&#039;;&lt;br /&gt;
&lt;br /&gt;
		paste.appendChild( instruction );&lt;br /&gt;
		paste.appendChild( subinstruction );&lt;br /&gt;
		paste.appendChild( preview );&lt;br /&gt;
&lt;br /&gt;
		left.appendChild( paste );&lt;br /&gt;
		left.appendChild( status );&lt;br /&gt;
&lt;br /&gt;
		var right = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var resultsTitle = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var results = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		resultsTitle.className = &#039;eql-iconfinder-results-title&#039;;&lt;br /&gt;
		resultsTitle.textContent = &#039;Possible Matches&#039;;&lt;br /&gt;
&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Paste an icon to search the uploaded images on Icon List.&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		right.appendChild( resultsTitle );&lt;br /&gt;
		right.appendChild( results );&lt;br /&gt;
&lt;br /&gt;
		shell.appendChild( left );&lt;br /&gt;
		shell.appendChild( right );&lt;br /&gt;
		host.appendChild( shell );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;paste&#039;, function ( event ) {&lt;br /&gt;
			var file = imageFromClipboardEvent( event );&lt;br /&gt;
&lt;br /&gt;
			if ( !file ) {&lt;br /&gt;
				status.textContent =&lt;br /&gt;
					&#039;Clipboard does not contain an image. Screen capture the icon, then paste again.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				file,&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;dragover&#039;, function ( event ) {&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;drop&#039;, function ( event ) {&lt;br /&gt;
			var files;&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
			files = event.dataTransfer &amp;amp;&amp;amp; event.dataTransfer.files;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				!files ||&lt;br /&gt;
				!files.length ||&lt;br /&gt;
				!/^image\//i.test( files[ 0 ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				status.textContent = &#039;Drop an image file here.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				files[ 0 ],&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Focus the paste target immediately when the panel first opens so&lt;br /&gt;
		 * the normal workflow is: click Icon Finder -&amp;gt; Ctrl+V.&lt;br /&gt;
		 */&lt;br /&gt;
		window.setTimeout( function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		}, 0 );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	window.EQLIconFinder = {&lt;br /&gt;
		init: function ( host ) {&lt;br /&gt;
			if ( !host ) {&lt;br /&gt;
				return Promise.reject(&lt;br /&gt;
					new Error( &#039;Icon Finder host element is missing.&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			addStyles();&lt;br /&gt;
&lt;br /&gt;
			if ( initializedHost !== host ) {&lt;br /&gt;
				buildUi( host );&lt;br /&gt;
				initializedHost = host;&lt;br /&gt;
			} else {&lt;br /&gt;
				var paste = host.querySelector( &#039;.eql-iconfinder-paste&#039; );&lt;br /&gt;
&lt;br /&gt;
				if ( paste ) {&lt;br /&gt;
					window.setTimeout( function () {&lt;br /&gt;
						paste.focus();&lt;br /&gt;
					}, 0 );&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return Promise.resolve();&lt;br /&gt;
		}&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168733</id>
		<title>MediaWiki:BlueprintLoader.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168733"/>
		<updated>2026-08-10T05:58:59Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Force refresh of Icon Finder matcher v6&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Editor Tools / Dynamic Blueprint Loader&lt;br /&gt;
 *&lt;br /&gt;
 * Source-editor behavior:&lt;br /&gt;
 * - On a new, truly empty page: show the Help:Contents Blueprint selector.&lt;br /&gt;
 * - On every source edit page: show the Icon Finder button.&lt;br /&gt;
 * - Heavy Icon Finder logic is lazy-loaded only when opened.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var BLUEPRINT_PAGE = &#039;Help:Contents&#039;;&lt;br /&gt;
	var TOOLS_ID = &#039;eql-editor-tools&#039;;&lt;br /&gt;
	var ICON_FINDER_PAGE = &#039;MediaWiki:IconFinder.js&#039;;&lt;br /&gt;
&lt;br /&gt;
	function htmlToText( html ) {&lt;br /&gt;
		var div = document.createElement( &#039;div&#039; );&lt;br /&gt;
		div.innerHTML = String( html || &#039;&#039; );&lt;br /&gt;
		return ( div.textContent || div.innerText || &#039;&#039; ).trim();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeLabel( heading ) {&lt;br /&gt;
		return heading&lt;br /&gt;
			.replace( /\s+Blueprints?\s*$/i, &#039;&#039; )&lt;br /&gt;
			.trim() + &#039; Blueprint&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getEditorContents( $textarea ) {&lt;br /&gt;
		try {&lt;br /&gt;
			return String( $textarea.textSelection( &#039;getContents&#039; ) || &#039;&#039; );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			return String( $textarea.val() || &#039;&#039; );&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setEditorContents( $textarea, text ) {&lt;br /&gt;
		try {&lt;br /&gt;
			$textarea.textSelection( &#039;setContents&#039;, text );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			$textarea.val( text );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea.trigger( &#039;input&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;change&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;focus&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function extractBlueprint( sectionText ) {&lt;br /&gt;
		var match;&lt;br /&gt;
		var blueprint;&lt;br /&gt;
&lt;br /&gt;
		sectionText = String( sectionText || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		match = sectionText.match(&lt;br /&gt;
			/&amp;lt;pre(?:\s[^&amp;gt;]*)?&amp;gt;([\s\S]*?)&amp;lt;\/pre&amp;gt;/i&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		if ( !match ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		blueprint = match[ 1 ];&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint.replace(&lt;br /&gt;
			/^\s*&amp;lt;nowiki&amp;gt;([\s\S]*?)&amp;lt;\/nowiki&amp;gt;\s*$/i,&lt;br /&gt;
			&#039;$1&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint&lt;br /&gt;
			.replace( /^\s*\n/, &#039;&#039; )&lt;br /&gt;
			.replace( /\s+$/, &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		return blueprint + &#039;\n&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintSections( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;tocdata&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var tocdata =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse.tocdata;&lt;br /&gt;
&lt;br /&gt;
			var sections =&lt;br /&gt;
				tocdata &amp;amp;&amp;amp;&lt;br /&gt;
				Array.isArray( tocdata.sections )&lt;br /&gt;
					? tocdata.sections&lt;br /&gt;
					: [];&lt;br /&gt;
&lt;br /&gt;
			if ( sections.length ) {&lt;br /&gt;
				return sections;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return api.get( {&lt;br /&gt;
				action: &#039;parse&#039;,&lt;br /&gt;
				page: BLUEPRINT_PAGE,&lt;br /&gt;
				prop: &#039;sections&#039;,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			} ).then( function ( fallbackData ) {&lt;br /&gt;
				return (&lt;br /&gt;
					fallbackData &amp;amp;&amp;amp;&lt;br /&gt;
					fallbackData.parse &amp;amp;&amp;amp;&lt;br /&gt;
					Array.isArray( fallbackData.parse.sections )&lt;br /&gt;
				)&lt;br /&gt;
					? fallbackData.parse.sections&lt;br /&gt;
					: [];&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeBlueprintSections( sections ) {&lt;br /&gt;
		return sections&lt;br /&gt;
			.map( function ( section ) {&lt;br /&gt;
				var heading = htmlToText( section.line || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
				return {&lt;br /&gt;
					heading: heading,&lt;br /&gt;
					label: makeLabel( heading ),&lt;br /&gt;
					index: String( section.index || &#039;&#039; ),&lt;br /&gt;
					anchor:&lt;br /&gt;
						section.linkAnchor ||&lt;br /&gt;
						section.anchor ||&lt;br /&gt;
						&#039;&#039;&lt;br /&gt;
				};&lt;br /&gt;
			} )&lt;br /&gt;
			.filter( function ( section ) {&lt;br /&gt;
				return (&lt;br /&gt;
					section.index &amp;amp;&amp;amp;&lt;br /&gt;
					/\bBlueprints?\s*$/i.test( section.heading )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintWikitext( api, sectionIndex ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;wikitext&#039;,&lt;br /&gt;
			section: sectionIndex,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var wikitext =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse&lt;br /&gt;
					? data.parse.wikitext&lt;br /&gt;
					: &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				wikitext &amp;amp;&amp;amp;&lt;br /&gt;
				typeof wikitext === &#039;object&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				Object.prototype.hasOwnProperty.call( wikitext, &#039;*&#039; )&lt;br /&gt;
			) {&lt;br /&gt;
				wikitext = wikitext[ &#039;*&#039; ];&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return String( wikitext || &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-editor-tools-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-editor-tools-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039;{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;margin:0 0 1rem 0;&#039; +&lt;br /&gt;
					&#039;padding:1rem 1.1rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,rgba(16,21,29,.97),rgba(9,13,19,.98));&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.38);&#039; +&lt;br /&gt;
					&#039;border-left:5px solid rgba(216,183,92,.82);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 9px 24px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.035);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(0,1fr) auto;&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:end;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-title,&#039; +&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .35rem 0;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;letter-spacing:.035em;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-description{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .7rem 0;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-controls{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;min-width:260px;&#039; +&lt;br /&gt;
					&#039;max-width:100%;&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .55rem;&#039; +&lt;br /&gt;
					&#039;background:#0b1017;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.36);&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#e2e2cf;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button{&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .85rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,#806d32,#594817);&#039; +&lt;br /&gt;
					&#039;border:1px solid #b69c4b;&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#fff5cc;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;cursor:pointer;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:hover:not(:disabled){&#039; +&lt;br /&gt;
					&#039;filter:brightness(1.08);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:disabled{&#039; +&lt;br /&gt;
					&#039;cursor:default;&#039; +&lt;br /&gt;
					&#039;opacity:.55;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-source{&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-status{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-top:.55rem;&#039; +&lt;br /&gt;
					&#039;min-height:1.2em;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:stretch;&#039; +&lt;br /&gt;
					&#039;justify-content:flex-end;&#039; +&lt;br /&gt;
					&#039;min-width:150px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-toggle{&#039; +&lt;br /&gt;
					&#039;white-space:nowrap;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host{&#039; +&lt;br /&gt;
					&#039;margin-top:1rem;&#039; +&lt;br /&gt;
					&#039;padding-top:1rem;&#039; +&lt;br /&gt;
					&#039;border-top:1px solid rgba(216,183,92,.22);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host[hidden]{display:none!important;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:760px){&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
						&#039;grid-template-columns:1fr;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
						&#039;width:100%;&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function loadIconFinderScript() {&lt;br /&gt;
		if ( window.EQLIconFinder ) {&lt;br /&gt;
			return Promise.resolve( window.EQLIconFinder );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( window.eqlIconFinderLoadingPromise ) {&lt;br /&gt;
			return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.eqlIconFinderLoadingPromise = new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var script = document.createElement( &#039;script&#039; );&lt;br /&gt;
&lt;br /&gt;
			script.src = mw.util.getUrl( ICON_FINDER_PAGE, {&lt;br /&gt;
				action: &#039;raw&#039;,&lt;br /&gt;
				ctype: &#039;text/javascript&#039;,&lt;br /&gt;
				v: &#039;6&#039;&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			script.async = true;&lt;br /&gt;
&lt;br /&gt;
			script.onload = function () {&lt;br /&gt;
				if ( window.EQLIconFinder ) {&lt;br /&gt;
					resolve( window.EQLIconFinder );&lt;br /&gt;
				} else {&lt;br /&gt;
					reject( new Error( &#039;IconFinder.js loaded without exposing EQLIconFinder.&#039; ) );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			script.onerror = function () {&lt;br /&gt;
				reject( new Error( &#039;Could not load IconFinder.js.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			document.head.appendChild( script );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildIconFinderLaunch( $row, $tools ) {&lt;br /&gt;
		var $launch = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-launch&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-title&#039;,&lt;br /&gt;
			text: &#039;ICON FINDER&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-toggle&#039;,&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Icon Finder&#039;,&lt;br /&gt;
			&#039;aria-expanded&#039;: &#039;false&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $host = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-host&#039;,&lt;br /&gt;
			hidden: true&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var opening = $host.prop( &#039;hidden&#039; );&lt;br /&gt;
&lt;br /&gt;
			if ( !opening ) {&lt;br /&gt;
				$host.prop( &#039;hidden&#039;, true );&lt;br /&gt;
				$button.attr( &#039;aria-expanded&#039;, &#039;false&#039; );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$button.text( &#039;Loading…&#039; );&lt;br /&gt;
&lt;br /&gt;
			loadIconFinderScript()&lt;br /&gt;
				.then( function ( finder ) {&lt;br /&gt;
					return finder.init( $host[ 0 ] );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$host.prop( &#039;hidden&#039;, false );&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$host&lt;br /&gt;
						.prop( &#039;hidden&#039;, false )&lt;br /&gt;
						.text( &#039;Icon Finder could not be loaded.&#039; );&lt;br /&gt;
&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.text( &#039;Icon Finder&#039; );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildBlueprintArea( $row, $textarea, api ) {&lt;br /&gt;
		var $area = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-area&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-title&#039;,&lt;br /&gt;
			text: &#039;CREATE FROM A BLUEPRINT&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-description&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;Load a standard page skeleton from Help:Contents, &#039; +&lt;br /&gt;
				&#039;or continue editing the current source below.&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $controls = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-controls&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $select = $( &#039;&amp;lt;select&amp;gt;&#039;, {&lt;br /&gt;
			&#039;aria-label&#039;: &#039;Select page blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
			value: &#039;&#039;,&lt;br /&gt;
			text: &#039;Loading blueprints...&#039;&lt;br /&gt;
		} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Load Blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $sourceLink = $( &#039;&amp;lt;a&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-source&#039;,&lt;br /&gt;
			href: mw.util.getUrl( BLUEPRINT_PAGE ),&lt;br /&gt;
			text: &#039;View Blueprints&#039;,&lt;br /&gt;
			target: &#039;_blank&#039;,&lt;br /&gt;
			rel: &#039;noopener&#039;&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $status = $( &#039;&amp;lt;span&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-status&#039;,&lt;br /&gt;
			&#039;aria-live&#039;: &#039;polite&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		fetchBlueprintSections( api )&lt;br /&gt;
			.then( function ( sections ) {&lt;br /&gt;
				var blueprints = normalizeBlueprintSections( sections );&lt;br /&gt;
&lt;br /&gt;
				$select.empty();&lt;br /&gt;
&lt;br /&gt;
				$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
					value: &#039;&#039;,&lt;br /&gt;
					text: blueprints.length ?&lt;br /&gt;
						&#039;Select a blueprint...&#039; :&lt;br /&gt;
						&#039;No blueprints found&#039;&lt;br /&gt;
				} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
				blueprints.forEach( function ( blueprint ) {&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: blueprint.index,&lt;br /&gt;
						text: blueprint.label&lt;br /&gt;
					} )&lt;br /&gt;
						.attr( &#039;data-anchor&#039;, blueprint.anchor )&lt;br /&gt;
						.appendTo( $select );&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
				$select.prop( &#039;disabled&#039;, !blueprints.length );&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				$select.empty().append(&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: &#039;&#039;,&lt;br /&gt;
						text: &#039;Could not load blueprints&#039;&lt;br /&gt;
					} )&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				$status.text(&lt;br /&gt;
					&#039;Could not read Blueprint headings from Help:Contents.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
		$select.on( &#039;change&#039;, function () {&lt;br /&gt;
			var $option = $select.find( &#039;option:selected&#039; );&lt;br /&gt;
			var anchor = $option.attr( &#039;data-anchor&#039; ) || &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
&lt;br /&gt;
			if ( anchor ) {&lt;br /&gt;
				$sourceLink.attr(&lt;br /&gt;
					&#039;href&#039;,&lt;br /&gt;
					mw.util.getUrl( BLUEPRINT_PAGE ) + &#039;#&#039; + anchor&lt;br /&gt;
				);&lt;br /&gt;
				$sourceLink.text( &#039;View This Blueprint&#039; );&lt;br /&gt;
			} else {&lt;br /&gt;
				$sourceLink.attr( &#039;href&#039;, mw.util.getUrl( BLUEPRINT_PAGE ) );&lt;br /&gt;
				$sourceLink.text( &#039;View Blueprints&#039; );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$status.text( &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var sectionIndex = $select.val();&lt;br /&gt;
			var existing;&lt;br /&gt;
&lt;br /&gt;
			if ( !sectionIndex ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			existing = getEditorContents( $textarea );&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				existing.trim() &amp;amp;&amp;amp;&lt;br /&gt;
				!window.confirm(&lt;br /&gt;
					&#039;The editor already contains text. Replace it with the selected blueprint?&#039;&lt;br /&gt;
				)&lt;br /&gt;
			) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$select.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$status.text( &#039;Loading current blueprint from Help:Contents...&#039; );&lt;br /&gt;
&lt;br /&gt;
			fetchBlueprintWikitext( api, sectionIndex )&lt;br /&gt;
				.then( function ( sectionText ) {&lt;br /&gt;
					var blueprint = extractBlueprint( sectionText );&lt;br /&gt;
&lt;br /&gt;
					if ( blueprint === null ) {&lt;br /&gt;
						throw new Error( &#039;No &amp;lt;pre&amp;gt; block found.&#039; );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					setEditorContents( $textarea, blueprint );&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Blueprint loaded. Edit the fields below, then publish normally.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Could not load this blueprint. Check Help:Contents and make sure the section contains a &amp;lt;pre&amp;gt; block.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$select.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function init() {&lt;br /&gt;
		var action = mw.config.get( &#039;wgAction&#039; );&lt;br /&gt;
		var $textarea;&lt;br /&gt;
		var $tools;&lt;br /&gt;
		var $row;&lt;br /&gt;
		var $editorHost;&lt;br /&gt;
		var api;&lt;br /&gt;
&lt;br /&gt;
		if ( [ &#039;edit&#039;, &#039;submit&#039; ].indexOf( action ) === -1 ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea = $( &#039;#wpTextbox1&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$textarea.length ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( document.getElementById( TOOLS_ID ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		addStyles();&lt;br /&gt;
&lt;br /&gt;
		$tools = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: TOOLS_ID&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$row = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-editor-tools-row&#039;&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		buildBlueprintArea( $row, $textarea, api );&lt;br /&gt;
&lt;br /&gt;
		buildIconFinderLaunch( $row, $tools );&lt;br /&gt;
&lt;br /&gt;
		$editorHost = $textarea.closest( &#039;.wikiEditor-ui&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$editorHost.length ) {&lt;br /&gt;
			$editorHost = $textarea;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$tools.insertBefore( $editorHost );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	mw.loader.using( [&lt;br /&gt;
		&#039;mediawiki.api&#039;,&lt;br /&gt;
		&#039;mediawiki.util&#039;,&lt;br /&gt;
		&#039;jquery.textSelection&#039;&lt;br /&gt;
	] ).then( function () {&lt;br /&gt;
		$( init );&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168732</id>
		<title>MediaWiki:IconFinder.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168732"/>
		<updated>2026-08-10T05:58:58Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Improve Icon Finder foreground and color matching&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Icon Finder&lt;br /&gt;
 *&lt;br /&gt;
 * Lazy-loaded from MediaWiki:BlueprintLoader.js.&lt;br /&gt;
 *&lt;br /&gt;
 * Matching workflow:&lt;br /&gt;
 * 1. User pastes a screen capture containing JUST the icon.&lt;br /&gt;
 * 2. Read the current image list from [[Icon List]].&lt;br /&gt;
 * 3. Resolve only existing uploaded files through imageinfo.&lt;br /&gt;
 * 4. Build compact perceptual fingerprints in the browser.&lt;br /&gt;
 * 5. Cache that fingerprint index in IndexedDB keyed to the Icon List revision.&lt;br /&gt;
 * 6. Rank possible matches by perceptual/pixel similarity.&lt;br /&gt;
 *&lt;br /&gt;
 * No icon-catalog requests or image downloads happen until the user actually&lt;br /&gt;
 * pastes an image into the Icon Finder.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var ICON_LIST_PAGE = &#039;Icon List&#039;;&lt;br /&gt;
	var DB_NAME = &#039;eql-icon-finder&#039;;&lt;br /&gt;
	var DB_VERSION = 1;&lt;br /&gt;
	var STORE_NAME = &#039;indexes&#039;;&lt;br /&gt;
	var ALGO_VERSION = 6;&lt;br /&gt;
	var COLOR_GRID = 16;&lt;br /&gt;
	var IMAGEINFO_BATCH = 50;&lt;br /&gt;
	var DOWNLOAD_CONCURRENCY = 8;&lt;br /&gt;
	var RESULT_COUNT = 10;&lt;br /&gt;
&lt;br /&gt;
	var initializedHost = null;&lt;br /&gt;
	var memoryIndex = null;&lt;br /&gt;
	var currentPreviewUrl = null;&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-icon-finder-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;.eql-iconfinder-shell{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(230px,.62fr) minmax(0,1.38fr);&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:start;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste{&#039; +&lt;br /&gt;
					&#039;min-height:160px;&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;justify-content:center;&#039; +&lt;br /&gt;
					&#039;gap:.45rem;&#039; +&lt;br /&gt;
					&#039;padding:1rem;&#039; +&lt;br /&gt;
					&#039;background:#080d13;&#039; +&lt;br /&gt;
					&#039;border:2px dashed rgba(216,183,92,.45);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;color:#dfe7ef;&#039; +&lt;br /&gt;
					&#039;text-align:center;&#039; +&lt;br /&gt;
					&#039;cursor:paste;&#039; +&lt;br /&gt;
					&#039;outline:none;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste:focus{&#039; +&lt;br /&gt;
					&#039;border-color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 0 0 2px rgba(216,183,92,.13);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-instruction{&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-subinstruction{&#039; +&lt;br /&gt;
					&#039;color:#9faaba;&#039; +&lt;br /&gt;
					&#039;font-size:.88rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview{&#039; +&lt;br /&gt;
					&#039;display:none;&#039; +&lt;br /&gt;
					&#039;width:72px;&#039; +&lt;br /&gt;
					&#039;height:72px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;image-rendering:auto;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.14);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview.is-visible{display:block;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-status{&#039; +&lt;br /&gt;
					&#039;margin-top:.65rem;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.4;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .55rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-empty{&#039; +&lt;br /&gt;
					&#039;padding:.9rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.025);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.08);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
					&#039;color:#96a2b2;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-grid{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:54px minmax(0,1fr);&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;padding:.65rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.03);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.09);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result img{&#039; +&lt;br /&gt;
					&#039;width:52px;&#039; +&lt;br /&gt;
					&#039;height:52px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.1);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-name{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.18rem;&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.2;&#039; +&lt;br /&gt;
					&#039;word-break:break-word;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-score{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.28rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-size:.8rem;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-meta{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;color:#8f9bac;&#039; +&lt;br /&gt;
					&#039;font-size:.76rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.25;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-id{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin:.2rem 0 .35rem;&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:800;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy-row{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;gap:.3rem;&#039; +&lt;br /&gt;
					&#039;margin-top:.38rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy{&#039; +&lt;br /&gt;
					&#039;min-height:1.8rem!important;&#039; +&lt;br /&gt;
					&#039;padding:.15rem .45rem!important;&#039; +&lt;br /&gt;
					&#039;font-size:.72rem!important;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-note{&#039; +&lt;br /&gt;
					&#039;margin-top:.7rem;&#039; +&lt;br /&gt;
					&#039;color:#7f8b9b;&#039; +&lt;br /&gt;
					&#039;font-size:.78rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:850px){&#039; +&lt;br /&gt;
					&#039;.eql-iconfinder-shell{grid-template-columns:1fr;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function openDb() {&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var request;&lt;br /&gt;
&lt;br /&gt;
			if ( !window.indexedDB ) {&lt;br /&gt;
				reject( new Error( &#039;IndexedDB unavailable.&#039; ) );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			request = indexedDB.open( DB_NAME, DB_VERSION );&lt;br /&gt;
&lt;br /&gt;
			request.onupgradeneeded = function () {&lt;br /&gt;
				var db = request.result;&lt;br /&gt;
&lt;br /&gt;
				if ( !db.objectStoreNames.contains( STORE_NAME ) ) {&lt;br /&gt;
					db.createObjectStore( STORE_NAME, {&lt;br /&gt;
						keyPath: &#039;key&#039;&lt;br /&gt;
					} );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onsuccess = function () {&lt;br /&gt;
				resolve( request.result );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onerror = function () {&lt;br /&gt;
				reject( request.error || new Error( &#039;IndexedDB open failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbGet( key ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readonly&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var request = store.get( key );&lt;br /&gt;
&lt;br /&gt;
				request.onsuccess = function () {&lt;br /&gt;
					resolve( request.result || null );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				request.onerror = function () {&lt;br /&gt;
					reject( request.error || new Error( &#039;IndexedDB read failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbPutAndPrune( value ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readwrite&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var cursorRequest;&lt;br /&gt;
&lt;br /&gt;
				store.put( value );&lt;br /&gt;
&lt;br /&gt;
				cursorRequest = store.openCursor();&lt;br /&gt;
&lt;br /&gt;
				cursorRequest.onsuccess = function () {&lt;br /&gt;
					var cursor = cursorRequest.result;&lt;br /&gt;
&lt;br /&gt;
					if ( !cursor ) {&lt;br /&gt;
						return;&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					if ( cursor.key !== value.key ) {&lt;br /&gt;
						cursor.delete();&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					cursor.continue();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					resolve();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.onerror = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					reject( tx.error || new Error( &#039;IndexedDB write failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeFileTitle( title ) {&lt;br /&gt;
		title = String( title || &#039;&#039; ).trim();&lt;br /&gt;
&lt;br /&gt;
		if ( !title ) {&lt;br /&gt;
			return &#039;&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return /^File:/i.test( title )&lt;br /&gt;
			? title&lt;br /&gt;
			: &#039;File:&#039; + title;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchCatalog( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: ICON_LIST_PAGE,&lt;br /&gt;
			prop: &#039;images|revid&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var parse = data &amp;amp;&amp;amp; data.parse ? data.parse : {};&lt;br /&gt;
			var titles = Array.isArray( parse.images ) ? parse.images : [];&lt;br /&gt;
&lt;br /&gt;
			titles = titles&lt;br /&gt;
				.map( normalizeFileTitle )&lt;br /&gt;
				.filter( Boolean )&lt;br /&gt;
				.filter( function ( title, index, list ) {&lt;br /&gt;
					return list.indexOf( title ) === index;&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				revid: Number( parse.revid ) || 0,&lt;br /&gt;
				titles: titles&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function chunk( list, size ) {&lt;br /&gt;
		var out = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; list.length; i += size ) {&lt;br /&gt;
			out.push( list.slice( i, i + size ) );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchImageInfo( api, titles, setStatus ) {&lt;br /&gt;
		var batches = chunk( titles, IMAGEINFO_BATCH );&lt;br /&gt;
		var records = [];&lt;br /&gt;
		var chain = Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
		batches.forEach( function ( batch, batchIndex ) {&lt;br /&gt;
			chain = chain.then( function () {&lt;br /&gt;
				if ( setStatus ) {&lt;br /&gt;
					setStatus(&lt;br /&gt;
						&#039;Reading uploaded icon files… &#039; +&lt;br /&gt;
						Math.min( batchIndex * IMAGEINFO_BATCH, titles.length ) +&lt;br /&gt;
						&#039; / &#039; + titles.length&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return api.get( {&lt;br /&gt;
					action: &#039;query&#039;,&lt;br /&gt;
					prop: &#039;imageinfo&#039;,&lt;br /&gt;
					titles: batch.join( &#039;|&#039; ),&lt;br /&gt;
					iiprop: &#039;url|size|sha1&#039;,&lt;br /&gt;
					formatversion: 2&lt;br /&gt;
				} ).then( function ( data ) {&lt;br /&gt;
					var pages =&lt;br /&gt;
						data &amp;amp;&amp;amp;&lt;br /&gt;
						data.query &amp;amp;&amp;amp;&lt;br /&gt;
						Array.isArray( data.query.pages )&lt;br /&gt;
							? data.query.pages&lt;br /&gt;
							: [];&lt;br /&gt;
&lt;br /&gt;
					pages.forEach( function ( page ) {&lt;br /&gt;
						var info =&lt;br /&gt;
							page &amp;amp;&amp;amp;&lt;br /&gt;
							Array.isArray( page.imageinfo ) &amp;amp;&amp;amp;&lt;br /&gt;
							page.imageinfo.length&lt;br /&gt;
								? page.imageinfo[ 0 ]&lt;br /&gt;
								: null;&lt;br /&gt;
&lt;br /&gt;
						if ( !info || !info.url ) {&lt;br /&gt;
							return;&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						records.push( {&lt;br /&gt;
							title: normalizeFileTitle( page.title ),&lt;br /&gt;
							url: String( info.url ),&lt;br /&gt;
							sha1: String( info.sha1 || &#039;&#039; ),&lt;br /&gt;
							width: Number( info.width ) || 0,&lt;br /&gt;
							height: Number( info.height ) || 0&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return chain.then( function () {&lt;br /&gt;
			return records;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function blobToDrawable( blob ) {&lt;br /&gt;
		if ( window.createImageBitmap ) {&lt;br /&gt;
			return createImageBitmap( blob );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var url = URL.createObjectURL( blob );&lt;br /&gt;
			var img = new Image();&lt;br /&gt;
&lt;br /&gt;
			img.onload = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				resolve( img );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.onerror = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				reject( new Error( &#039;Image decode failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.src = url;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchDrawable( url ) {&lt;br /&gt;
		return fetch( url, {&lt;br /&gt;
			credentials: &#039;same-origin&#039;,&lt;br /&gt;
			cache: &#039;force-cache&#039;&lt;br /&gt;
		} )&lt;br /&gt;
			.then( function ( response ) {&lt;br /&gt;
				if ( !response.ok ) {&lt;br /&gt;
					throw new Error( &#039;HTTP &#039; + response.status );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return response.blob();&lt;br /&gt;
			} )&lt;br /&gt;
			.then( blobToDrawable );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function closeDrawable( drawable ) {&lt;br /&gt;
		if ( drawable &amp;amp;&amp;amp; typeof drawable.close === &#039;function&#039; ) {&lt;br /&gt;
			drawable.close();&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function drawScaled( drawable, width, height, cropFraction ) {&lt;br /&gt;
		var sourceCanvas = document.createElement( &#039;canvas&#039; );&lt;br /&gt;
		var sourceWidth =&lt;br /&gt;
			Number( drawable.width || drawable.naturalWidth ) || 1;&lt;br /&gt;
		var sourceHeight =&lt;br /&gt;
			Number( drawable.height || drawable.naturalHeight ) || 1;&lt;br /&gt;
		var sourceCtx;&lt;br /&gt;
		var sourceImage;&lt;br /&gt;
		var sourceData;&lt;br /&gt;
		var pixelCount = sourceWidth * sourceHeight;&lt;br /&gt;
		var transparentCount = 0;&lt;br /&gt;
		var mask = new Uint8Array( pixelCount );&lt;br /&gt;
		var foregroundCount = 0;&lt;br /&gt;
		var borderR = [];&lt;br /&gt;
		var borderG = [];&lt;br /&gt;
		var borderB = [];&lt;br /&gt;
		var bgR = 0;&lt;br /&gt;
		var bgG = 0;&lt;br /&gt;
		var bgB = 0;&lt;br /&gt;
		var bgLuma = 0;&lt;br /&gt;
		var borderSpread = [];&lt;br /&gt;
		var tolerance;&lt;br /&gt;
		var darkCutoff;&lt;br /&gt;
		var minX = sourceWidth;&lt;br /&gt;
		var minY = sourceHeight;&lt;br /&gt;
		var maxX = -1;&lt;br /&gt;
		var maxY = -1;&lt;br /&gt;
		var cleanCanvas;&lt;br /&gt;
		var cleanCtx;&lt;br /&gt;
		var cleanImage;&lt;br /&gt;
		var targetCanvas;&lt;br /&gt;
		var targetCtx;&lt;br /&gt;
		var usableWidth;&lt;br /&gt;
		var usableHeight;&lt;br /&gt;
		var scale;&lt;br /&gt;
		var drawWidth;&lt;br /&gt;
		var drawHeight;&lt;br /&gt;
		var drawX;&lt;br /&gt;
		var drawY;&lt;br /&gt;
		var i;&lt;br /&gt;
		var p;&lt;br /&gt;
		var x;&lt;br /&gt;
		var y;&lt;br /&gt;
		var r;&lt;br /&gt;
		var g;&lt;br /&gt;
		var b;&lt;br /&gt;
		var a;&lt;br /&gt;
		var luma;&lt;br /&gt;
		var chroma;&lt;br /&gt;
		var maxChannel;&lt;br /&gt;
		var minChannel;&lt;br /&gt;
		var colorDistance;&lt;br /&gt;
&lt;br /&gt;
		function median( values ) {&lt;br /&gt;
			var sorted;&lt;br /&gt;
&lt;br /&gt;
			if ( !values.length ) {&lt;br /&gt;
				return 0;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			sorted = values.slice().sort(&lt;br /&gt;
				function ( left, right ) {&lt;br /&gt;
					return left - right;&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			return sorted[&lt;br /&gt;
				Math.floor( sorted.length / 2 )&lt;br /&gt;
			];&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		function addBorderPixel( px, py ) {&lt;br /&gt;
			var index =&lt;br /&gt;
				( py * sourceWidth + px ) * 4;&lt;br /&gt;
&lt;br /&gt;
			if ( sourceData[ index + 3 ] &amp;lt; 16 ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			borderR.push( sourceData[ index ] );&lt;br /&gt;
			borderG.push( sourceData[ index + 1 ] );&lt;br /&gt;
			borderB.push( sourceData[ index + 2 ] );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		sourceCanvas.width = sourceWidth;&lt;br /&gt;
		sourceCanvas.height = sourceHeight;&lt;br /&gt;
&lt;br /&gt;
		sourceCtx = sourceCanvas.getContext(&lt;br /&gt;
			&#039;2d&#039;,&lt;br /&gt;
			{&lt;br /&gt;
				willReadFrequently: true&lt;br /&gt;
			}&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Preserve real PNG transparency.&lt;br /&gt;
		 */&lt;br /&gt;
		sourceCtx.clearRect(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			sourceWidth,&lt;br /&gt;
			sourceHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceCtx.drawImage(&lt;br /&gt;
			drawable,&lt;br /&gt;
			0,&lt;br /&gt;
			0&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceImage = sourceCtx.getImageData(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			sourceWidth,&lt;br /&gt;
			sourceHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		sourceData = sourceImage.data;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; pixelCount; i++ ) {&lt;br /&gt;
			if ( sourceData[ i * 4 + 3 ] &amp;lt; 245 ) {&lt;br /&gt;
				transparentCount++;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			transparentCount &amp;gt;&lt;br /&gt;
			Math.max( 4, pixelCount * 0.01 )&lt;br /&gt;
		) {&lt;br /&gt;
			/*&lt;br /&gt;
			 * Uploaded transparent PNG:&lt;br /&gt;
			 * alpha is already an excellent mask.&lt;br /&gt;
			 */&lt;br /&gt;
			for ( i = 0; i &amp;lt; pixelCount; i++ ) {&lt;br /&gt;
				if (&lt;br /&gt;
					sourceData[ i * 4 + 3 ] &amp;gt; 24&lt;br /&gt;
				) {&lt;br /&gt;
					mask[ i ] = 1;&lt;br /&gt;
					foregroundCount++;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		} else {&lt;br /&gt;
			/*&lt;br /&gt;
			 * Pasted screenshot:&lt;br /&gt;
			 *&lt;br /&gt;
			 * Infer the UI background from a two-pixel&lt;br /&gt;
			 * border around the capture.&lt;br /&gt;
			 */&lt;br /&gt;
			for (&lt;br /&gt;
				x = 0;&lt;br /&gt;
				x &amp;lt; sourceWidth;&lt;br /&gt;
				x++&lt;br /&gt;
			) {&lt;br /&gt;
				for (&lt;br /&gt;
					y = 0;&lt;br /&gt;
					y &amp;lt; Math.min(&lt;br /&gt;
						2,&lt;br /&gt;
						sourceHeight&lt;br /&gt;
					);&lt;br /&gt;
					y++&lt;br /&gt;
				) {&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						sourceHeight - 1 - y&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				y = 0;&lt;br /&gt;
				y &amp;lt; sourceHeight;&lt;br /&gt;
				y++&lt;br /&gt;
			) {&lt;br /&gt;
				for (&lt;br /&gt;
					x = 0;&lt;br /&gt;
					x &amp;lt; Math.min(&lt;br /&gt;
						2,&lt;br /&gt;
						sourceWidth&lt;br /&gt;
					);&lt;br /&gt;
					x++&lt;br /&gt;
				) {&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					addBorderPixel(&lt;br /&gt;
						sourceWidth - 1 - x,&lt;br /&gt;
						y&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			bgR = median( borderR );&lt;br /&gt;
			bgG = median( borderG );&lt;br /&gt;
			bgB = median( borderB );&lt;br /&gt;
&lt;br /&gt;
			bgLuma = luminance(&lt;br /&gt;
				bgR,&lt;br /&gt;
				bgG,&lt;br /&gt;
				bgB&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				i = 0;&lt;br /&gt;
				i &amp;lt; borderR.length;&lt;br /&gt;
				i++&lt;br /&gt;
			) {&lt;br /&gt;
				borderSpread.push(&lt;br /&gt;
					Math.sqrt(&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderR[ i ] - bgR,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderG[ i ] - bgG,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							borderB[ i ] - bgB,&lt;br /&gt;
							2&lt;br /&gt;
						)&lt;br /&gt;
					)&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * The old crop variants blindly trimmed the&lt;br /&gt;
			 * image square.&lt;br /&gt;
			 *&lt;br /&gt;
			 * Now they instead make background removal&lt;br /&gt;
			 * slightly more aggressive.&lt;br /&gt;
			 */&lt;br /&gt;
			tolerance = Math.max(&lt;br /&gt;
				20,&lt;br /&gt;
				Math.min(&lt;br /&gt;
					74,&lt;br /&gt;
					18 +&lt;br /&gt;
						median(&lt;br /&gt;
							borderSpread&lt;br /&gt;
						) * 2.5 +&lt;br /&gt;
						cropFraction * 80&lt;br /&gt;
				)&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			darkCutoff = Math.max(&lt;br /&gt;
				36,&lt;br /&gt;
				Math.min(&lt;br /&gt;
					88,&lt;br /&gt;
					bgLuma +&lt;br /&gt;
						22 +&lt;br /&gt;
						cropFraction * 100&lt;br /&gt;
				)&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			for (&lt;br /&gt;
				i = 0;&lt;br /&gt;
				i &amp;lt; pixelCount;&lt;br /&gt;
				i++&lt;br /&gt;
			) {&lt;br /&gt;
				p = i * 4;&lt;br /&gt;
&lt;br /&gt;
				r = sourceData[ p ];&lt;br /&gt;
				g = sourceData[ p + 1 ];&lt;br /&gt;
				b = sourceData[ p + 2 ];&lt;br /&gt;
				a = sourceData[ p + 3 ];&lt;br /&gt;
&lt;br /&gt;
				if ( a &amp;lt; 16 ) {&lt;br /&gt;
					continue;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				maxChannel = Math.max(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				minChannel = Math.min(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				chroma =&lt;br /&gt;
					maxChannel -&lt;br /&gt;
					minChannel;&lt;br /&gt;
&lt;br /&gt;
				luma = luminance(&lt;br /&gt;
					r,&lt;br /&gt;
					g,&lt;br /&gt;
					b&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				colorDistance =&lt;br /&gt;
					Math.sqrt(&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							r - bgR,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							g - bgG,&lt;br /&gt;
							2&lt;br /&gt;
						) +&lt;br /&gt;
						Math.pow(&lt;br /&gt;
							b - bgB,&lt;br /&gt;
							2&lt;br /&gt;
						)&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				/*&lt;br /&gt;
				 * EQ&#039;s editor/game icon background is&lt;br /&gt;
				 * normally very dark.&lt;br /&gt;
				 *&lt;br /&gt;
				 * Discard dark neutral pixels, but allow&lt;br /&gt;
				 * strongly colored pixels to survive at&lt;br /&gt;
				 * lower brightness.&lt;br /&gt;
				 */&lt;br /&gt;
				if ( bgLuma &amp;lt; 95 ) {&lt;br /&gt;
					if (&lt;br /&gt;
						(&lt;br /&gt;
							luma &amp;gt;=&lt;br /&gt;
								darkCutoff &amp;amp;&amp;amp;&lt;br /&gt;
							colorDistance &amp;gt;=&lt;br /&gt;
								tolerance *&lt;br /&gt;
								0.65&lt;br /&gt;
						) ||&lt;br /&gt;
						(&lt;br /&gt;
							chroma &amp;gt;= 24 &amp;amp;&amp;amp;&lt;br /&gt;
							luma &amp;gt;=&lt;br /&gt;
								bgLuma +&lt;br /&gt;
								7 +&lt;br /&gt;
								cropFraction *&lt;br /&gt;
								30 &amp;amp;&amp;amp;&lt;br /&gt;
							colorDistance &amp;gt;=&lt;br /&gt;
								tolerance *&lt;br /&gt;
								0.55&lt;br /&gt;
						)&lt;br /&gt;
					) {&lt;br /&gt;
						mask[ i ] = 1;&lt;br /&gt;
						foregroundCount++;&lt;br /&gt;
					}&lt;br /&gt;
				} else if (&lt;br /&gt;
					colorDistance &amp;gt;=&lt;br /&gt;
					tolerance&lt;br /&gt;
				) {&lt;br /&gt;
					mask[ i ] = 1;&lt;br /&gt;
					foregroundCount++;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * Fail soft for an unusual image instead of&lt;br /&gt;
			 * producing an empty fingerprint.&lt;br /&gt;
			 */&lt;br /&gt;
			if (&lt;br /&gt;
				foregroundCount &amp;lt; 6 ||&lt;br /&gt;
				foregroundCount &amp;gt;&lt;br /&gt;
					pixelCount * 0.90&lt;br /&gt;
			) {&lt;br /&gt;
				mask.fill( 0 );&lt;br /&gt;
				foregroundCount = 0;&lt;br /&gt;
&lt;br /&gt;
				for (&lt;br /&gt;
					i = 0;&lt;br /&gt;
					i &amp;lt; pixelCount;&lt;br /&gt;
					i++&lt;br /&gt;
				) {&lt;br /&gt;
					if (&lt;br /&gt;
						sourceData[&lt;br /&gt;
							i * 4 + 3&lt;br /&gt;
						] &amp;gt; 24&lt;br /&gt;
					) {&lt;br /&gt;
						mask[ i ] = 1;&lt;br /&gt;
						foregroundCount++;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Find the actual sprite bounds.&lt;br /&gt;
		 */&lt;br /&gt;
		for (&lt;br /&gt;
			y = 0;&lt;br /&gt;
			y &amp;lt; sourceHeight;&lt;br /&gt;
			y++&lt;br /&gt;
		) {&lt;br /&gt;
			for (&lt;br /&gt;
				x = 0;&lt;br /&gt;
				x &amp;lt; sourceWidth;&lt;br /&gt;
				x++&lt;br /&gt;
			) {&lt;br /&gt;
				i =&lt;br /&gt;
					y * sourceWidth +&lt;br /&gt;
					x;&lt;br /&gt;
&lt;br /&gt;
				if ( !mask[ i ] ) {&lt;br /&gt;
					continue;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				minX = Math.min(&lt;br /&gt;
					minX,&lt;br /&gt;
					x&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				minY = Math.min(&lt;br /&gt;
					minY,&lt;br /&gt;
					y&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				maxX = Math.max(&lt;br /&gt;
					maxX,&lt;br /&gt;
					x&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				maxY = Math.max(&lt;br /&gt;
					maxY,&lt;br /&gt;
					y&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			maxX &amp;lt; minX ||&lt;br /&gt;
			maxY &amp;lt; minY&lt;br /&gt;
		) {&lt;br /&gt;
			minX = 0;&lt;br /&gt;
			minY = 0;&lt;br /&gt;
			maxX = sourceWidth - 1;&lt;br /&gt;
			maxY = sourceHeight - 1;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Create a transparent copy containing only&lt;br /&gt;
		 * detected foreground pixels.&lt;br /&gt;
		 */&lt;br /&gt;
		cleanCanvas =&lt;br /&gt;
			document.createElement(&lt;br /&gt;
				&#039;canvas&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		cleanCanvas.width =&lt;br /&gt;
			sourceWidth;&lt;br /&gt;
&lt;br /&gt;
		cleanCanvas.height =&lt;br /&gt;
			sourceHeight;&lt;br /&gt;
&lt;br /&gt;
		cleanCtx =&lt;br /&gt;
			cleanCanvas.getContext(&lt;br /&gt;
				&#039;2d&#039;,&lt;br /&gt;
				{&lt;br /&gt;
					willReadFrequently: true&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		cleanImage =&lt;br /&gt;
			cleanCtx.createImageData(&lt;br /&gt;
				sourceWidth,&lt;br /&gt;
				sourceHeight&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i &amp;lt; pixelCount;&lt;br /&gt;
			i++&lt;br /&gt;
		) {&lt;br /&gt;
			if ( !mask[ i ] ) {&lt;br /&gt;
				continue;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			p = i * 4;&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p ] =&lt;br /&gt;
				sourceData[ p ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 1 ] =&lt;br /&gt;
				sourceData[ p + 1 ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 2 ] =&lt;br /&gt;
				sourceData[ p + 2 ];&lt;br /&gt;
&lt;br /&gt;
			cleanImage.data[ p + 3 ] =&lt;br /&gt;
				255;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		cleanCtx.putImageData(&lt;br /&gt;
			cleanImage,&lt;br /&gt;
			0,&lt;br /&gt;
			0&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Normalize the detected sprite to the requested&lt;br /&gt;
		 * comparison size.&lt;br /&gt;
		 */&lt;br /&gt;
		targetCanvas =&lt;br /&gt;
			document.createElement(&lt;br /&gt;
				&#039;canvas&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		targetCanvas.width = width;&lt;br /&gt;
		targetCanvas.height = height;&lt;br /&gt;
&lt;br /&gt;
		targetCtx =&lt;br /&gt;
			targetCanvas.getContext(&lt;br /&gt;
				&#039;2d&#039;,&lt;br /&gt;
				{&lt;br /&gt;
					willReadFrequently: true&lt;br /&gt;
				}&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.fillStyle = &#039;#000&#039;;&lt;br /&gt;
&lt;br /&gt;
		targetCtx.fillRect(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			width,&lt;br /&gt;
			height&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.imageSmoothingEnabled =&lt;br /&gt;
			true;&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			&#039;imageSmoothingQuality&#039; in&lt;br /&gt;
			targetCtx&lt;br /&gt;
		) {&lt;br /&gt;
			targetCtx.imageSmoothingQuality =&lt;br /&gt;
				&#039;high&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		usableWidth =&lt;br /&gt;
			Math.max(&lt;br /&gt;
				1,&lt;br /&gt;
				width - 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		usableHeight =&lt;br /&gt;
			Math.max(&lt;br /&gt;
				1,&lt;br /&gt;
				height - 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		scale = Math.min(&lt;br /&gt;
			usableWidth /&lt;br /&gt;
				Math.max(&lt;br /&gt;
					1,&lt;br /&gt;
					maxX - minX + 1&lt;br /&gt;
				),&lt;br /&gt;
			usableHeight /&lt;br /&gt;
				Math.max(&lt;br /&gt;
					1,&lt;br /&gt;
					maxY - minY + 1&lt;br /&gt;
				)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawWidth = Math.max(&lt;br /&gt;
			1,&lt;br /&gt;
			Math.round(&lt;br /&gt;
				(&lt;br /&gt;
					maxX -&lt;br /&gt;
					minX +&lt;br /&gt;
					1&lt;br /&gt;
				) * scale&lt;br /&gt;
			)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawHeight = Math.max(&lt;br /&gt;
			1,&lt;br /&gt;
			Math.round(&lt;br /&gt;
				(&lt;br /&gt;
					maxY -&lt;br /&gt;
					minY +&lt;br /&gt;
					1&lt;br /&gt;
				) * scale&lt;br /&gt;
			)&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawX = Math.floor(&lt;br /&gt;
			(&lt;br /&gt;
				width -&lt;br /&gt;
				drawWidth&lt;br /&gt;
			) / 2&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		drawY = Math.floor(&lt;br /&gt;
			(&lt;br /&gt;
				height -&lt;br /&gt;
				drawHeight&lt;br /&gt;
			) / 2&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		targetCtx.drawImage(&lt;br /&gt;
			cleanCanvas,&lt;br /&gt;
			minX,&lt;br /&gt;
			minY,&lt;br /&gt;
			maxX - minX + 1,&lt;br /&gt;
			maxY - minY + 1,&lt;br /&gt;
			drawX,&lt;br /&gt;
			drawY,&lt;br /&gt;
			drawWidth,&lt;br /&gt;
			drawHeight&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		return targetCtx.getImageData(&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			width,&lt;br /&gt;
			height&lt;br /&gt;
		);&lt;br /&gt;
	}&lt;br /&gt;
	function luminance( r, g, b ) {&lt;br /&gt;
		return 0.2126 * r + 0.7152 * g + 0.0722 * b;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeDHash( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled( drawable, 9, 8, cropFraction );&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var hi = 0;&lt;br /&gt;
		var lo = 0;&lt;br /&gt;
		var bit = 0;&lt;br /&gt;
		var y;&lt;br /&gt;
		var x;&lt;br /&gt;
		var left;&lt;br /&gt;
		var right;&lt;br /&gt;
		var li;&lt;br /&gt;
		var ri;&lt;br /&gt;
&lt;br /&gt;
		for ( y = 0; y &amp;lt; 8; y++ ) {&lt;br /&gt;
			for ( x = 0; x &amp;lt; 8; x++ ) {&lt;br /&gt;
				li = ( y * 9 + x ) * 4;&lt;br /&gt;
				ri = ( y * 9 + x + 1 ) * 4;&lt;br /&gt;
&lt;br /&gt;
				left = luminance(&lt;br /&gt;
					data[ li ],&lt;br /&gt;
					data[ li + 1 ],&lt;br /&gt;
					data[ li + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				right = luminance(&lt;br /&gt;
					data[ ri ],&lt;br /&gt;
					data[ ri + 1 ],&lt;br /&gt;
					data[ ri + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				if ( left &amp;gt; right ) {&lt;br /&gt;
					if ( bit &amp;lt; 32 ) {&lt;br /&gt;
						lo = ( lo | ( 1 &amp;lt;&amp;lt; bit ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					} else {&lt;br /&gt;
						hi = ( hi | ( 1 &amp;lt;&amp;lt; ( bit - 32 ) ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				bit++;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hi: hi &amp;gt;&amp;gt;&amp;gt; 0,&lt;br /&gt;
			lo: lo &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeColorGrid( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled(&lt;br /&gt;
			drawable,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			cropFraction&lt;br /&gt;
		);&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var out = new Uint8Array( COLOR_GRID * COLOR_GRID * 3 );&lt;br /&gt;
		var source = 0;&lt;br /&gt;
		var target = 0;&lt;br /&gt;
&lt;br /&gt;
		while ( source &amp;lt; data.length ) {&lt;br /&gt;
			out[ target++ ] = data[ source ];&lt;br /&gt;
			out[ target++ ] = data[ source + 1 ];&lt;br /&gt;
			out[ target++ ] = data[ source + 2 ];&lt;br /&gt;
			source += 4;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDrawable( drawable, cropFraction ) {&lt;br /&gt;
		var hash = computeDHash( drawable, cropFraction );&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hashHi: hash.hi,&lt;br /&gt;
			hashLo: hash.lo,&lt;br /&gt;
			rgb: computeColorGrid( drawable, cropFraction )&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function popcount32( value ) {&lt;br /&gt;
		value = value &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
		value = value - ( ( value &amp;gt;&amp;gt;&amp;gt; 1 ) &amp;amp; 0x55555555 );&lt;br /&gt;
		value = ( value &amp;amp; 0x33333333 ) + ( ( value &amp;gt;&amp;gt;&amp;gt; 2 ) &amp;amp; 0x33333333 );&lt;br /&gt;
		return (&lt;br /&gt;
			(&lt;br /&gt;
				(&lt;br /&gt;
					value + ( value &amp;gt;&amp;gt;&amp;gt; 4 )&lt;br /&gt;
				) &amp;amp; 0x0F0F0F0F&lt;br /&gt;
			) * 0x01010101&lt;br /&gt;
		) &amp;gt;&amp;gt;&amp;gt; 24;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDistance( a, b ) {&lt;br /&gt;
		var hashBits =&lt;br /&gt;
			popcount32(&lt;br /&gt;
				(&lt;br /&gt;
					a.hashHi ^&lt;br /&gt;
					b.hashHi&lt;br /&gt;
				) &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
			) +&lt;br /&gt;
			popcount32(&lt;br /&gt;
				(&lt;br /&gt;
					a.hashLo ^&lt;br /&gt;
					b.hashLo&lt;br /&gt;
				) &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		var hashDistance =&lt;br /&gt;
			hashBits / 64;&lt;br /&gt;
&lt;br /&gt;
		var length = Math.min(&lt;br /&gt;
			a.rgb.length,&lt;br /&gt;
			b.rgb.length&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		var histogramA =&lt;br /&gt;
			new Uint16Array( 64 );&lt;br /&gt;
&lt;br /&gt;
		var histogramB =&lt;br /&gt;
			new Uint16Array( 64 );&lt;br /&gt;
&lt;br /&gt;
		var foregroundA = 0;&lt;br /&gt;
		var foregroundB = 0;&lt;br /&gt;
		var intersection = 0;&lt;br /&gt;
		var union = 0;&lt;br /&gt;
		var colorSum = 0;&lt;br /&gt;
		var i;&lt;br /&gt;
		var ar;&lt;br /&gt;
		var ag;&lt;br /&gt;
		var ab;&lt;br /&gt;
		var br;&lt;br /&gt;
		var bg;&lt;br /&gt;
		var bb;&lt;br /&gt;
		var aForeground;&lt;br /&gt;
		var bForeground;&lt;br /&gt;
		var dr;&lt;br /&gt;
		var dg;&lt;br /&gt;
		var db;&lt;br /&gt;
		var binA;&lt;br /&gt;
		var binB;&lt;br /&gt;
		var shapeDistance;&lt;br /&gt;
		var colorDistance;&lt;br /&gt;
		var histogramDistance = 0;&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i + 2 &amp;lt; length;&lt;br /&gt;
			i += 3&lt;br /&gt;
		) {&lt;br /&gt;
			ar = a.rgb[ i ];&lt;br /&gt;
			ag = a.rgb[ i + 1 ];&lt;br /&gt;
			ab = a.rgb[ i + 2 ];&lt;br /&gt;
&lt;br /&gt;
			br = b.rgb[ i ];&lt;br /&gt;
			bg = b.rgb[ i + 1 ];&lt;br /&gt;
			bb = b.rgb[ i + 2 ];&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * Background is normalized to black by&lt;br /&gt;
			 * drawScaled().&lt;br /&gt;
			 */&lt;br /&gt;
			aForeground =&lt;br /&gt;
				Math.max(&lt;br /&gt;
					ar,&lt;br /&gt;
					ag,&lt;br /&gt;
					ab&lt;br /&gt;
				) &amp;gt; 8;&lt;br /&gt;
&lt;br /&gt;
			bForeground =&lt;br /&gt;
				Math.max(&lt;br /&gt;
					br,&lt;br /&gt;
					bg,&lt;br /&gt;
					bb&lt;br /&gt;
				) &amp;gt; 8;&lt;br /&gt;
&lt;br /&gt;
			if ( aForeground ) {&lt;br /&gt;
				foregroundA++;&lt;br /&gt;
&lt;br /&gt;
				binA =&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							ar &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 4&lt;br /&gt;
					) |&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							ag &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 2&lt;br /&gt;
					) |&lt;br /&gt;
					Math.min(&lt;br /&gt;
						3,&lt;br /&gt;
						ab &amp;gt;&amp;gt; 6&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				histogramA[ binA ]++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if ( bForeground ) {&lt;br /&gt;
				foregroundB++;&lt;br /&gt;
&lt;br /&gt;
				binB =&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							br &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 4&lt;br /&gt;
					) |&lt;br /&gt;
					(&lt;br /&gt;
						Math.min(&lt;br /&gt;
							3,&lt;br /&gt;
							bg &amp;gt;&amp;gt; 6&lt;br /&gt;
						) &amp;lt;&amp;lt; 2&lt;br /&gt;
					) |&lt;br /&gt;
					Math.min(&lt;br /&gt;
						3,&lt;br /&gt;
						bb &amp;gt;&amp;gt; 6&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
				histogramB[ binB ]++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				aForeground ||&lt;br /&gt;
				bForeground&lt;br /&gt;
			) {&lt;br /&gt;
				union++;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				!aForeground ||&lt;br /&gt;
				!bForeground&lt;br /&gt;
			) {&lt;br /&gt;
				continue;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			intersection++;&lt;br /&gt;
&lt;br /&gt;
			dr = ar - br;&lt;br /&gt;
			dg = ag - bg;&lt;br /&gt;
			db = ab - bb;&lt;br /&gt;
&lt;br /&gt;
			colorSum +=&lt;br /&gt;
				Math.sqrt(&lt;br /&gt;
					dr * dr +&lt;br /&gt;
					dg * dg +&lt;br /&gt;
					db * db&lt;br /&gt;
				) /&lt;br /&gt;
				441.67295593;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		shapeDistance =&lt;br /&gt;
			union&lt;br /&gt;
				? 1 -&lt;br /&gt;
					intersection /&lt;br /&gt;
						union&lt;br /&gt;
				: 1;&lt;br /&gt;
&lt;br /&gt;
		colorDistance =&lt;br /&gt;
			intersection&lt;br /&gt;
				? colorSum /&lt;br /&gt;
					intersection&lt;br /&gt;
				: 1;&lt;br /&gt;
&lt;br /&gt;
		for (&lt;br /&gt;
			i = 0;&lt;br /&gt;
			i &amp;lt; 64;&lt;br /&gt;
			i++&lt;br /&gt;
		) {&lt;br /&gt;
			histogramDistance +=&lt;br /&gt;
				Math.abs(&lt;br /&gt;
					histogramA[ i ] /&lt;br /&gt;
						Math.max(&lt;br /&gt;
							1,&lt;br /&gt;
							foregroundA&lt;br /&gt;
						) -&lt;br /&gt;
					histogramB[ i ] /&lt;br /&gt;
						Math.max(&lt;br /&gt;
							1,&lt;br /&gt;
							foregroundB&lt;br /&gt;
						)&lt;br /&gt;
				);&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Normalized histogram L1 range is 0..2.&lt;br /&gt;
		 */&lt;br /&gt;
		histogramDistance =&lt;br /&gt;
			Math.min(&lt;br /&gt;
				1,&lt;br /&gt;
				histogramDistance / 2&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Foreground color is deliberately the largest&lt;br /&gt;
		 * contributor now.&lt;br /&gt;
		 */&lt;br /&gt;
		return Math.min(&lt;br /&gt;
			1,&lt;br /&gt;
			0.50 * colorDistance +&lt;br /&gt;
				0.25 * shapeDistance +&lt;br /&gt;
				0.15 * histogramDistance +&lt;br /&gt;
				0.10 * hashDistance&lt;br /&gt;
		);&lt;br /&gt;
	}&lt;br /&gt;
	function mapLimit( list, limit, worker ) {&lt;br /&gt;
		var nextIndex = 0;&lt;br /&gt;
		var workers = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		function runWorker() {&lt;br /&gt;
			var index;&lt;br /&gt;
&lt;br /&gt;
			if ( nextIndex &amp;gt;= list.length ) {&lt;br /&gt;
				return Promise.resolve();&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			index = nextIndex++;&lt;br /&gt;
			return Promise.resolve( worker( list[ index ], index ) )&lt;br /&gt;
				.then( runWorker );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; Math.min( limit, list.length ); i++ ) {&lt;br /&gt;
			workers.push( runWorker() );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return Promise.all( workers );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildFingerprintIndex( records, setStatus ) {&lt;br /&gt;
		var complete = 0;&lt;br /&gt;
		var successful = [];&lt;br /&gt;
		var grouped = Object.create( null );&lt;br /&gt;
		var groups = [];&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * If several icon filenames are byte-identical, imageinfo gives them&lt;br /&gt;
		 * the same SHA-1. Fingerprint that image only once and fan the result&lt;br /&gt;
		 * back out to every filename. This can substantially reduce the first&lt;br /&gt;
		 * build&#039;s static-file traffic without changing the visible results.&lt;br /&gt;
		 */&lt;br /&gt;
		records.forEach( function ( record ) {&lt;br /&gt;
			var key = record.sha1 || record.url;&lt;br /&gt;
&lt;br /&gt;
			if ( !grouped[ key ] ) {&lt;br /&gt;
				grouped[ key ] = {&lt;br /&gt;
					key: key,&lt;br /&gt;
					representative: record,&lt;br /&gt;
					records: []&lt;br /&gt;
				};&lt;br /&gt;
				groups.push( grouped[ key ] );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			grouped[ key ].records.push( record );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return mapLimit(&lt;br /&gt;
			groups,&lt;br /&gt;
			DOWNLOAD_CONCURRENCY,&lt;br /&gt;
			function ( group ) {&lt;br /&gt;
				return fetchDrawable( group.representative.url )&lt;br /&gt;
					.then( function ( drawable ) {&lt;br /&gt;
						var fp = fingerprintDrawable( drawable, 0 );&lt;br /&gt;
&lt;br /&gt;
						closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
						group.records.forEach( function ( record ) {&lt;br /&gt;
							successful.push( {&lt;br /&gt;
								title: record.title,&lt;br /&gt;
								url: record.url,&lt;br /&gt;
								sha1: record.sha1,&lt;br /&gt;
								width: record.width,&lt;br /&gt;
								height: record.height,&lt;br /&gt;
								hashHi: fp.hashHi,&lt;br /&gt;
								hashLo: fp.hashLo,&lt;br /&gt;
								rgb: fp.rgb&lt;br /&gt;
							} );&lt;br /&gt;
						} );&lt;br /&gt;
					} )&lt;br /&gt;
					.catch( function () {&lt;br /&gt;
						/*&lt;br /&gt;
						 * A missing/corrupt/non-raster file should not prevent&lt;br /&gt;
						 * the rest of the icon index from becoming usable.&lt;br /&gt;
						 */&lt;br /&gt;
					} )&lt;br /&gt;
					.then( function () {&lt;br /&gt;
						complete++;&lt;br /&gt;
&lt;br /&gt;
						if (&lt;br /&gt;
							setStatus &amp;amp;&amp;amp;&lt;br /&gt;
							(&lt;br /&gt;
								complete === groups.length ||&lt;br /&gt;
								complete % 25 === 0&lt;br /&gt;
							)&lt;br /&gt;
						) {&lt;br /&gt;
							setStatus(&lt;br /&gt;
								&#039;Building pixel index… &#039; +&lt;br /&gt;
									complete + &#039; / &#039; + groups.length +&lt;br /&gt;
									&#039; unique images (&#039; +&lt;br /&gt;
									records.length + &#039; filenames)&#039;&lt;br /&gt;
							);&lt;br /&gt;
						}&lt;br /&gt;
					} );&lt;br /&gt;
			}&lt;br /&gt;
		).then( function () {&lt;br /&gt;
			return successful;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cacheKey( revid ) {&lt;br /&gt;
		return [&lt;br /&gt;
			&#039;icon-list&#039;,&lt;br /&gt;
			ALGO_VERSION,&lt;br /&gt;
			revid&lt;br /&gt;
		].join( &#039;:&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getOrBuildIndex( api, setStatus ) {&lt;br /&gt;
		if ( memoryIndex ) {&lt;br /&gt;
			return Promise.resolve( memoryIndex );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		setStatus( &#039;Reading Icon List…&#039; );&lt;br /&gt;
&lt;br /&gt;
		return fetchCatalog( api ).then( function ( catalog ) {&lt;br /&gt;
			var key = cacheKey( catalog.revid );&lt;br /&gt;
&lt;br /&gt;
			return idbGet( key )&lt;br /&gt;
				.catch( function () {&lt;br /&gt;
					return null;&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function ( cached ) {&lt;br /&gt;
					if (&lt;br /&gt;
						cached &amp;amp;&amp;amp;&lt;br /&gt;
						Array.isArray( cached.records ) &amp;amp;&amp;amp;&lt;br /&gt;
						cached.records.length&lt;br /&gt;
					) {&lt;br /&gt;
						memoryIndex = cached.records;&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Icon index ready: &#039; +&lt;br /&gt;
								memoryIndex.length +&lt;br /&gt;
								&#039; uploaded icons (cached locally).&#039;&lt;br /&gt;
						);&lt;br /&gt;
						return memoryIndex;&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					return fetchImageInfo(&lt;br /&gt;
						api,&lt;br /&gt;
						catalog.titles,&lt;br /&gt;
						setStatus&lt;br /&gt;
					).then( function ( imageRecords ) {&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Found &#039; + imageRecords.length +&lt;br /&gt;
								&#039; uploaded icons. Building pixel index…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						return buildFingerprintIndex(&lt;br /&gt;
							imageRecords,&lt;br /&gt;
							setStatus&lt;br /&gt;
						);&lt;br /&gt;
					} ).then( function ( records ) {&lt;br /&gt;
						memoryIndex = records;&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Icon index ready: &#039; +&lt;br /&gt;
								records.length +&lt;br /&gt;
								&#039; uploaded icons.&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						return idbPutAndPrune( {&lt;br /&gt;
							key: key,&lt;br /&gt;
							revid: catalog.revid,&lt;br /&gt;
							algorithm: ALGO_VERSION,&lt;br /&gt;
							builtAt: Date.now(),&lt;br /&gt;
							records: records&lt;br /&gt;
						} )&lt;br /&gt;
							.catch( function () {&lt;br /&gt;
								/*&lt;br /&gt;
								 * Matching still works for this session even if&lt;br /&gt;
								 * the browser blocks persistent storage.&lt;br /&gt;
								 */&lt;br /&gt;
							} )&lt;br /&gt;
							.then( function () {&lt;br /&gt;
								return records;&lt;br /&gt;
							} );&lt;br /&gt;
					} );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintPastedImage( drawable ) {&lt;br /&gt;
		/*&lt;br /&gt;
		 * Small centered crop variants make the matcher forgiving of a&lt;br /&gt;
		 * 1-2 pixel screen-capture border while still preferring an exact crop.&lt;br /&gt;
		 */&lt;br /&gt;
		return [&lt;br /&gt;
			fingerprintDrawable( drawable, 0 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.035 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.07 )&lt;br /&gt;
		];&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function rankMatches( targetFingerprints, records ) {&lt;br /&gt;
		var ranked = records.map( function ( record ) {&lt;br /&gt;
			var candidate = {&lt;br /&gt;
				hashHi: record.hashHi,&lt;br /&gt;
				hashLo: record.hashLo,&lt;br /&gt;
				rgb: record.rgb&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			var distance = Infinity;&lt;br /&gt;
&lt;br /&gt;
			targetFingerprints.forEach( function ( target ) {&lt;br /&gt;
				distance = Math.min(&lt;br /&gt;
					distance,&lt;br /&gt;
					fingerprintDistance( target, candidate )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				record: record,&lt;br /&gt;
				distance: distance,&lt;br /&gt;
				similarity: Math.max(&lt;br /&gt;
					0,&lt;br /&gt;
					Math.min( 100, ( 1 - distance ) * 100 )&lt;br /&gt;
				)&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		ranked.sort( function ( a, b ) {&lt;br /&gt;
			return a.distance - b.distance;&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return ranked.slice( 0, RESULT_COUNT );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cleanDisplayTitle( title ) {&lt;br /&gt;
		return String( title || &#039;&#039; ).replace( /^File:/i, &#039;&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function iconMetadata( title ) {&lt;br /&gt;
		var clean = cleanDisplayTitle( title );&lt;br /&gt;
		var itemMatch = clean.match( /^Item\s+(\d+)\.png$/i );&lt;br /&gt;
		var spellMatch = clean.match( /^Spellicon_([^.]*)\.png$/i );&lt;br /&gt;
		var id = &#039;&#039;;&lt;br /&gt;
		var kind = &#039;Icon file&#039;;&lt;br /&gt;
		var parameter = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		if ( itemMatch ) {&lt;br /&gt;
			id = itemMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Item icon&#039;;&lt;br /&gt;
			parameter = &#039;|lucy_img_ID = &#039; + id;&lt;br /&gt;
		} else if ( spellMatch ) {&lt;br /&gt;
			id = spellMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Spell icon&#039;;&lt;br /&gt;
			parameter = &#039;|spellicon = &#039; + id;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			kind: kind,&lt;br /&gt;
			id: id,&lt;br /&gt;
			filename: clean,&lt;br /&gt;
			parameter: parameter,&lt;br /&gt;
			rawWiki: &#039;[[File:&#039; + clean + &#039;]]&#039;,&lt;br /&gt;
			staticWiki: &#039;[[File:&#039; + clean + &#039;|32x32px]]&#039;,&lt;br /&gt;
			scaledWiki: &#039;[[File:&#039; + clean + &#039;|frameless|upright=1.5]]&#039;&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function copyText( text, button ) {&lt;br /&gt;
		var original = button.textContent;&lt;br /&gt;
&lt;br /&gt;
		function done() {&lt;br /&gt;
			button.textContent = &#039;Copied&#039;;&lt;br /&gt;
			window.setTimeout( function () {&lt;br /&gt;
				button.textContent = original;&lt;br /&gt;
			}, 1000 );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			navigator.clipboard &amp;amp;&amp;amp;&lt;br /&gt;
			typeof navigator.clipboard.writeText === &#039;function&#039;&lt;br /&gt;
		) {&lt;br /&gt;
			navigator.clipboard.writeText( text )&lt;br /&gt;
				.then( done )&lt;br /&gt;
				.catch( function () {&lt;br /&gt;
					window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
				} );&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeCopyButton( label, value ) {&lt;br /&gt;
		var button = document.createElement( &#039;button&#039; );&lt;br /&gt;
&lt;br /&gt;
		button.className = &#039;eql-iconfinder-copy&#039;;&lt;br /&gt;
		button.type = &#039;button&#039;;&lt;br /&gt;
		button.textContent = label;&lt;br /&gt;
&lt;br /&gt;
		button.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			copyText( value, button );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return button;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function renderMatches( container, matches ) {&lt;br /&gt;
		container.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		var title = document.createElement( &#039;div&#039; );&lt;br /&gt;
		title.className = &#039;eql-iconfinder-results-title&#039;;&lt;br /&gt;
		title.textContent = &#039;Possible Matches&#039;;&lt;br /&gt;
		container.appendChild( title );&lt;br /&gt;
&lt;br /&gt;
		var grid = document.createElement( &#039;div&#039; );&lt;br /&gt;
		grid.className = &#039;eql-iconfinder-results-grid&#039;;&lt;br /&gt;
		container.appendChild( grid );&lt;br /&gt;
&lt;br /&gt;
		matches.forEach( function ( match ) {&lt;br /&gt;
			var record = match.record;&lt;br /&gt;
			var meta = iconMetadata( record.title );&lt;br /&gt;
			var card = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var img = document.createElement( &#039;img&#039; );&lt;br /&gt;
			var textWrap = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var link = document.createElement( &#039;a&#039; );&lt;br /&gt;
			var score = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var iconId = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var metadata = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var copyRow = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
			card.className = &#039;eql-iconfinder-result&#039;;&lt;br /&gt;
&lt;br /&gt;
			img.src = record.url;&lt;br /&gt;
			img.alt = meta.filename;&lt;br /&gt;
			img.loading = &#039;lazy&#039;;&lt;br /&gt;
&lt;br /&gt;
			link.className = &#039;eql-iconfinder-result-name&#039;;&lt;br /&gt;
			link.href = mw.util.getUrl( record.title );&lt;br /&gt;
			link.target = &#039;_blank&#039;;&lt;br /&gt;
			link.rel = &#039;noopener&#039;;&lt;br /&gt;
			link.textContent = meta.filename;&lt;br /&gt;
&lt;br /&gt;
			score.className = &#039;eql-iconfinder-result-score&#039;;&lt;br /&gt;
			score.textContent =&lt;br /&gt;
				&#039;Match score &#039; + match.similarity.toFixed( 1 );&lt;br /&gt;
&lt;br /&gt;
			iconId.className = &#039;eql-iconfinder-result-id&#039;;&lt;br /&gt;
			iconId.textContent = meta.id&lt;br /&gt;
				? &#039;Icon &#039; + meta.id&lt;br /&gt;
				: meta.filename;&lt;br /&gt;
&lt;br /&gt;
			metadata.className = &#039;eql-iconfinder-result-meta&#039;;&lt;br /&gt;
			metadata.textContent = meta.kind;&lt;br /&gt;
&lt;br /&gt;
			copyRow.className = &#039;eql-iconfinder-copy-row&#039;;&lt;br /&gt;
&lt;br /&gt;
			if ( meta.id ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy ID&#039;, meta.id )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if ( meta.parameter ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy Parameter&#039;, meta.parameter )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Raw Wikicode&#039;, meta.rawWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;32×32&#039;, meta.staticWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Scaled&#039;, meta.scaledWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			textWrap.appendChild( link );&lt;br /&gt;
			textWrap.appendChild( iconId );&lt;br /&gt;
			textWrap.appendChild( score );&lt;br /&gt;
			textWrap.appendChild( metadata );&lt;br /&gt;
			textWrap.appendChild( copyRow );&lt;br /&gt;
&lt;br /&gt;
			card.appendChild( img );&lt;br /&gt;
			card.appendChild( textWrap );&lt;br /&gt;
			grid.appendChild( card );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		var note = document.createElement( &#039;div&#039; );&lt;br /&gt;
		note.className = &#039;eql-iconfinder-note&#039;;&lt;br /&gt;
		note.textContent =&lt;br /&gt;
			&#039;The tool only identifies likely matches and copies values/wikicode to the clipboard; it never changes the editor. &#039; +&lt;br /&gt;
			&#039;For the best result, capture only the square icon with as little surrounding UI as possible.&#039;;&lt;br /&gt;
		container.appendChild( note );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function imageFromClipboardEvent( event ) {&lt;br /&gt;
		var clipboard = event.clipboardData;&lt;br /&gt;
		var items;&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		if ( !clipboard || !clipboard.items ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		items = clipboard.items;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; items.length; i++ ) {&lt;br /&gt;
			if (&lt;br /&gt;
				items[ i ].kind === &#039;file&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				/^image\//i.test( items[ i ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				return items[ i ].getAsFile();&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return null;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setPreview( img, blob ) {&lt;br /&gt;
		if ( currentPreviewUrl ) {&lt;br /&gt;
			URL.revokeObjectURL( currentPreviewUrl );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		currentPreviewUrl = URL.createObjectURL( blob );&lt;br /&gt;
		img.src = currentPreviewUrl;&lt;br /&gt;
		img.classList.add( &#039;is-visible&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function processBlob( blob, preview, status, results, api ) {&lt;br /&gt;
		var setStatus = function ( text ) {&lt;br /&gt;
			status.textContent = text;&lt;br /&gt;
		};&lt;br /&gt;
&lt;br /&gt;
		setPreview( preview, blob );&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Analyzing pasted icon…&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		return blobToDrawable( blob )&lt;br /&gt;
			.then( function ( drawable ) {&lt;br /&gt;
				var targets = fingerprintPastedImage( drawable );&lt;br /&gt;
				closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
				return getOrBuildIndex( api, setStatus )&lt;br /&gt;
					.then( function ( index ) {&lt;br /&gt;
						var matches;&lt;br /&gt;
&lt;br /&gt;
						if ( !index.length ) {&lt;br /&gt;
							throw new Error( &#039;No uploaded icons were found.&#039; );&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Comparing against &#039; +&lt;br /&gt;
								index.length +&lt;br /&gt;
								&#039; uploaded icons…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						/*&lt;br /&gt;
						 * Yield once so the status message can paint before&lt;br /&gt;
						 * ranking several thousand in-memory fingerprints.&lt;br /&gt;
						 */&lt;br /&gt;
						return new Promise( function ( resolve ) {&lt;br /&gt;
							window.setTimeout( function () {&lt;br /&gt;
								matches = rankMatches( targets, index );&lt;br /&gt;
								resolve( matches );&lt;br /&gt;
							}, 0 );&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
			} )&lt;br /&gt;
			.then( function ( matches ) {&lt;br /&gt;
				renderMatches( results, matches );&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Done. Showing the &#039; +&lt;br /&gt;
						matches.length +&lt;br /&gt;
						&#039; closest matches.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				results.innerHTML =&lt;br /&gt;
					&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
						&#039;The icon could not be matched.&#039; +&lt;br /&gt;
					&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Icon Finder error: &#039; +&lt;br /&gt;
						( error &amp;amp;&amp;amp; error.message ? error.message : &#039;Unknown error&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildUi( host ) {&lt;br /&gt;
		var api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		host.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		var shell = document.createElement( &#039;div&#039; );&lt;br /&gt;
		shell.className = &#039;eql-iconfinder-shell&#039;;&lt;br /&gt;
&lt;br /&gt;
		var left = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var paste = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var instruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var subinstruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var preview = document.createElement( &#039;img&#039; );&lt;br /&gt;
		var status = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		paste.className = &#039;eql-iconfinder-paste&#039;;&lt;br /&gt;
		paste.tabIndex = 0;&lt;br /&gt;
		paste.setAttribute( &#039;role&#039;, &#039;button&#039; );&lt;br /&gt;
		paste.setAttribute(&lt;br /&gt;
			&#039;aria-label&#039;,&lt;br /&gt;
			&#039;Paste a screen capture of an EverQuest icon&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		instruction.className = &#039;eql-iconfinder-instruction&#039;;&lt;br /&gt;
		instruction.textContent =&lt;br /&gt;
			&#039;Screen capture and paste JUST the Icon&#039;;&lt;br /&gt;
&lt;br /&gt;
		subinstruction.className = &#039;eql-iconfinder-subinstruction&#039;;&lt;br /&gt;
		subinstruction.textContent =&lt;br /&gt;
			&#039;Click this box, then press Ctrl+V. You can also drag an image here.&#039;;&lt;br /&gt;
&lt;br /&gt;
		preview.className = &#039;eql-iconfinder-preview&#039;;&lt;br /&gt;
		preview.alt = &#039;Pasted icon preview&#039;;&lt;br /&gt;
&lt;br /&gt;
		status.className = &#039;eql-iconfinder-status&#039;;&lt;br /&gt;
		status.setAttribute( &#039;aria-live&#039;, &#039;polite&#039; );&lt;br /&gt;
		status.textContent =&lt;br /&gt;
			&#039;Nothing is loaded from Icon List until you paste an image.&#039;;&lt;br /&gt;
&lt;br /&gt;
		paste.appendChild( instruction );&lt;br /&gt;
		paste.appendChild( subinstruction );&lt;br /&gt;
		paste.appendChild( preview );&lt;br /&gt;
&lt;br /&gt;
		left.appendChild( paste );&lt;br /&gt;
		left.appendChild( status );&lt;br /&gt;
&lt;br /&gt;
		var right = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var resultsTitle = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var results = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		resultsTitle.className = &#039;eql-iconfinder-results-title&#039;;&lt;br /&gt;
		resultsTitle.textContent = &#039;Possible Matches&#039;;&lt;br /&gt;
&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Paste an icon to search the uploaded images on Icon List.&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		right.appendChild( resultsTitle );&lt;br /&gt;
		right.appendChild( results );&lt;br /&gt;
&lt;br /&gt;
		shell.appendChild( left );&lt;br /&gt;
		shell.appendChild( right );&lt;br /&gt;
		host.appendChild( shell );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;paste&#039;, function ( event ) {&lt;br /&gt;
			var file = imageFromClipboardEvent( event );&lt;br /&gt;
&lt;br /&gt;
			if ( !file ) {&lt;br /&gt;
				status.textContent =&lt;br /&gt;
					&#039;Clipboard does not contain an image. Screen capture the icon, then paste again.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				file,&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;dragover&#039;, function ( event ) {&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;drop&#039;, function ( event ) {&lt;br /&gt;
			var files;&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
			files = event.dataTransfer &amp;amp;&amp;amp; event.dataTransfer.files;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				!files ||&lt;br /&gt;
				!files.length ||&lt;br /&gt;
				!/^image\//i.test( files[ 0 ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				status.textContent = &#039;Drop an image file here.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				files[ 0 ],&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Focus the paste target immediately when the panel first opens so&lt;br /&gt;
		 * the normal workflow is: click Icon Finder -&amp;gt; Ctrl+V.&lt;br /&gt;
		 */&lt;br /&gt;
		window.setTimeout( function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		}, 0 );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	window.EQLIconFinder = {&lt;br /&gt;
		init: function ( host ) {&lt;br /&gt;
			if ( !host ) {&lt;br /&gt;
				return Promise.reject(&lt;br /&gt;
					new Error( &#039;Icon Finder host element is missing.&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			addStyles();&lt;br /&gt;
&lt;br /&gt;
			if ( initializedHost !== host ) {&lt;br /&gt;
				buildUi( host );&lt;br /&gt;
				initializedHost = host;&lt;br /&gt;
			} else {&lt;br /&gt;
				var paste = host.querySelector( &#039;.eql-iconfinder-paste&#039; );&lt;br /&gt;
&lt;br /&gt;
				if ( paste ) {&lt;br /&gt;
					window.setTimeout( function () {&lt;br /&gt;
						paste.focus();&lt;br /&gt;
					}, 0 );&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return Promise.resolve();&lt;br /&gt;
		}&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168727</id>
		<title>MediaWiki:BlueprintLoader.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168727"/>
		<updated>2026-08-10T05:56:10Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Show Blueprint selector on all source editors&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Editor Tools / Dynamic Blueprint Loader&lt;br /&gt;
 *&lt;br /&gt;
 * Source-editor behavior:&lt;br /&gt;
 * - On a new, truly empty page: show the Help:Contents Blueprint selector.&lt;br /&gt;
 * - On every source edit page: show the Icon Finder button.&lt;br /&gt;
 * - Heavy Icon Finder logic is lazy-loaded only when opened.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var BLUEPRINT_PAGE = &#039;Help:Contents&#039;;&lt;br /&gt;
	var TOOLS_ID = &#039;eql-editor-tools&#039;;&lt;br /&gt;
	var ICON_FINDER_PAGE = &#039;MediaWiki:IconFinder.js&#039;;&lt;br /&gt;
&lt;br /&gt;
	function htmlToText( html ) {&lt;br /&gt;
		var div = document.createElement( &#039;div&#039; );&lt;br /&gt;
		div.innerHTML = String( html || &#039;&#039; );&lt;br /&gt;
		return ( div.textContent || div.innerText || &#039;&#039; ).trim();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeLabel( heading ) {&lt;br /&gt;
		return heading&lt;br /&gt;
			.replace( /\s+Blueprints?\s*$/i, &#039;&#039; )&lt;br /&gt;
			.trim() + &#039; Blueprint&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getEditorContents( $textarea ) {&lt;br /&gt;
		try {&lt;br /&gt;
			return String( $textarea.textSelection( &#039;getContents&#039; ) || &#039;&#039; );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			return String( $textarea.val() || &#039;&#039; );&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setEditorContents( $textarea, text ) {&lt;br /&gt;
		try {&lt;br /&gt;
			$textarea.textSelection( &#039;setContents&#039;, text );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			$textarea.val( text );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea.trigger( &#039;input&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;change&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;focus&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function extractBlueprint( sectionText ) {&lt;br /&gt;
		var match;&lt;br /&gt;
		var blueprint;&lt;br /&gt;
&lt;br /&gt;
		sectionText = String( sectionText || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		match = sectionText.match(&lt;br /&gt;
			/&amp;lt;pre(?:\s[^&amp;gt;]*)?&amp;gt;([\s\S]*?)&amp;lt;\/pre&amp;gt;/i&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		if ( !match ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		blueprint = match[ 1 ];&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint.replace(&lt;br /&gt;
			/^\s*&amp;lt;nowiki&amp;gt;([\s\S]*?)&amp;lt;\/nowiki&amp;gt;\s*$/i,&lt;br /&gt;
			&#039;$1&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint&lt;br /&gt;
			.replace( /^\s*\n/, &#039;&#039; )&lt;br /&gt;
			.replace( /\s+$/, &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		return blueprint + &#039;\n&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintSections( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;tocdata&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var tocdata =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse.tocdata;&lt;br /&gt;
&lt;br /&gt;
			var sections =&lt;br /&gt;
				tocdata &amp;amp;&amp;amp;&lt;br /&gt;
				Array.isArray( tocdata.sections )&lt;br /&gt;
					? tocdata.sections&lt;br /&gt;
					: [];&lt;br /&gt;
&lt;br /&gt;
			if ( sections.length ) {&lt;br /&gt;
				return sections;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return api.get( {&lt;br /&gt;
				action: &#039;parse&#039;,&lt;br /&gt;
				page: BLUEPRINT_PAGE,&lt;br /&gt;
				prop: &#039;sections&#039;,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			} ).then( function ( fallbackData ) {&lt;br /&gt;
				return (&lt;br /&gt;
					fallbackData &amp;amp;&amp;amp;&lt;br /&gt;
					fallbackData.parse &amp;amp;&amp;amp;&lt;br /&gt;
					Array.isArray( fallbackData.parse.sections )&lt;br /&gt;
				)&lt;br /&gt;
					? fallbackData.parse.sections&lt;br /&gt;
					: [];&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeBlueprintSections( sections ) {&lt;br /&gt;
		return sections&lt;br /&gt;
			.map( function ( section ) {&lt;br /&gt;
				var heading = htmlToText( section.line || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
				return {&lt;br /&gt;
					heading: heading,&lt;br /&gt;
					label: makeLabel( heading ),&lt;br /&gt;
					index: String( section.index || &#039;&#039; ),&lt;br /&gt;
					anchor:&lt;br /&gt;
						section.linkAnchor ||&lt;br /&gt;
						section.anchor ||&lt;br /&gt;
						&#039;&#039;&lt;br /&gt;
				};&lt;br /&gt;
			} )&lt;br /&gt;
			.filter( function ( section ) {&lt;br /&gt;
				return (&lt;br /&gt;
					section.index &amp;amp;&amp;amp;&lt;br /&gt;
					/\bBlueprints?\s*$/i.test( section.heading )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintWikitext( api, sectionIndex ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;wikitext&#039;,&lt;br /&gt;
			section: sectionIndex,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var wikitext =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse&lt;br /&gt;
					? data.parse.wikitext&lt;br /&gt;
					: &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				wikitext &amp;amp;&amp;amp;&lt;br /&gt;
				typeof wikitext === &#039;object&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				Object.prototype.hasOwnProperty.call( wikitext, &#039;*&#039; )&lt;br /&gt;
			) {&lt;br /&gt;
				wikitext = wikitext[ &#039;*&#039; ];&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return String( wikitext || &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-editor-tools-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-editor-tools-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039;{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;margin:0 0 1rem 0;&#039; +&lt;br /&gt;
					&#039;padding:1rem 1.1rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,rgba(16,21,29,.97),rgba(9,13,19,.98));&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.38);&#039; +&lt;br /&gt;
					&#039;border-left:5px solid rgba(216,183,92,.82);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 9px 24px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.035);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(0,1fr) auto;&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:end;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-title,&#039; +&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .35rem 0;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;letter-spacing:.035em;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-description{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .7rem 0;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-controls{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;min-width:260px;&#039; +&lt;br /&gt;
					&#039;max-width:100%;&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .55rem;&#039; +&lt;br /&gt;
					&#039;background:#0b1017;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.36);&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#e2e2cf;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button{&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .85rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,#806d32,#594817);&#039; +&lt;br /&gt;
					&#039;border:1px solid #b69c4b;&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#fff5cc;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;cursor:pointer;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:hover:not(:disabled){&#039; +&lt;br /&gt;
					&#039;filter:brightness(1.08);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:disabled{&#039; +&lt;br /&gt;
					&#039;cursor:default;&#039; +&lt;br /&gt;
					&#039;opacity:.55;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-source{&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-status{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-top:.55rem;&#039; +&lt;br /&gt;
					&#039;min-height:1.2em;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:stretch;&#039; +&lt;br /&gt;
					&#039;justify-content:flex-end;&#039; +&lt;br /&gt;
					&#039;min-width:150px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-toggle{&#039; +&lt;br /&gt;
					&#039;white-space:nowrap;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host{&#039; +&lt;br /&gt;
					&#039;margin-top:1rem;&#039; +&lt;br /&gt;
					&#039;padding-top:1rem;&#039; +&lt;br /&gt;
					&#039;border-top:1px solid rgba(216,183,92,.22);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host[hidden]{display:none!important;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:760px){&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
						&#039;grid-template-columns:1fr;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
						&#039;width:100%;&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function loadIconFinderScript() {&lt;br /&gt;
		if ( window.EQLIconFinder ) {&lt;br /&gt;
			return Promise.resolve( window.EQLIconFinder );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( window.eqlIconFinderLoadingPromise ) {&lt;br /&gt;
			return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.eqlIconFinderLoadingPromise = new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var script = document.createElement( &#039;script&#039; );&lt;br /&gt;
&lt;br /&gt;
			script.src = mw.util.getUrl( ICON_FINDER_PAGE, {&lt;br /&gt;
				action: &#039;raw&#039;,&lt;br /&gt;
				ctype: &#039;text/javascript&#039;&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			script.async = true;&lt;br /&gt;
&lt;br /&gt;
			script.onload = function () {&lt;br /&gt;
				if ( window.EQLIconFinder ) {&lt;br /&gt;
					resolve( window.EQLIconFinder );&lt;br /&gt;
				} else {&lt;br /&gt;
					reject( new Error( &#039;IconFinder.js loaded without exposing EQLIconFinder.&#039; ) );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			script.onerror = function () {&lt;br /&gt;
				reject( new Error( &#039;Could not load IconFinder.js.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			document.head.appendChild( script );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildIconFinderLaunch( $row, $tools ) {&lt;br /&gt;
		var $launch = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-launch&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-title&#039;,&lt;br /&gt;
			text: &#039;ICON FINDER&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-toggle&#039;,&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Icon Finder&#039;,&lt;br /&gt;
			&#039;aria-expanded&#039;: &#039;false&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $host = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-host&#039;,&lt;br /&gt;
			hidden: true&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var opening = $host.prop( &#039;hidden&#039; );&lt;br /&gt;
&lt;br /&gt;
			if ( !opening ) {&lt;br /&gt;
				$host.prop( &#039;hidden&#039;, true );&lt;br /&gt;
				$button.attr( &#039;aria-expanded&#039;, &#039;false&#039; );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$button.text( &#039;Loading…&#039; );&lt;br /&gt;
&lt;br /&gt;
			loadIconFinderScript()&lt;br /&gt;
				.then( function ( finder ) {&lt;br /&gt;
					return finder.init( $host[ 0 ] );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$host.prop( &#039;hidden&#039;, false );&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$host&lt;br /&gt;
						.prop( &#039;hidden&#039;, false )&lt;br /&gt;
						.text( &#039;Icon Finder could not be loaded.&#039; );&lt;br /&gt;
&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.text( &#039;Icon Finder&#039; );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildBlueprintArea( $row, $textarea, api ) {&lt;br /&gt;
		var $area = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-area&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-title&#039;,&lt;br /&gt;
			text: &#039;CREATE FROM A BLUEPRINT&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-description&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;Load a standard page skeleton from Help:Contents, &#039; +&lt;br /&gt;
				&#039;or continue editing the current source below.&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $controls = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-controls&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $select = $( &#039;&amp;lt;select&amp;gt;&#039;, {&lt;br /&gt;
			&#039;aria-label&#039;: &#039;Select page blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
			value: &#039;&#039;,&lt;br /&gt;
			text: &#039;Loading blueprints...&#039;&lt;br /&gt;
		} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Load Blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $sourceLink = $( &#039;&amp;lt;a&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-source&#039;,&lt;br /&gt;
			href: mw.util.getUrl( BLUEPRINT_PAGE ),&lt;br /&gt;
			text: &#039;View Blueprints&#039;,&lt;br /&gt;
			target: &#039;_blank&#039;,&lt;br /&gt;
			rel: &#039;noopener&#039;&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $status = $( &#039;&amp;lt;span&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-status&#039;,&lt;br /&gt;
			&#039;aria-live&#039;: &#039;polite&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		fetchBlueprintSections( api )&lt;br /&gt;
			.then( function ( sections ) {&lt;br /&gt;
				var blueprints = normalizeBlueprintSections( sections );&lt;br /&gt;
&lt;br /&gt;
				$select.empty();&lt;br /&gt;
&lt;br /&gt;
				$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
					value: &#039;&#039;,&lt;br /&gt;
					text: blueprints.length ?&lt;br /&gt;
						&#039;Select a blueprint...&#039; :&lt;br /&gt;
						&#039;No blueprints found&#039;&lt;br /&gt;
				} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
				blueprints.forEach( function ( blueprint ) {&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: blueprint.index,&lt;br /&gt;
						text: blueprint.label&lt;br /&gt;
					} )&lt;br /&gt;
						.attr( &#039;data-anchor&#039;, blueprint.anchor )&lt;br /&gt;
						.appendTo( $select );&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
				$select.prop( &#039;disabled&#039;, !blueprints.length );&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				$select.empty().append(&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: &#039;&#039;,&lt;br /&gt;
						text: &#039;Could not load blueprints&#039;&lt;br /&gt;
					} )&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				$status.text(&lt;br /&gt;
					&#039;Could not read Blueprint headings from Help:Contents.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
		$select.on( &#039;change&#039;, function () {&lt;br /&gt;
			var $option = $select.find( &#039;option:selected&#039; );&lt;br /&gt;
			var anchor = $option.attr( &#039;data-anchor&#039; ) || &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
&lt;br /&gt;
			if ( anchor ) {&lt;br /&gt;
				$sourceLink.attr(&lt;br /&gt;
					&#039;href&#039;,&lt;br /&gt;
					mw.util.getUrl( BLUEPRINT_PAGE ) + &#039;#&#039; + anchor&lt;br /&gt;
				);&lt;br /&gt;
				$sourceLink.text( &#039;View This Blueprint&#039; );&lt;br /&gt;
			} else {&lt;br /&gt;
				$sourceLink.attr( &#039;href&#039;, mw.util.getUrl( BLUEPRINT_PAGE ) );&lt;br /&gt;
				$sourceLink.text( &#039;View Blueprints&#039; );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$status.text( &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var sectionIndex = $select.val();&lt;br /&gt;
			var existing;&lt;br /&gt;
&lt;br /&gt;
			if ( !sectionIndex ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			existing = getEditorContents( $textarea );&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				existing.trim() &amp;amp;&amp;amp;&lt;br /&gt;
				!window.confirm(&lt;br /&gt;
					&#039;The editor already contains text. Replace it with the selected blueprint?&#039;&lt;br /&gt;
				)&lt;br /&gt;
			) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$select.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$status.text( &#039;Loading current blueprint from Help:Contents...&#039; );&lt;br /&gt;
&lt;br /&gt;
			fetchBlueprintWikitext( api, sectionIndex )&lt;br /&gt;
				.then( function ( sectionText ) {&lt;br /&gt;
					var blueprint = extractBlueprint( sectionText );&lt;br /&gt;
&lt;br /&gt;
					if ( blueprint === null ) {&lt;br /&gt;
						throw new Error( &#039;No &amp;lt;pre&amp;gt; block found.&#039; );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					setEditorContents( $textarea, blueprint );&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Blueprint loaded. Edit the fields below, then publish normally.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Could not load this blueprint. Check Help:Contents and make sure the section contains a &amp;lt;pre&amp;gt; block.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$select.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function init() {&lt;br /&gt;
		var action = mw.config.get( &#039;wgAction&#039; );&lt;br /&gt;
		var $textarea;&lt;br /&gt;
		var $tools;&lt;br /&gt;
		var $row;&lt;br /&gt;
		var $editorHost;&lt;br /&gt;
		var api;&lt;br /&gt;
&lt;br /&gt;
		if ( [ &#039;edit&#039;, &#039;submit&#039; ].indexOf( action ) === -1 ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea = $( &#039;#wpTextbox1&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$textarea.length ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( document.getElementById( TOOLS_ID ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		addStyles();&lt;br /&gt;
&lt;br /&gt;
		$tools = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: TOOLS_ID&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$row = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-editor-tools-row&#039;&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		buildBlueprintArea( $row, $textarea, api );&lt;br /&gt;
&lt;br /&gt;
		buildIconFinderLaunch( $row, $tools );&lt;br /&gt;
&lt;br /&gt;
		$editorHost = $textarea.closest( &#039;.wikiEditor-ui&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$editorHost.length ) {&lt;br /&gt;
			$editorHost = $textarea;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$tools.insertBefore( $editorHost );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	mw.loader.using( [&lt;br /&gt;
		&#039;mediawiki.api&#039;,&lt;br /&gt;
		&#039;mediawiki.util&#039;,&lt;br /&gt;
		&#039;jquery.textSelection&#039;&lt;br /&gt;
	] ).then( function () {&lt;br /&gt;
		$( init );&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:Common.js&amp;diff=168723</id>
		<title>MediaWiki:Common.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:Common.js&amp;diff=168723"/>
		<updated>2026-08-10T05:38:29Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Load Blueprint and Icon Finder tools on source editors&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* =========================================================&lt;br /&gt;
 * Raid Instance View&lt;br /&gt;
 *&lt;br /&gt;
 * Pages containing a heading named &amp;quot;Raid Instance&amp;quot; receive a&lt;br /&gt;
 * Normal View / Raid View toggle.&lt;br /&gt;
 *&lt;br /&gt;
 * Normal View:&lt;br /&gt;
 *   - Shows the regular article&lt;br /&gt;
 *   - Hides the complete Raid Instance section&lt;br /&gt;
 *&lt;br /&gt;
 * Raid View:&lt;br /&gt;
 *   - Shows only the Raid Instance section&lt;br /&gt;
 *   - Applies an ornate rose-gold raid theme&lt;br /&gt;
 *   - Adds ?raidview=1 to the URL for shareable raid links&lt;br /&gt;
 * ========================================================= */&lt;br /&gt;
&lt;br /&gt;
(function () {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	if (mw.config.get(&#039;wgAction&#039;) !== &#039;view&#039;) {&lt;br /&gt;
		return;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	mw.loader.using(&#039;mediawiki.util&#039;).then(function () {&lt;br /&gt;
		mw.util.addCSS(`&lt;br /&gt;
			/* ---------- View switching ---------- */&lt;br /&gt;
&lt;br /&gt;
			.eql-raid-view-hidden {&lt;br /&gt;
				display: none !important;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			#eql-raid-view-toolbar {&lt;br /&gt;
				display: flex;&lt;br /&gt;
				justify-content: center;&lt;br /&gt;
				align-items: center;&lt;br /&gt;
				margin: 1rem 0 1.5rem;&lt;br /&gt;
				min-height: 84px;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			#eql-raid-view-toggle {&lt;br /&gt;
				position: relative;&lt;br /&gt;
				display: inline-flex;&lt;br /&gt;
				align-items: center;&lt;br /&gt;
				justify-content: center;&lt;br /&gt;
				gap: 0.65rem;&lt;br /&gt;
				min-height: 40px;&lt;br /&gt;
				padding: 0.55rem 1.3rem;&lt;br /&gt;
				border: 0;&lt;br /&gt;
				font-family: inherit;&lt;br /&gt;
				font-size: 0.9rem;&lt;br /&gt;
				font-weight: 700;&lt;br /&gt;
				letter-spacing: 0.055em;&lt;br /&gt;
				line-height: 1;&lt;br /&gt;
				text-transform: uppercase;&lt;br /&gt;
				cursor: pointer;&lt;br /&gt;
				transition:&lt;br /&gt;
					transform 150ms ease,&lt;br /&gt;
					filter 150ms ease,&lt;br /&gt;
					background 150ms ease,&lt;br /&gt;
					border-color 150ms ease;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			#eql-raid-view-toggle:hover {&lt;br /&gt;
				transform: translateY(-2px);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			#eql-raid-view-toggle:active {&lt;br /&gt;
				transform: translateY(1px);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			#eql-raid-view-toggle:focus-visible {&lt;br /&gt;
				outline: 2px solid #fff2c2;&lt;br /&gt;
				outline-offset: 4px;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			.eql-raid-view-toggle-icon {&lt;br /&gt;
				font-size: 1.15em;&lt;br /&gt;
				line-height: 1;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* Large, ornate Raid View button */&lt;br /&gt;
			#eql-raid-view-toggle.eql-raid-view-toggle--raid {&lt;br /&gt;
				box-sizing: border-box;&lt;br /&gt;
				width: min(100%, 420px);&lt;br /&gt;
				min-height: 80px;&lt;br /&gt;
				padding: 1rem 3.5rem;&lt;br /&gt;
				color: #ffe2dc;&lt;br /&gt;
				background:&lt;br /&gt;
					linear-gradient(&lt;br /&gt;
						180deg,&lt;br /&gt;
						#8f4747 0%,&lt;br /&gt;
						#6e3036 48%,&lt;br /&gt;
						#3b1c25 100%&lt;br /&gt;
					);&lt;br /&gt;
				clip-path: polygon(&lt;br /&gt;
					0 50%,&lt;br /&gt;
					10px 37%,&lt;br /&gt;
					6px 17%,&lt;br /&gt;
					26px 13%,&lt;br /&gt;
					35px 0,&lt;br /&gt;
					calc(100% - 35px) 0,&lt;br /&gt;
					calc(100% - 26px) 13%,&lt;br /&gt;
					calc(100% - 6px) 17%,&lt;br /&gt;
					calc(100% - 10px) 37%,&lt;br /&gt;
					100% 50%,&lt;br /&gt;
					calc(100% - 10px) 63%,&lt;br /&gt;
					calc(100% - 6px) 83%,&lt;br /&gt;
					calc(100% - 26px) 87%,&lt;br /&gt;
					calc(100% - 35px) 100%,&lt;br /&gt;
					35px 100%,&lt;br /&gt;
					26px 87%,&lt;br /&gt;
					6px 83%,&lt;br /&gt;
					10px 63%&lt;br /&gt;
				);&lt;br /&gt;
				font-size: 1.25rem;&lt;br /&gt;
				letter-spacing: 0.09em;&lt;br /&gt;
				filter:&lt;br /&gt;
					drop-shadow(0 3px 3px rgba(0, 0, 0, 0.68))&lt;br /&gt;
					drop-shadow(0 0 9px rgba(224, 96, 80, 0.34));&lt;br /&gt;
				text-shadow:&lt;br /&gt;
					0 1px 1px rgba(0, 0, 0, 0.9),&lt;br /&gt;
					0 0 8px rgba(255, 182, 170, 0.28);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			#eql-raid-view-toggle.eql-raid-view-toggle--raid:hover {&lt;br /&gt;
				background:&lt;br /&gt;
					linear-gradient(&lt;br /&gt;
						180deg,&lt;br /&gt;
						#a85651 0%,&lt;br /&gt;
						#7c3439 48%,&lt;br /&gt;
						#481f29 100%&lt;br /&gt;
					);&lt;br /&gt;
				filter:&lt;br /&gt;
					drop-shadow(0 4px 4px rgba(0, 0, 0, 0.72))&lt;br /&gt;
					drop-shadow(0 0 14px rgba(255, 122, 109, 0.5));&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* Restrained Normal View button */&lt;br /&gt;
			#eql-raid-view-toggle.eql-raid-view-toggle--normal {&lt;br /&gt;
				color: var(--eql-gold, #d8b75c);&lt;br /&gt;
				border: 1px solid var(&lt;br /&gt;
					--eql-border,&lt;br /&gt;
					rgba(193, 151, 57, 0.36)&lt;br /&gt;
				);&lt;br /&gt;
				border-radius: 3px;&lt;br /&gt;
				background:&lt;br /&gt;
					linear-gradient(&lt;br /&gt;
						180deg,&lt;br /&gt;
						rgba(44, 48, 55, 0.94),&lt;br /&gt;
						rgba(21, 25, 31, 0.96)&lt;br /&gt;
					);&lt;br /&gt;
				box-shadow:&lt;br /&gt;
					inset 0 1px rgba(255, 255, 255, 0.045),&lt;br /&gt;
					0 2px 5px rgba(0, 0, 0, 0.35);&lt;br /&gt;
				text-shadow: 0 1px 1px rgba(0, 0, 0, 0.9);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			#eql-raid-view-toggle.eql-raid-view-toggle--normal:hover {&lt;br /&gt;
				color: #f2df9b;&lt;br /&gt;
				border-color: rgba(216, 183, 92, 0.6);&lt;br /&gt;
				background:&lt;br /&gt;
					linear-gradient(&lt;br /&gt;
						180deg,&lt;br /&gt;
						rgba(55, 59, 66, 0.96),&lt;br /&gt;
						rgba(28, 32, 38, 0.98)&lt;br /&gt;
					);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* ---------- Ornate Raid View article ---------- */&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view #mw-content-text &amp;gt; .mw-parser-output {&lt;br /&gt;
				position: relative;&lt;br /&gt;
				isolation: isolate;&lt;br /&gt;
				box-sizing: border-box;&lt;br /&gt;
				width: 100%;&lt;br /&gt;
				max-width: 1120px;&lt;br /&gt;
				margin: 1.8rem auto 2rem;&lt;br /&gt;
				padding: clamp(1.4rem, 3vw, 2.4rem);&lt;br /&gt;
				border: 1px solid rgba(224, 189, 159, 0.48);&lt;br /&gt;
				border-top: 3px solid #e0bd9f;&lt;br /&gt;
				border-bottom: 3px solid #783b40;&lt;br /&gt;
				background-color: rgba(15, 13, 19, 0.94);&lt;br /&gt;
				background-image:&lt;br /&gt;
					linear-gradient(&lt;br /&gt;
						135deg,&lt;br /&gt;
						rgba(224, 96, 80, 0.045) 25%,&lt;br /&gt;
						transparent 25%&lt;br /&gt;
					),&lt;br /&gt;
					linear-gradient(&lt;br /&gt;
						225deg,&lt;br /&gt;
						rgba(224, 96, 80, 0.045) 25%,&lt;br /&gt;
						transparent 25%&lt;br /&gt;
					),&lt;br /&gt;
					radial-gradient(&lt;br /&gt;
						circle at 50% 0,&lt;br /&gt;
						rgba(143, 71, 71, 0.26),&lt;br /&gt;
						transparent 48%&lt;br /&gt;
					),&lt;br /&gt;
					linear-gradient(&lt;br /&gt;
						180deg,&lt;br /&gt;
						rgba(44, 20, 29, 0.42),&lt;br /&gt;
						rgba(10, 12, 18, 0.96) 42%,&lt;br /&gt;
						rgba(18, 12, 18, 0.98)&lt;br /&gt;
					);&lt;br /&gt;
				background-position:&lt;br /&gt;
					0 0,&lt;br /&gt;
					18px 18px,&lt;br /&gt;
					0 0,&lt;br /&gt;
					0 0;&lt;br /&gt;
				background-size:&lt;br /&gt;
					36px 36px,&lt;br /&gt;
					36px 36px,&lt;br /&gt;
					auto,&lt;br /&gt;
					auto;&lt;br /&gt;
				box-shadow:&lt;br /&gt;
					0 18px 45px rgba(0, 0, 0, 0.52),&lt;br /&gt;
					0 0 28px rgba(143, 71, 71, 0.12),&lt;br /&gt;
					inset 0 0 45px rgba(126, 49, 55, 0.08);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view #mw-content-text &amp;gt; .mw-parser-output::before {&lt;br /&gt;
				content: &amp;quot;◆  ✦  ◆&amp;quot;;&lt;br /&gt;
				position: absolute;&lt;br /&gt;
				z-index: 2;&lt;br /&gt;
				top: -1.15rem;&lt;br /&gt;
				left: 50%;&lt;br /&gt;
				min-width: 150px;&lt;br /&gt;
				padding: 0.38rem 2rem;&lt;br /&gt;
				color: #f3c1bd;&lt;br /&gt;
				background:&lt;br /&gt;
					linear-gradient(&lt;br /&gt;
						180deg,&lt;br /&gt;
						#683039,&lt;br /&gt;
						#2a1821&lt;br /&gt;
					);&lt;br /&gt;
				clip-path: polygon(&lt;br /&gt;
					0 50%,&lt;br /&gt;
					14px 25%,&lt;br /&gt;
					28px 25%,&lt;br /&gt;
					36px 0,&lt;br /&gt;
					calc(100% - 36px) 0,&lt;br /&gt;
					calc(100% - 28px) 25%,&lt;br /&gt;
					calc(100% - 14px) 25%,&lt;br /&gt;
					100% 50%,&lt;br /&gt;
					calc(100% - 14px) 75%,&lt;br /&gt;
					calc(100% - 28px) 75%,&lt;br /&gt;
					calc(100% - 36px) 100%,&lt;br /&gt;
					36px 100%,&lt;br /&gt;
					28px 75%,&lt;br /&gt;
					14px 75%&lt;br /&gt;
				);&lt;br /&gt;
				font-size: 0.82rem;&lt;br /&gt;
				letter-spacing: 0.45rem;&lt;br /&gt;
				text-align: center;&lt;br /&gt;
				text-indent: 0.45rem;&lt;br /&gt;
				text-shadow: 0 0 8px rgba(255, 182, 170, 0.45);&lt;br /&gt;
				filter:&lt;br /&gt;
					drop-shadow(0 2px 3px rgba(0, 0, 0, 0.8))&lt;br /&gt;
					drop-shadow(0 0 7px rgba(224, 96, 80, 0.25));&lt;br /&gt;
				transform: translateX(-50%);&lt;br /&gt;
				pointer-events: none;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view #mw-content-text &amp;gt; .mw-parser-output::after {&lt;br /&gt;
				content: &amp;quot;&amp;quot;;&lt;br /&gt;
				position: absolute;&lt;br /&gt;
				right: 8%;&lt;br /&gt;
				bottom: -9px;&lt;br /&gt;
				left: 8%;&lt;br /&gt;
				height: 15px;&lt;br /&gt;
				background:&lt;br /&gt;
					linear-gradient(&lt;br /&gt;
						90deg,&lt;br /&gt;
						transparent,&lt;br /&gt;
						#783b40 18%,&lt;br /&gt;
						#e0bd9f 50%,&lt;br /&gt;
						#783b40 82%,&lt;br /&gt;
						transparent&lt;br /&gt;
					);&lt;br /&gt;
				clip-path: polygon(&lt;br /&gt;
					0 42%,&lt;br /&gt;
					43% 42%,&lt;br /&gt;
					46% 0,&lt;br /&gt;
					50% 42%,&lt;br /&gt;
					54% 0,&lt;br /&gt;
					57% 42%,&lt;br /&gt;
					100% 42%,&lt;br /&gt;
					100% 58%,&lt;br /&gt;
					57% 58%,&lt;br /&gt;
					54% 100%,&lt;br /&gt;
					50% 58%,&lt;br /&gt;
					46% 100%,&lt;br /&gt;
					43% 58%,&lt;br /&gt;
					0 58%&lt;br /&gt;
				);&lt;br /&gt;
				opacity: 0.8;&lt;br /&gt;
				pointer-events: none;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* Raid Instance title */&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view #Raid_Instance {&lt;br /&gt;
				margin-top: 0.3rem;&lt;br /&gt;
				margin-bottom: 1.35rem;&lt;br /&gt;
				color: #ffe2dc;&lt;br /&gt;
				font-variant: small-caps;&lt;br /&gt;
				letter-spacing: 0.11em;&lt;br /&gt;
				text-align: center;&lt;br /&gt;
				text-transform: uppercase;&lt;br /&gt;
				text-shadow:&lt;br /&gt;
					0 2px 2px rgba(0, 0, 0, 0.95),&lt;br /&gt;
					0 0 12px rgba(255, 122, 109, 0.32);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view #Raid_Instance::before,&lt;br /&gt;
			html.eql-raid-view #Raid_Instance::after {&lt;br /&gt;
				content: &amp;quot;◆&amp;quot;;&lt;br /&gt;
				display: inline-block;&lt;br /&gt;
				margin: 0 0.65em;&lt;br /&gt;
				color: #e0bd9f;&lt;br /&gt;
				font-size: 0.55em;&lt;br /&gt;
				vertical-align: 0.2em;&lt;br /&gt;
				text-shadow: 0 0 8px rgba(216, 183, 92, 0.4);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* Raid subsection headings */&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view&lt;br /&gt;
			#mw-content-text&lt;br /&gt;
			.mw-parser-output&lt;br /&gt;
			.mw-heading2 h2 {&lt;br /&gt;
				color: #f3c1bd;&lt;br /&gt;
				border-bottom-color: rgba(224, 189, 159, 0.42);&lt;br /&gt;
				letter-spacing: 0.035em;&lt;br /&gt;
				text-shadow:&lt;br /&gt;
					0 1px 1px rgba(0, 0, 0, 0.9),&lt;br /&gt;
					0 0 7px rgba(143, 71, 71, 0.28);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view&lt;br /&gt;
			#mw-content-text&lt;br /&gt;
			.mw-parser-output&lt;br /&gt;
			.mw-heading2 h2::before {&lt;br /&gt;
				content: &amp;quot;◆&amp;quot;;&lt;br /&gt;
				margin-right: 0.55em;&lt;br /&gt;
				color: #a85651;&lt;br /&gt;
				font-size: 0.55em;&lt;br /&gt;
				vertical-align: 0.18em;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* Raid tables and maps */&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view&lt;br /&gt;
			#mw-content-text&lt;br /&gt;
			.mw-parser-output &amp;gt; table {&lt;br /&gt;
				border-color: rgba(224, 189, 159, 0.42) !important;&lt;br /&gt;
				background: rgba(9, 11, 17, 0.52);&lt;br /&gt;
				box-shadow:&lt;br /&gt;
					0 7px 18px rgba(0, 0, 0, 0.28),&lt;br /&gt;
					inset 0 0 22px rgba(143, 71, 71, 0.04);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view&lt;br /&gt;
			#mw-content-text&lt;br /&gt;
			.zoneTopTable {&lt;br /&gt;
				border: 1px solid rgba(224, 189, 159, 0.28);&lt;br /&gt;
				background: rgba(13, 15, 21, 0.58);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view&lt;br /&gt;
			#mw-content-text&lt;br /&gt;
			.zoneTopTable th {&lt;br /&gt;
				color: #f3c1bd;&lt;br /&gt;
				background: rgba(111, 47, 53, 0.16);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view&lt;br /&gt;
			#mw-content-text&lt;br /&gt;
			figure img {&lt;br /&gt;
				border-color: rgba(224, 189, 159, 0.48);&lt;br /&gt;
				box-shadow:&lt;br /&gt;
					0 7px 18px rgba(0, 0, 0, 0.45),&lt;br /&gt;
					0 0 12px rgba(143, 71, 71, 0.1);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* ---------- Table of contents ---------- */&lt;br /&gt;
&lt;br /&gt;
			/* Normal View hides the Raid Instance TOC branch. */&lt;br /&gt;
			html.eql-normal-view #toc-Raid_Instance {&lt;br /&gt;
				display: none !important;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* Raid View shows only the Raid Instance TOC branch. */&lt;br /&gt;
			html.eql-raid-view&lt;br /&gt;
			#mw-panel-toc-list &amp;gt; li:not(#toc-Raid_Instance) {&lt;br /&gt;
				display: none !important;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view&lt;br /&gt;
			#toc-Raid_Instance&lt;br /&gt;
			.vector-toc-numb {&lt;br /&gt;
				display: none;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			html.eql-raid-view&lt;br /&gt;
			#toc-Raid_Instance &amp;gt; .vector-toc-link {&lt;br /&gt;
				color: #f3c1bd;&lt;br /&gt;
				border-left-color: #a85651;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* ---------- Responsive sizing ---------- */&lt;br /&gt;
&lt;br /&gt;
			@media (max-width: 720px) {&lt;br /&gt;
				#eql-raid-view-toolbar {&lt;br /&gt;
					justify-content: center;&lt;br /&gt;
					min-height: 72px;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				#eql-raid-view-toggle {&lt;br /&gt;
					width: min(100%, 420px);&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				#eql-raid-view-toggle.eql-raid-view-toggle--raid {&lt;br /&gt;
					min-height: 68px;&lt;br /&gt;
					padding: 0.85rem 2.5rem;&lt;br /&gt;
					font-size: 1.08rem;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				html.eql-raid-view&lt;br /&gt;
				#mw-content-text &amp;gt; .mw-parser-output {&lt;br /&gt;
					margin-top: 1.6rem;&lt;br /&gt;
					padding: 1.5rem 1rem;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				html.eql-raid-view #Raid_Instance {&lt;br /&gt;
					font-size: 1.7rem;&lt;br /&gt;
					letter-spacing: 0.07em;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				html.eql-raid-view #Raid_Instance::before,&lt;br /&gt;
				html.eql-raid-view #Raid_Instance::after {&lt;br /&gt;
					margin: 0 0.35em;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			@media (prefers-reduced-motion: reduce) {&lt;br /&gt;
				#eql-raid-view-toggle {&lt;br /&gt;
					transition: none;&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		`);&lt;br /&gt;
&lt;br /&gt;
		function getHeadingLevel(element) {&lt;br /&gt;
			var match;&lt;br /&gt;
&lt;br /&gt;
			if (!element || !element.tagName) {&lt;br /&gt;
				return 0;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			match = /^H([1-6])$/.exec(element.tagName);&lt;br /&gt;
&lt;br /&gt;
			return match ? parseInt(match[1], 10) : 0;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		function findDirectHeading(element) {&lt;br /&gt;
			var children;&lt;br /&gt;
			var i;&lt;br /&gt;
&lt;br /&gt;
			if (getHeadingLevel(element)) {&lt;br /&gt;
				return element;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			children = element.children;&lt;br /&gt;
&lt;br /&gt;
			for (i = 0; i &amp;lt; children.length; i++) {&lt;br /&gt;
				if (getHeadingLevel(children[i])) {&lt;br /&gt;
					return children[i];&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		function getHashTarget() {&lt;br /&gt;
			var id;&lt;br /&gt;
&lt;br /&gt;
			if (!window.location.hash) {&lt;br /&gt;
				return null;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			try {&lt;br /&gt;
				id = decodeURIComponent(window.location.hash.slice(1));&lt;br /&gt;
			} catch (error) {&lt;br /&gt;
				id = window.location.hash.slice(1);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return document.getElementById(id);&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		function initializeRaidView($content) {&lt;br /&gt;
			var hookRoot = $content &amp;amp;&amp;amp; $content[0] ?&lt;br /&gt;
				$content[0] :&lt;br /&gt;
				document;&lt;br /&gt;
&lt;br /&gt;
			var parserOutput = hookRoot.matches &amp;amp;&amp;amp;&lt;br /&gt;
				hookRoot.matches(&#039;.mw-parser-output&#039;) ?&lt;br /&gt;
				hookRoot :&lt;br /&gt;
				hookRoot.querySelector(&#039;.mw-parser-output&#039;);&lt;br /&gt;
&lt;br /&gt;
			var contentText;&lt;br /&gt;
			var raidHeading;&lt;br /&gt;
			var raidAnchor;&lt;br /&gt;
			var headingCandidates;&lt;br /&gt;
			var raidStart;&lt;br /&gt;
			var raidLevel;&lt;br /&gt;
			var allNodes;&lt;br /&gt;
			var raidNodes = [];&lt;br /&gt;
			var raidNodeSet;&lt;br /&gt;
			var startIndex;&lt;br /&gt;
			var i;&lt;br /&gt;
			var nextHeading;&lt;br /&gt;
			var toolbar;&lt;br /&gt;
			var button;&lt;br /&gt;
			var icon;&lt;br /&gt;
			var label;&lt;br /&gt;
			var existingToolbar;&lt;br /&gt;
			var initialRaidView;&lt;br /&gt;
			var requestedView;&lt;br /&gt;
			var hashTarget;&lt;br /&gt;
&lt;br /&gt;
			if (!parserOutput) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			existingToolbar = document.getElementById(&lt;br /&gt;
				&#039;eql-raid-view-toolbar&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			if (existingToolbar) {&lt;br /&gt;
				existingToolbar.remove();&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			document.documentElement.classList.remove(&lt;br /&gt;
				&#039;eql-normal-view&#039;,&lt;br /&gt;
				&#039;eql-raid-view&#039;&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			/* Prefer the stable MediaWiki heading ID. */&lt;br /&gt;
			raidAnchor = parserOutput.querySelector(&#039;#Raid_Instance&#039;);&lt;br /&gt;
&lt;br /&gt;
			if (raidAnchor) {&lt;br /&gt;
				if (getHeadingLevel(raidAnchor)) {&lt;br /&gt;
					raidHeading = raidAnchor;&lt;br /&gt;
				} else {&lt;br /&gt;
					raidHeading =&lt;br /&gt;
						raidAnchor.closest(&#039;h1, h2, h3, h4, h5, h6&#039;) ||&lt;br /&gt;
						raidAnchor.querySelector(&lt;br /&gt;
							&#039;h1, h2, h3, h4, h5, h6&#039;&lt;br /&gt;
						);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* Fallback for unusual heading markup. */&lt;br /&gt;
			if (!raidHeading) {&lt;br /&gt;
				headingCandidates = parserOutput.querySelectorAll(&lt;br /&gt;
					&#039;h1, h2, h3, h4, h5, h6&#039;&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				for (i = 0; i &amp;lt; headingCandidates.length; i++) {&lt;br /&gt;
					if (&lt;br /&gt;
						headingCandidates[i].textContent&lt;br /&gt;
							.trim()&lt;br /&gt;
							.toLowerCase() === &#039;raid instance&#039;&lt;br /&gt;
					) {&lt;br /&gt;
						raidHeading = headingCandidates[i];&lt;br /&gt;
						break;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* Pages without the heading receive no button. */&lt;br /&gt;
			if (!raidHeading) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			raidLevel = getHeadingLevel(raidHeading);&lt;br /&gt;
			raidStart = raidHeading;&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * MediaWiki 1.45 wraps headings in .mw-heading elements.&lt;br /&gt;
			 * Older versions may leave headings as direct children.&lt;br /&gt;
			 */&lt;br /&gt;
			while (&lt;br /&gt;
				raidStart.parentElement &amp;amp;&amp;amp;&lt;br /&gt;
				raidStart.parentElement !== parserOutput&lt;br /&gt;
			) {&lt;br /&gt;
				raidStart = raidStart.parentElement;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if (raidStart.parentElement !== parserOutput) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			allNodes = Array.prototype.slice.call(&lt;br /&gt;
				parserOutput.children&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			startIndex = allNodes.indexOf(raidStart);&lt;br /&gt;
&lt;br /&gt;
			if (startIndex === -1) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * The raid section begins at &amp;quot;Raid Instance&amp;quot; and continues&lt;br /&gt;
			 * until the next heading of the same or a higher level.&lt;br /&gt;
			 */&lt;br /&gt;
			for (i = startIndex; i &amp;lt; allNodes.length; i++) {&lt;br /&gt;
				if (i &amp;gt; startIndex) {&lt;br /&gt;
					nextHeading = findDirectHeading(allNodes[i]);&lt;br /&gt;
&lt;br /&gt;
					if (&lt;br /&gt;
						nextHeading &amp;amp;&amp;amp;&lt;br /&gt;
						getHeadingLevel(nextHeading) &amp;lt;= raidLevel&lt;br /&gt;
					) {&lt;br /&gt;
						break;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				raidNodes.push(allNodes[i]);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			raidNodeSet = new Set(raidNodes);&lt;br /&gt;
			contentText = document.getElementById(&#039;mw-content-text&#039;);&lt;br /&gt;
&lt;br /&gt;
			if (!contentText) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			/* Build the toggle. */&lt;br /&gt;
			toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
			toolbar.id = &#039;eql-raid-view-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
			button = document.createElement(&#039;button&#039;);&lt;br /&gt;
			button.id = &#039;eql-raid-view-toggle&#039;;&lt;br /&gt;
			button.type = &#039;button&#039;;&lt;br /&gt;
&lt;br /&gt;
			icon = document.createElement(&#039;span&#039;);&lt;br /&gt;
			icon.className = &#039;eql-raid-view-toggle-icon&#039;;&lt;br /&gt;
			icon.setAttribute(&#039;aria-hidden&#039;, &#039;true&#039;);&lt;br /&gt;
&lt;br /&gt;
			label = document.createElement(&#039;span&#039;);&lt;br /&gt;
			label.className = &#039;eql-raid-view-toggle-label&#039;;&lt;br /&gt;
&lt;br /&gt;
			button.appendChild(icon);&lt;br /&gt;
			button.appendChild(label);&lt;br /&gt;
			toolbar.appendChild(button);&lt;br /&gt;
&lt;br /&gt;
			contentText.parentNode.insertBefore(toolbar, contentText);&lt;br /&gt;
&lt;br /&gt;
			function targetIsInRaidSection(target) {&lt;br /&gt;
				var j;&lt;br /&gt;
&lt;br /&gt;
				if (!target) {&lt;br /&gt;
					return false;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				for (j = 0; j &amp;lt; raidNodes.length; j++) {&lt;br /&gt;
					if (&lt;br /&gt;
						raidNodes[j] === target ||&lt;br /&gt;
						raidNodes[j].contains(target)&lt;br /&gt;
					) {&lt;br /&gt;
						return true;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return false;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			function updateUrl(isRaidView) {&lt;br /&gt;
				var url;&lt;br /&gt;
				var currentTarget;&lt;br /&gt;
&lt;br /&gt;
				try {&lt;br /&gt;
					url = new URL(window.location.href);&lt;br /&gt;
&lt;br /&gt;
					if (isRaidView) {&lt;br /&gt;
						url.searchParams.set(&#039;raidview&#039;, &#039;1&#039;);&lt;br /&gt;
					} else {&lt;br /&gt;
						url.searchParams.delete(&#039;raidview&#039;);&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					/*&lt;br /&gt;
					 * Remove a fragment if it points to content hidden&lt;br /&gt;
					 * by the newly selected view.&lt;br /&gt;
					 */&lt;br /&gt;
					currentTarget = getHashTarget();&lt;br /&gt;
&lt;br /&gt;
					if (&lt;br /&gt;
						currentTarget &amp;amp;&amp;amp;&lt;br /&gt;
						targetIsInRaidSection(currentTarget) !==&lt;br /&gt;
							isRaidView&lt;br /&gt;
					) {&lt;br /&gt;
						url.hash = &#039;&#039;;&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					window.history.replaceState(&lt;br /&gt;
						window.history.state,&lt;br /&gt;
						document.title,&lt;br /&gt;
						url.toString()&lt;br /&gt;
					);&lt;br /&gt;
				} catch (error) {&lt;br /&gt;
					/* View switching still works without URL changes. */&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			function setRaidView(isRaidView, changeUrl) {&lt;br /&gt;
				allNodes.forEach(function (node) {&lt;br /&gt;
					var shouldHide = isRaidView ?&lt;br /&gt;
						!raidNodeSet.has(node) :&lt;br /&gt;
						raidNodeSet.has(node);&lt;br /&gt;
&lt;br /&gt;
					node.classList.toggle(&lt;br /&gt;
						&#039;eql-raid-view-hidden&#039;,&lt;br /&gt;
						shouldHide&lt;br /&gt;
					);&lt;br /&gt;
				});&lt;br /&gt;
&lt;br /&gt;
				document.documentElement.classList.toggle(&lt;br /&gt;
					&#039;eql-raid-view&#039;,&lt;br /&gt;
					isRaidView&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				document.documentElement.classList.toggle(&lt;br /&gt;
					&#039;eql-normal-view&#039;,&lt;br /&gt;
					!isRaidView&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				if (isRaidView) {&lt;br /&gt;
					icon.textContent = &#039;◆&#039;;&lt;br /&gt;
					label.textContent = &#039;Switch to Normal View&#039;;&lt;br /&gt;
&lt;br /&gt;
					button.className =&lt;br /&gt;
						&#039;eql-raid-view-toggle--normal&#039;;&lt;br /&gt;
&lt;br /&gt;
					button.setAttribute(&lt;br /&gt;
						&#039;aria-label&#039;,&lt;br /&gt;
						&#039;Switch to Normal View&#039;&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					button.title = &#039;Show the normal zone article&#039;;&lt;br /&gt;
				} else {&lt;br /&gt;
					icon.textContent = &#039;✦&#039;;&lt;br /&gt;
					label.textContent = &#039;Switch to Raid View&#039;;&lt;br /&gt;
&lt;br /&gt;
					button.className =&lt;br /&gt;
						&#039;eql-raid-view-toggle--raid&#039;;&lt;br /&gt;
&lt;br /&gt;
					button.setAttribute(&lt;br /&gt;
						&#039;aria-label&#039;,&lt;br /&gt;
						&#039;Switch to Raid View&#039;&lt;br /&gt;
					);&lt;br /&gt;
&lt;br /&gt;
					button.title = &#039;Show the Raid Instance article&#039;;&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				if (changeUrl) {&lt;br /&gt;
					updateUrl(isRaidView);&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			button.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
				setRaidView(&lt;br /&gt;
					!document.documentElement.classList.contains(&lt;br /&gt;
						&#039;eql-raid-view&#039;&lt;br /&gt;
					),&lt;br /&gt;
					true&lt;br /&gt;
				);&lt;br /&gt;
			});&lt;br /&gt;
&lt;br /&gt;
			requestedView = new URL(&lt;br /&gt;
				window.location.href&lt;br /&gt;
			).searchParams.get(&#039;raidview&#039;);&lt;br /&gt;
&lt;br /&gt;
			hashTarget = getHashTarget();&lt;br /&gt;
&lt;br /&gt;
			/*&lt;br /&gt;
			 * ?raidview=1 explicitly requests Raid View.&lt;br /&gt;
			 * Direct links to headings within the raid section also&lt;br /&gt;
			 * select Raid View automatically.&lt;br /&gt;
			 */&lt;br /&gt;
			initialRaidView =&lt;br /&gt;
				requestedView === &#039;1&#039; ||&lt;br /&gt;
				targetIsInRaidSection(hashTarget);&lt;br /&gt;
&lt;br /&gt;
			setRaidView(initialRaidView, false);&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		mw.hook(&#039;wikipage.content&#039;).add(initializeRaidView);&lt;br /&gt;
	});&lt;br /&gt;
}());&lt;br /&gt;
/* =====================================================&lt;br /&gt;
   EQL editor tools&lt;br /&gt;
   - Dynamic Blueprint selector on empty/new pages&lt;br /&gt;
   - Icon Finder on every source-edit page&lt;br /&gt;
   ===================================================== */&lt;br /&gt;
&lt;br /&gt;
( function () {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	if (&lt;br /&gt;
		[&lt;br /&gt;
			&#039;edit&#039;,&lt;br /&gt;
			&#039;submit&#039;&lt;br /&gt;
		].indexOf(&lt;br /&gt;
			mw.config.get( &#039;wgAction&#039; )&lt;br /&gt;
		) === -1&lt;br /&gt;
	) {&lt;br /&gt;
		return;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	mw.loader.load(&lt;br /&gt;
		&#039;/index.php?&#039; +&lt;br /&gt;
		&#039;title=MediaWiki:BlueprintLoader.js&#039; +&lt;br /&gt;
		&#039;&amp;amp;action=raw&#039; +&lt;br /&gt;
		&#039;&amp;amp;ctype=text/javascript&#039;&lt;br /&gt;
	);&lt;br /&gt;
}() );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168722</id>
		<title>MediaWiki:IconFinder.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:IconFinder.js&amp;diff=168722"/>
		<updated>2026-08-10T05:38:28Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Add editor Icon Finder&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Icon Finder&lt;br /&gt;
 *&lt;br /&gt;
 * Lazy-loaded from MediaWiki:BlueprintLoader.js.&lt;br /&gt;
 *&lt;br /&gt;
 * Matching workflow:&lt;br /&gt;
 * 1. User pastes a screen capture containing JUST the icon.&lt;br /&gt;
 * 2. Read the current image list from [[Icon List]].&lt;br /&gt;
 * 3. Resolve only existing uploaded files through imageinfo.&lt;br /&gt;
 * 4. Build compact perceptual fingerprints in the browser.&lt;br /&gt;
 * 5. Cache that fingerprint index in IndexedDB keyed to the Icon List revision.&lt;br /&gt;
 * 6. Rank possible matches by perceptual/pixel similarity.&lt;br /&gt;
 *&lt;br /&gt;
 * No icon-catalog requests or image downloads happen until the user actually&lt;br /&gt;
 * pastes an image into the Icon Finder.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var ICON_LIST_PAGE = &#039;Icon List&#039;;&lt;br /&gt;
	var DB_NAME = &#039;eql-icon-finder&#039;;&lt;br /&gt;
	var DB_VERSION = 1;&lt;br /&gt;
	var STORE_NAME = &#039;indexes&#039;;&lt;br /&gt;
	var ALGO_VERSION = 3;&lt;br /&gt;
	var COLOR_GRID = 12;&lt;br /&gt;
	var IMAGEINFO_BATCH = 50;&lt;br /&gt;
	var DOWNLOAD_CONCURRENCY = 8;&lt;br /&gt;
	var RESULT_COUNT = 10;&lt;br /&gt;
&lt;br /&gt;
	var initializedHost = null;&lt;br /&gt;
	var memoryIndex = null;&lt;br /&gt;
	var currentPreviewUrl = null;&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-icon-finder-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;.eql-iconfinder-shell{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(230px,.62fr) minmax(0,1.38fr);&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:start;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste{&#039; +&lt;br /&gt;
					&#039;min-height:160px;&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;justify-content:center;&#039; +&lt;br /&gt;
					&#039;gap:.45rem;&#039; +&lt;br /&gt;
					&#039;padding:1rem;&#039; +&lt;br /&gt;
					&#039;background:#080d13;&#039; +&lt;br /&gt;
					&#039;border:2px dashed rgba(216,183,92,.45);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;color:#dfe7ef;&#039; +&lt;br /&gt;
					&#039;text-align:center;&#039; +&lt;br /&gt;
					&#039;cursor:paste;&#039; +&lt;br /&gt;
					&#039;outline:none;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-paste:focus{&#039; +&lt;br /&gt;
					&#039;border-color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 0 0 2px rgba(216,183,92,.13);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-instruction{&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-subinstruction{&#039; +&lt;br /&gt;
					&#039;color:#9faaba;&#039; +&lt;br /&gt;
					&#039;font-size:.88rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview{&#039; +&lt;br /&gt;
					&#039;display:none;&#039; +&lt;br /&gt;
					&#039;width:72px;&#039; +&lt;br /&gt;
					&#039;height:72px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;image-rendering:auto;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.14);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-preview.is-visible{display:block;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-status{&#039; +&lt;br /&gt;
					&#039;margin-top:.65rem;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.4;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .55rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-empty{&#039; +&lt;br /&gt;
					&#039;padding:.9rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.025);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.08);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
					&#039;color:#96a2b2;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-results-grid{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:54px minmax(0,1fr);&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;padding:.65rem;&#039; +&lt;br /&gt;
					&#039;background:rgba(255,255,255,.03);&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.09);&#039; +&lt;br /&gt;
					&#039;border-radius:7px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result img{&#039; +&lt;br /&gt;
					&#039;width:52px;&#039; +&lt;br /&gt;
					&#039;height:52px;&#039; +&lt;br /&gt;
					&#039;object-fit:contain;&#039; +&lt;br /&gt;
					&#039;background:#000;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(255,255,255,.1);&#039; +&lt;br /&gt;
					&#039;border-radius:4px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-name{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.18rem;&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.2;&#039; +&lt;br /&gt;
					&#039;word-break:break-word;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-score{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-bottom:.28rem;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-size:.8rem;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-meta{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;color:#8f9bac;&#039; +&lt;br /&gt;
					&#039;font-size:.76rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.25;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-result-id{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin:.2rem 0 .35rem;&#039; +&lt;br /&gt;
					&#039;color:#fff0b5;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:800;&#039; +&lt;br /&gt;
					&#039;font-variant-numeric:tabular-nums;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy-row{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;gap:.3rem;&#039; +&lt;br /&gt;
					&#039;margin-top:.38rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-copy{&#039; +&lt;br /&gt;
					&#039;min-height:1.8rem!important;&#039; +&lt;br /&gt;
					&#039;padding:.15rem .45rem!important;&#039; +&lt;br /&gt;
					&#039;font-size:.72rem!important;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;.eql-iconfinder-note{&#039; +&lt;br /&gt;
					&#039;margin-top:.7rem;&#039; +&lt;br /&gt;
					&#039;color:#7f8b9b;&#039; +&lt;br /&gt;
					&#039;font-size:.78rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:850px){&#039; +&lt;br /&gt;
					&#039;.eql-iconfinder-shell{grid-template-columns:1fr;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function openDb() {&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var request;&lt;br /&gt;
&lt;br /&gt;
			if ( !window.indexedDB ) {&lt;br /&gt;
				reject( new Error( &#039;IndexedDB unavailable.&#039; ) );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			request = indexedDB.open( DB_NAME, DB_VERSION );&lt;br /&gt;
&lt;br /&gt;
			request.onupgradeneeded = function () {&lt;br /&gt;
				var db = request.result;&lt;br /&gt;
&lt;br /&gt;
				if ( !db.objectStoreNames.contains( STORE_NAME ) ) {&lt;br /&gt;
					db.createObjectStore( STORE_NAME, {&lt;br /&gt;
						keyPath: &#039;key&#039;&lt;br /&gt;
					} );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onsuccess = function () {&lt;br /&gt;
				resolve( request.result );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			request.onerror = function () {&lt;br /&gt;
				reject( request.error || new Error( &#039;IndexedDB open failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbGet( key ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readonly&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var request = store.get( key );&lt;br /&gt;
&lt;br /&gt;
				request.onsuccess = function () {&lt;br /&gt;
					resolve( request.result || null );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				request.onerror = function () {&lt;br /&gt;
					reject( request.error || new Error( &#039;IndexedDB read failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function idbPutAndPrune( value ) {&lt;br /&gt;
		return openDb().then( function ( db ) {&lt;br /&gt;
			return new Promise( function ( resolve, reject ) {&lt;br /&gt;
				var tx = db.transaction( STORE_NAME, &#039;readwrite&#039; );&lt;br /&gt;
				var store = tx.objectStore( STORE_NAME );&lt;br /&gt;
				var cursorRequest;&lt;br /&gt;
&lt;br /&gt;
				store.put( value );&lt;br /&gt;
&lt;br /&gt;
				cursorRequest = store.openCursor();&lt;br /&gt;
&lt;br /&gt;
				cursorRequest.onsuccess = function () {&lt;br /&gt;
					var cursor = cursorRequest.result;&lt;br /&gt;
&lt;br /&gt;
					if ( !cursor ) {&lt;br /&gt;
						return;&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					if ( cursor.key !== value.key ) {&lt;br /&gt;
						cursor.delete();&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					cursor.continue();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.oncomplete = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					resolve();&lt;br /&gt;
				};&lt;br /&gt;
&lt;br /&gt;
				tx.onerror = function () {&lt;br /&gt;
					db.close();&lt;br /&gt;
					reject( tx.error || new Error( &#039;IndexedDB write failed.&#039; ) );&lt;br /&gt;
				};&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeFileTitle( title ) {&lt;br /&gt;
		title = String( title || &#039;&#039; ).trim();&lt;br /&gt;
&lt;br /&gt;
		if ( !title ) {&lt;br /&gt;
			return &#039;&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return /^File:/i.test( title )&lt;br /&gt;
			? title&lt;br /&gt;
			: &#039;File:&#039; + title;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchCatalog( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: ICON_LIST_PAGE,&lt;br /&gt;
			prop: &#039;images|revid&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var parse = data &amp;amp;&amp;amp; data.parse ? data.parse : {};&lt;br /&gt;
			var titles = Array.isArray( parse.images ) ? parse.images : [];&lt;br /&gt;
&lt;br /&gt;
			titles = titles&lt;br /&gt;
				.map( normalizeFileTitle )&lt;br /&gt;
				.filter( Boolean )&lt;br /&gt;
				.filter( function ( title, index, list ) {&lt;br /&gt;
					return list.indexOf( title ) === index;&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				revid: Number( parse.revid ) || 0,&lt;br /&gt;
				titles: titles&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function chunk( list, size ) {&lt;br /&gt;
		var out = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; list.length; i += size ) {&lt;br /&gt;
			out.push( list.slice( i, i + size ) );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchImageInfo( api, titles, setStatus ) {&lt;br /&gt;
		var batches = chunk( titles, IMAGEINFO_BATCH );&lt;br /&gt;
		var records = [];&lt;br /&gt;
		var chain = Promise.resolve();&lt;br /&gt;
&lt;br /&gt;
		batches.forEach( function ( batch, batchIndex ) {&lt;br /&gt;
			chain = chain.then( function () {&lt;br /&gt;
				if ( setStatus ) {&lt;br /&gt;
					setStatus(&lt;br /&gt;
						&#039;Reading uploaded icon files… &#039; +&lt;br /&gt;
						Math.min( batchIndex * IMAGEINFO_BATCH, titles.length ) +&lt;br /&gt;
						&#039; / &#039; + titles.length&lt;br /&gt;
					);&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return api.get( {&lt;br /&gt;
					action: &#039;query&#039;,&lt;br /&gt;
					prop: &#039;imageinfo&#039;,&lt;br /&gt;
					titles: batch.join( &#039;|&#039; ),&lt;br /&gt;
					iiprop: &#039;url|size|sha1&#039;,&lt;br /&gt;
					formatversion: 2&lt;br /&gt;
				} ).then( function ( data ) {&lt;br /&gt;
					var pages =&lt;br /&gt;
						data &amp;amp;&amp;amp;&lt;br /&gt;
						data.query &amp;amp;&amp;amp;&lt;br /&gt;
						Array.isArray( data.query.pages )&lt;br /&gt;
							? data.query.pages&lt;br /&gt;
							: [];&lt;br /&gt;
&lt;br /&gt;
					pages.forEach( function ( page ) {&lt;br /&gt;
						var info =&lt;br /&gt;
							page &amp;amp;&amp;amp;&lt;br /&gt;
							Array.isArray( page.imageinfo ) &amp;amp;&amp;amp;&lt;br /&gt;
							page.imageinfo.length&lt;br /&gt;
								? page.imageinfo[ 0 ]&lt;br /&gt;
								: null;&lt;br /&gt;
&lt;br /&gt;
						if ( !info || !info.url ) {&lt;br /&gt;
							return;&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						records.push( {&lt;br /&gt;
							title: normalizeFileTitle( page.title ),&lt;br /&gt;
							url: String( info.url ),&lt;br /&gt;
							sha1: String( info.sha1 || &#039;&#039; ),&lt;br /&gt;
							width: Number( info.width ) || 0,&lt;br /&gt;
							height: Number( info.height ) || 0&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return chain.then( function () {&lt;br /&gt;
			return records;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function blobToDrawable( blob ) {&lt;br /&gt;
		if ( window.createImageBitmap ) {&lt;br /&gt;
			return createImageBitmap( blob );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var url = URL.createObjectURL( blob );&lt;br /&gt;
			var img = new Image();&lt;br /&gt;
&lt;br /&gt;
			img.onload = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				resolve( img );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.onerror = function () {&lt;br /&gt;
				URL.revokeObjectURL( url );&lt;br /&gt;
				reject( new Error( &#039;Image decode failed.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			img.src = url;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchDrawable( url ) {&lt;br /&gt;
		return fetch( url, {&lt;br /&gt;
			credentials: &#039;same-origin&#039;,&lt;br /&gt;
			cache: &#039;force-cache&#039;&lt;br /&gt;
		} )&lt;br /&gt;
			.then( function ( response ) {&lt;br /&gt;
				if ( !response.ok ) {&lt;br /&gt;
					throw new Error( &#039;HTTP &#039; + response.status );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				return response.blob();&lt;br /&gt;
			} )&lt;br /&gt;
			.then( blobToDrawable );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function closeDrawable( drawable ) {&lt;br /&gt;
		if ( drawable &amp;amp;&amp;amp; typeof drawable.close === &#039;function&#039; ) {&lt;br /&gt;
			drawable.close();&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function drawScaled( drawable, width, height, cropFraction ) {&lt;br /&gt;
		var canvas = document.createElement( &#039;canvas&#039; );&lt;br /&gt;
		var ctx = canvas.getContext( &#039;2d&#039;, {&lt;br /&gt;
			willReadFrequently: true&lt;br /&gt;
		} );&lt;br /&gt;
		var sourceWidth = Number( drawable.width || drawable.naturalWidth ) || 1;&lt;br /&gt;
		var sourceHeight = Number( drawable.height || drawable.naturalHeight ) || 1;&lt;br /&gt;
		var cropX = Math.round( sourceWidth * cropFraction );&lt;br /&gt;
		var cropY = Math.round( sourceHeight * cropFraction );&lt;br /&gt;
		var cropW = Math.max( 1, sourceWidth - cropX * 2 );&lt;br /&gt;
		var cropH = Math.max( 1, sourceHeight - cropY * 2 );&lt;br /&gt;
&lt;br /&gt;
		canvas.width = width;&lt;br /&gt;
		canvas.height = height;&lt;br /&gt;
&lt;br /&gt;
		ctx.imageSmoothingEnabled = true;&lt;br /&gt;
&lt;br /&gt;
		if ( &#039;imageSmoothingQuality&#039; in ctx ) {&lt;br /&gt;
			ctx.imageSmoothingQuality = &#039;high&#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Transparent pixels in uploaded PNG icons normally render over a dark&lt;br /&gt;
		 * game/UI background in screenshots. Normalizing them to black keeps&lt;br /&gt;
		 * the comparison stable.&lt;br /&gt;
		 */&lt;br /&gt;
		ctx.fillStyle = &#039;#000&#039;;&lt;br /&gt;
		ctx.fillRect( 0, 0, width, height );&lt;br /&gt;
&lt;br /&gt;
		ctx.drawImage(&lt;br /&gt;
			drawable,&lt;br /&gt;
			cropX,&lt;br /&gt;
			cropY,&lt;br /&gt;
			cropW,&lt;br /&gt;
			cropH,&lt;br /&gt;
			0,&lt;br /&gt;
			0,&lt;br /&gt;
			width,&lt;br /&gt;
			height&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		return ctx.getImageData( 0, 0, width, height );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function luminance( r, g, b ) {&lt;br /&gt;
		return 0.2126 * r + 0.7152 * g + 0.0722 * b;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeDHash( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled( drawable, 9, 8, cropFraction );&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var hi = 0;&lt;br /&gt;
		var lo = 0;&lt;br /&gt;
		var bit = 0;&lt;br /&gt;
		var y;&lt;br /&gt;
		var x;&lt;br /&gt;
		var left;&lt;br /&gt;
		var right;&lt;br /&gt;
		var li;&lt;br /&gt;
		var ri;&lt;br /&gt;
&lt;br /&gt;
		for ( y = 0; y &amp;lt; 8; y++ ) {&lt;br /&gt;
			for ( x = 0; x &amp;lt; 8; x++ ) {&lt;br /&gt;
				li = ( y * 9 + x ) * 4;&lt;br /&gt;
				ri = ( y * 9 + x + 1 ) * 4;&lt;br /&gt;
&lt;br /&gt;
				left = luminance(&lt;br /&gt;
					data[ li ],&lt;br /&gt;
					data[ li + 1 ],&lt;br /&gt;
					data[ li + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				right = luminance(&lt;br /&gt;
					data[ ri ],&lt;br /&gt;
					data[ ri + 1 ],&lt;br /&gt;
					data[ ri + 2 ]&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				if ( left &amp;gt; right ) {&lt;br /&gt;
					if ( bit &amp;lt; 32 ) {&lt;br /&gt;
						lo = ( lo | ( 1 &amp;lt;&amp;lt; bit ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					} else {&lt;br /&gt;
						hi = ( hi | ( 1 &amp;lt;&amp;lt; ( bit - 32 ) ) ) &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
					}&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				bit++;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hi: hi &amp;gt;&amp;gt;&amp;gt; 0,&lt;br /&gt;
			lo: lo &amp;gt;&amp;gt;&amp;gt; 0&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function computeColorGrid( drawable, cropFraction ) {&lt;br /&gt;
		var imageData = drawScaled(&lt;br /&gt;
			drawable,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			COLOR_GRID,&lt;br /&gt;
			cropFraction&lt;br /&gt;
		);&lt;br /&gt;
		var data = imageData.data;&lt;br /&gt;
		var out = new Uint8Array( COLOR_GRID * COLOR_GRID * 3 );&lt;br /&gt;
		var source = 0;&lt;br /&gt;
		var target = 0;&lt;br /&gt;
&lt;br /&gt;
		while ( source &amp;lt; data.length ) {&lt;br /&gt;
			out[ target++ ] = data[ source ];&lt;br /&gt;
			out[ target++ ] = data[ source + 1 ];&lt;br /&gt;
			out[ target++ ] = data[ source + 2 ];&lt;br /&gt;
			source += 4;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDrawable( drawable, cropFraction ) {&lt;br /&gt;
		var hash = computeDHash( drawable, cropFraction );&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			hashHi: hash.hi,&lt;br /&gt;
			hashLo: hash.lo,&lt;br /&gt;
			rgb: computeColorGrid( drawable, cropFraction )&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function popcount32( value ) {&lt;br /&gt;
		value = value &amp;gt;&amp;gt;&amp;gt; 0;&lt;br /&gt;
		value = value - ( ( value &amp;gt;&amp;gt;&amp;gt; 1 ) &amp;amp; 0x55555555 );&lt;br /&gt;
		value = ( value &amp;amp; 0x33333333 ) + ( ( value &amp;gt;&amp;gt;&amp;gt; 2 ) &amp;amp; 0x33333333 );&lt;br /&gt;
		return (&lt;br /&gt;
			(&lt;br /&gt;
				(&lt;br /&gt;
					value + ( value &amp;gt;&amp;gt;&amp;gt; 4 )&lt;br /&gt;
				) &amp;amp; 0x0F0F0F0F&lt;br /&gt;
			) * 0x01010101&lt;br /&gt;
		) &amp;gt;&amp;gt;&amp;gt; 24;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintDistance( a, b ) {&lt;br /&gt;
		var hashBits =&lt;br /&gt;
			popcount32( ( a.hashHi ^ b.hashHi ) &amp;gt;&amp;gt;&amp;gt; 0 ) +&lt;br /&gt;
			popcount32( ( a.hashLo ^ b.hashLo ) &amp;gt;&amp;gt;&amp;gt; 0 );&lt;br /&gt;
&lt;br /&gt;
		var hashDistance = hashBits / 64;&lt;br /&gt;
		var length = Math.min( a.rgb.length, b.rgb.length );&lt;br /&gt;
		var sum = 0;&lt;br /&gt;
		var i;&lt;br /&gt;
		var delta;&lt;br /&gt;
		var colorDistance;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; length; i++ ) {&lt;br /&gt;
			delta = a.rgb[ i ] - b.rgb[ i ];&lt;br /&gt;
			sum += delta * delta;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		colorDistance = Math.sqrt(&lt;br /&gt;
			sum / Math.max( 1, length )&lt;br /&gt;
		) / 255;&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * dHash is resilient to light rescaling/compression.&lt;br /&gt;
		 * The small RGB grid breaks ties between similarly-shaped icons.&lt;br /&gt;
		 */&lt;br /&gt;
		return 0.58 * hashDistance + 0.42 * colorDistance;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function mapLimit( list, limit, worker ) {&lt;br /&gt;
		var nextIndex = 0;&lt;br /&gt;
		var workers = [];&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		function runWorker() {&lt;br /&gt;
			var index;&lt;br /&gt;
&lt;br /&gt;
			if ( nextIndex &amp;gt;= list.length ) {&lt;br /&gt;
				return Promise.resolve();&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			index = nextIndex++;&lt;br /&gt;
			return Promise.resolve( worker( list[ index ], index ) )&lt;br /&gt;
				.then( runWorker );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; Math.min( limit, list.length ); i++ ) {&lt;br /&gt;
			workers.push( runWorker() );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return Promise.all( workers );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildFingerprintIndex( records, setStatus ) {&lt;br /&gt;
		var complete = 0;&lt;br /&gt;
		var successful = [];&lt;br /&gt;
		var grouped = Object.create( null );&lt;br /&gt;
		var groups = [];&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * If several icon filenames are byte-identical, imageinfo gives them&lt;br /&gt;
		 * the same SHA-1. Fingerprint that image only once and fan the result&lt;br /&gt;
		 * back out to every filename. This can substantially reduce the first&lt;br /&gt;
		 * build&#039;s static-file traffic without changing the visible results.&lt;br /&gt;
		 */&lt;br /&gt;
		records.forEach( function ( record ) {&lt;br /&gt;
			var key = record.sha1 || record.url;&lt;br /&gt;
&lt;br /&gt;
			if ( !grouped[ key ] ) {&lt;br /&gt;
				grouped[ key ] = {&lt;br /&gt;
					key: key,&lt;br /&gt;
					representative: record,&lt;br /&gt;
					records: []&lt;br /&gt;
				};&lt;br /&gt;
				groups.push( grouped[ key ] );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			grouped[ key ].records.push( record );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return mapLimit(&lt;br /&gt;
			groups,&lt;br /&gt;
			DOWNLOAD_CONCURRENCY,&lt;br /&gt;
			function ( group ) {&lt;br /&gt;
				return fetchDrawable( group.representative.url )&lt;br /&gt;
					.then( function ( drawable ) {&lt;br /&gt;
						var fp = fingerprintDrawable( drawable, 0 );&lt;br /&gt;
&lt;br /&gt;
						closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
						group.records.forEach( function ( record ) {&lt;br /&gt;
							successful.push( {&lt;br /&gt;
								title: record.title,&lt;br /&gt;
								url: record.url,&lt;br /&gt;
								sha1: record.sha1,&lt;br /&gt;
								width: record.width,&lt;br /&gt;
								height: record.height,&lt;br /&gt;
								hashHi: fp.hashHi,&lt;br /&gt;
								hashLo: fp.hashLo,&lt;br /&gt;
								rgb: fp.rgb&lt;br /&gt;
							} );&lt;br /&gt;
						} );&lt;br /&gt;
					} )&lt;br /&gt;
					.catch( function () {&lt;br /&gt;
						/*&lt;br /&gt;
						 * A missing/corrupt/non-raster file should not prevent&lt;br /&gt;
						 * the rest of the icon index from becoming usable.&lt;br /&gt;
						 */&lt;br /&gt;
					} )&lt;br /&gt;
					.then( function () {&lt;br /&gt;
						complete++;&lt;br /&gt;
&lt;br /&gt;
						if (&lt;br /&gt;
							setStatus &amp;amp;&amp;amp;&lt;br /&gt;
							(&lt;br /&gt;
								complete === groups.length ||&lt;br /&gt;
								complete % 25 === 0&lt;br /&gt;
							)&lt;br /&gt;
						) {&lt;br /&gt;
							setStatus(&lt;br /&gt;
								&#039;Building pixel index… &#039; +&lt;br /&gt;
									complete + &#039; / &#039; + groups.length +&lt;br /&gt;
									&#039; unique images (&#039; +&lt;br /&gt;
									records.length + &#039; filenames)&#039;&lt;br /&gt;
							);&lt;br /&gt;
						}&lt;br /&gt;
					} );&lt;br /&gt;
			}&lt;br /&gt;
		).then( function () {&lt;br /&gt;
			return successful;&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cacheKey( revid ) {&lt;br /&gt;
		return [&lt;br /&gt;
			&#039;icon-list&#039;,&lt;br /&gt;
			ALGO_VERSION,&lt;br /&gt;
			revid&lt;br /&gt;
		].join( &#039;:&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getOrBuildIndex( api, setStatus ) {&lt;br /&gt;
		if ( memoryIndex ) {&lt;br /&gt;
			return Promise.resolve( memoryIndex );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		setStatus( &#039;Reading Icon List…&#039; );&lt;br /&gt;
&lt;br /&gt;
		return fetchCatalog( api ).then( function ( catalog ) {&lt;br /&gt;
			var key = cacheKey( catalog.revid );&lt;br /&gt;
&lt;br /&gt;
			return idbGet( key )&lt;br /&gt;
				.catch( function () {&lt;br /&gt;
					return null;&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function ( cached ) {&lt;br /&gt;
					if (&lt;br /&gt;
						cached &amp;amp;&amp;amp;&lt;br /&gt;
						Array.isArray( cached.records ) &amp;amp;&amp;amp;&lt;br /&gt;
						cached.records.length&lt;br /&gt;
					) {&lt;br /&gt;
						memoryIndex = cached.records;&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Icon index ready: &#039; +&lt;br /&gt;
								memoryIndex.length +&lt;br /&gt;
								&#039; uploaded icons (cached locally).&#039;&lt;br /&gt;
						);&lt;br /&gt;
						return memoryIndex;&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					return fetchImageInfo(&lt;br /&gt;
						api,&lt;br /&gt;
						catalog.titles,&lt;br /&gt;
						setStatus&lt;br /&gt;
					).then( function ( imageRecords ) {&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Found &#039; + imageRecords.length +&lt;br /&gt;
								&#039; uploaded icons. Building pixel index…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						return buildFingerprintIndex(&lt;br /&gt;
							imageRecords,&lt;br /&gt;
							setStatus&lt;br /&gt;
						);&lt;br /&gt;
					} ).then( function ( records ) {&lt;br /&gt;
						memoryIndex = records;&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Icon index ready: &#039; +&lt;br /&gt;
								records.length +&lt;br /&gt;
								&#039; uploaded icons.&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						return idbPutAndPrune( {&lt;br /&gt;
							key: key,&lt;br /&gt;
							revid: catalog.revid,&lt;br /&gt;
							algorithm: ALGO_VERSION,&lt;br /&gt;
							builtAt: Date.now(),&lt;br /&gt;
							records: records&lt;br /&gt;
						} )&lt;br /&gt;
							.catch( function () {&lt;br /&gt;
								/*&lt;br /&gt;
								 * Matching still works for this session even if&lt;br /&gt;
								 * the browser blocks persistent storage.&lt;br /&gt;
								 */&lt;br /&gt;
							} )&lt;br /&gt;
							.then( function () {&lt;br /&gt;
								return records;&lt;br /&gt;
							} );&lt;br /&gt;
					} );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fingerprintPastedImage( drawable ) {&lt;br /&gt;
		/*&lt;br /&gt;
		 * Small centered crop variants make the matcher forgiving of a&lt;br /&gt;
		 * 1-2 pixel screen-capture border while still preferring an exact crop.&lt;br /&gt;
		 */&lt;br /&gt;
		return [&lt;br /&gt;
			fingerprintDrawable( drawable, 0 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.035 ),&lt;br /&gt;
			fingerprintDrawable( drawable, 0.07 )&lt;br /&gt;
		];&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function rankMatches( targetFingerprints, records ) {&lt;br /&gt;
		var ranked = records.map( function ( record ) {&lt;br /&gt;
			var candidate = {&lt;br /&gt;
				hashHi: record.hashHi,&lt;br /&gt;
				hashLo: record.hashLo,&lt;br /&gt;
				rgb: record.rgb&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			var distance = Infinity;&lt;br /&gt;
&lt;br /&gt;
			targetFingerprints.forEach( function ( target ) {&lt;br /&gt;
				distance = Math.min(&lt;br /&gt;
					distance,&lt;br /&gt;
					fingerprintDistance( target, candidate )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			return {&lt;br /&gt;
				record: record,&lt;br /&gt;
				distance: distance,&lt;br /&gt;
				similarity: Math.max(&lt;br /&gt;
					0,&lt;br /&gt;
					Math.min( 100, ( 1 - distance ) * 100 )&lt;br /&gt;
				)&lt;br /&gt;
			};&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		ranked.sort( function ( a, b ) {&lt;br /&gt;
			return a.distance - b.distance;&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return ranked.slice( 0, RESULT_COUNT );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function cleanDisplayTitle( title ) {&lt;br /&gt;
		return String( title || &#039;&#039; ).replace( /^File:/i, &#039;&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function iconMetadata( title ) {&lt;br /&gt;
		var clean = cleanDisplayTitle( title );&lt;br /&gt;
		var itemMatch = clean.match( /^Item\s+(\d+)\.png$/i );&lt;br /&gt;
		var spellMatch = clean.match( /^Spellicon_([^.]*)\.png$/i );&lt;br /&gt;
		var id = &#039;&#039;;&lt;br /&gt;
		var kind = &#039;Icon file&#039;;&lt;br /&gt;
		var parameter = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		if ( itemMatch ) {&lt;br /&gt;
			id = itemMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Item icon&#039;;&lt;br /&gt;
			parameter = &#039;|lucy_img_ID = &#039; + id;&lt;br /&gt;
		} else if ( spellMatch ) {&lt;br /&gt;
			id = spellMatch[ 1 ];&lt;br /&gt;
			kind = &#039;Spell icon&#039;;&lt;br /&gt;
			parameter = &#039;|spellicon = &#039; + id;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return {&lt;br /&gt;
			kind: kind,&lt;br /&gt;
			id: id,&lt;br /&gt;
			filename: clean,&lt;br /&gt;
			parameter: parameter,&lt;br /&gt;
			rawWiki: &#039;[[File:&#039; + clean + &#039;]]&#039;,&lt;br /&gt;
			staticWiki: &#039;[[File:&#039; + clean + &#039;|32x32px]]&#039;,&lt;br /&gt;
			scaledWiki: &#039;[[File:&#039; + clean + &#039;|frameless|upright=1.5]]&#039;&lt;br /&gt;
		};&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function copyText( text, button ) {&lt;br /&gt;
		var original = button.textContent;&lt;br /&gt;
&lt;br /&gt;
		function done() {&lt;br /&gt;
			button.textContent = &#039;Copied&#039;;&lt;br /&gt;
			window.setTimeout( function () {&lt;br /&gt;
				button.textContent = original;&lt;br /&gt;
			}, 1000 );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if (&lt;br /&gt;
			navigator.clipboard &amp;amp;&amp;amp;&lt;br /&gt;
			typeof navigator.clipboard.writeText === &#039;function&#039;&lt;br /&gt;
		) {&lt;br /&gt;
			navigator.clipboard.writeText( text )&lt;br /&gt;
				.then( done )&lt;br /&gt;
				.catch( function () {&lt;br /&gt;
					window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
				} );&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.prompt( &#039;Copy:&#039;, text );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeCopyButton( label, value ) {&lt;br /&gt;
		var button = document.createElement( &#039;button&#039; );&lt;br /&gt;
&lt;br /&gt;
		button.className = &#039;eql-iconfinder-copy&#039;;&lt;br /&gt;
		button.type = &#039;button&#039;;&lt;br /&gt;
		button.textContent = label;&lt;br /&gt;
&lt;br /&gt;
		button.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			copyText( value, button );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return button;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function renderMatches( container, matches ) {&lt;br /&gt;
		container.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		var title = document.createElement( &#039;div&#039; );&lt;br /&gt;
		title.className = &#039;eql-iconfinder-results-title&#039;;&lt;br /&gt;
		title.textContent = &#039;Possible Matches&#039;;&lt;br /&gt;
		container.appendChild( title );&lt;br /&gt;
&lt;br /&gt;
		var grid = document.createElement( &#039;div&#039; );&lt;br /&gt;
		grid.className = &#039;eql-iconfinder-results-grid&#039;;&lt;br /&gt;
		container.appendChild( grid );&lt;br /&gt;
&lt;br /&gt;
		matches.forEach( function ( match ) {&lt;br /&gt;
			var record = match.record;&lt;br /&gt;
			var meta = iconMetadata( record.title );&lt;br /&gt;
			var card = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var img = document.createElement( &#039;img&#039; );&lt;br /&gt;
			var textWrap = document.createElement( &#039;div&#039; );&lt;br /&gt;
			var link = document.createElement( &#039;a&#039; );&lt;br /&gt;
			var score = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var iconId = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var metadata = document.createElement( &#039;span&#039; );&lt;br /&gt;
			var copyRow = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
			card.className = &#039;eql-iconfinder-result&#039;;&lt;br /&gt;
&lt;br /&gt;
			img.src = record.url;&lt;br /&gt;
			img.alt = meta.filename;&lt;br /&gt;
			img.loading = &#039;lazy&#039;;&lt;br /&gt;
&lt;br /&gt;
			link.className = &#039;eql-iconfinder-result-name&#039;;&lt;br /&gt;
			link.href = mw.util.getUrl( record.title );&lt;br /&gt;
			link.target = &#039;_blank&#039;;&lt;br /&gt;
			link.rel = &#039;noopener&#039;;&lt;br /&gt;
			link.textContent = meta.filename;&lt;br /&gt;
&lt;br /&gt;
			score.className = &#039;eql-iconfinder-result-score&#039;;&lt;br /&gt;
			score.textContent =&lt;br /&gt;
				match.similarity.toFixed( 1 ) + &#039;% similarity&#039;;&lt;br /&gt;
&lt;br /&gt;
			iconId.className = &#039;eql-iconfinder-result-id&#039;;&lt;br /&gt;
			iconId.textContent = meta.id&lt;br /&gt;
				? &#039;Icon &#039; + meta.id&lt;br /&gt;
				: meta.filename;&lt;br /&gt;
&lt;br /&gt;
			metadata.className = &#039;eql-iconfinder-result-meta&#039;;&lt;br /&gt;
			metadata.textContent = meta.kind;&lt;br /&gt;
&lt;br /&gt;
			copyRow.className = &#039;eql-iconfinder-copy-row&#039;;&lt;br /&gt;
&lt;br /&gt;
			if ( meta.id ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy ID&#039;, meta.id )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			if ( meta.parameter ) {&lt;br /&gt;
				copyRow.appendChild(&lt;br /&gt;
					makeCopyButton( &#039;Copy Parameter&#039;, meta.parameter )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Raw Wikicode&#039;, meta.rawWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;32×32&#039;, meta.staticWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			copyRow.appendChild(&lt;br /&gt;
				makeCopyButton( &#039;Scaled&#039;, meta.scaledWiki )&lt;br /&gt;
			);&lt;br /&gt;
&lt;br /&gt;
			textWrap.appendChild( link );&lt;br /&gt;
			textWrap.appendChild( iconId );&lt;br /&gt;
			textWrap.appendChild( score );&lt;br /&gt;
			textWrap.appendChild( metadata );&lt;br /&gt;
			textWrap.appendChild( copyRow );&lt;br /&gt;
&lt;br /&gt;
			card.appendChild( img );&lt;br /&gt;
			card.appendChild( textWrap );&lt;br /&gt;
			grid.appendChild( card );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		var note = document.createElement( &#039;div&#039; );&lt;br /&gt;
		note.className = &#039;eql-iconfinder-note&#039;;&lt;br /&gt;
		note.textContent =&lt;br /&gt;
			&#039;The tool only identifies likely matches and copies values/wikicode to the clipboard; it never changes the editor. &#039; +&lt;br /&gt;
			&#039;For the best result, capture only the square icon with as little surrounding UI as possible.&#039;;&lt;br /&gt;
		container.appendChild( note );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function imageFromClipboardEvent( event ) {&lt;br /&gt;
		var clipboard = event.clipboardData;&lt;br /&gt;
		var items;&lt;br /&gt;
		var i;&lt;br /&gt;
&lt;br /&gt;
		if ( !clipboard || !clipboard.items ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		items = clipboard.items;&lt;br /&gt;
&lt;br /&gt;
		for ( i = 0; i &amp;lt; items.length; i++ ) {&lt;br /&gt;
			if (&lt;br /&gt;
				items[ i ].kind === &#039;file&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				/^image\//i.test( items[ i ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				return items[ i ].getAsFile();&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return null;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setPreview( img, blob ) {&lt;br /&gt;
		if ( currentPreviewUrl ) {&lt;br /&gt;
			URL.revokeObjectURL( currentPreviewUrl );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		currentPreviewUrl = URL.createObjectURL( blob );&lt;br /&gt;
		img.src = currentPreviewUrl;&lt;br /&gt;
		img.classList.add( &#039;is-visible&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function processBlob( blob, preview, status, results, api ) {&lt;br /&gt;
		var setStatus = function ( text ) {&lt;br /&gt;
			status.textContent = text;&lt;br /&gt;
		};&lt;br /&gt;
&lt;br /&gt;
		setPreview( preview, blob );&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Analyzing pasted icon…&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		return blobToDrawable( blob )&lt;br /&gt;
			.then( function ( drawable ) {&lt;br /&gt;
				var targets = fingerprintPastedImage( drawable );&lt;br /&gt;
				closeDrawable( drawable );&lt;br /&gt;
&lt;br /&gt;
				return getOrBuildIndex( api, setStatus )&lt;br /&gt;
					.then( function ( index ) {&lt;br /&gt;
						var matches;&lt;br /&gt;
&lt;br /&gt;
						if ( !index.length ) {&lt;br /&gt;
							throw new Error( &#039;No uploaded icons were found.&#039; );&lt;br /&gt;
						}&lt;br /&gt;
&lt;br /&gt;
						setStatus(&lt;br /&gt;
							&#039;Comparing against &#039; +&lt;br /&gt;
								index.length +&lt;br /&gt;
								&#039; uploaded icons…&#039;&lt;br /&gt;
						);&lt;br /&gt;
&lt;br /&gt;
						/*&lt;br /&gt;
						 * Yield once so the status message can paint before&lt;br /&gt;
						 * ranking several thousand in-memory fingerprints.&lt;br /&gt;
						 */&lt;br /&gt;
						return new Promise( function ( resolve ) {&lt;br /&gt;
							window.setTimeout( function () {&lt;br /&gt;
								matches = rankMatches( targets, index );&lt;br /&gt;
								resolve( matches );&lt;br /&gt;
							}, 0 );&lt;br /&gt;
						} );&lt;br /&gt;
					} );&lt;br /&gt;
			} )&lt;br /&gt;
			.then( function ( matches ) {&lt;br /&gt;
				renderMatches( results, matches );&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Done. Showing the &#039; +&lt;br /&gt;
						matches.length +&lt;br /&gt;
						&#039; closest matches.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				results.innerHTML =&lt;br /&gt;
					&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
						&#039;The icon could not be matched.&#039; +&lt;br /&gt;
					&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
				setStatus(&lt;br /&gt;
					&#039;Icon Finder error: &#039; +&lt;br /&gt;
						( error &amp;amp;&amp;amp; error.message ? error.message : &#039;Unknown error&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildUi( host ) {&lt;br /&gt;
		var api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
		host.innerHTML = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
		var shell = document.createElement( &#039;div&#039; );&lt;br /&gt;
		shell.className = &#039;eql-iconfinder-shell&#039;;&lt;br /&gt;
&lt;br /&gt;
		var left = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var paste = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var instruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var subinstruction = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var preview = document.createElement( &#039;img&#039; );&lt;br /&gt;
		var status = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		paste.className = &#039;eql-iconfinder-paste&#039;;&lt;br /&gt;
		paste.tabIndex = 0;&lt;br /&gt;
		paste.setAttribute( &#039;role&#039;, &#039;button&#039; );&lt;br /&gt;
		paste.setAttribute(&lt;br /&gt;
			&#039;aria-label&#039;,&lt;br /&gt;
			&#039;Paste a screen capture of an EverQuest icon&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		instruction.className = &#039;eql-iconfinder-instruction&#039;;&lt;br /&gt;
		instruction.textContent =&lt;br /&gt;
			&#039;Screen capture and paste JUST the Icon&#039;;&lt;br /&gt;
&lt;br /&gt;
		subinstruction.className = &#039;eql-iconfinder-subinstruction&#039;;&lt;br /&gt;
		subinstruction.textContent =&lt;br /&gt;
			&#039;Click this box, then press Ctrl+V. You can also drag an image here.&#039;;&lt;br /&gt;
&lt;br /&gt;
		preview.className = &#039;eql-iconfinder-preview&#039;;&lt;br /&gt;
		preview.alt = &#039;Pasted icon preview&#039;;&lt;br /&gt;
&lt;br /&gt;
		status.className = &#039;eql-iconfinder-status&#039;;&lt;br /&gt;
		status.setAttribute( &#039;aria-live&#039;, &#039;polite&#039; );&lt;br /&gt;
		status.textContent =&lt;br /&gt;
			&#039;Nothing is loaded from Icon List until you paste an image.&#039;;&lt;br /&gt;
&lt;br /&gt;
		paste.appendChild( instruction );&lt;br /&gt;
		paste.appendChild( subinstruction );&lt;br /&gt;
		paste.appendChild( preview );&lt;br /&gt;
&lt;br /&gt;
		left.appendChild( paste );&lt;br /&gt;
		left.appendChild( status );&lt;br /&gt;
&lt;br /&gt;
		var right = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var resultsTitle = document.createElement( &#039;div&#039; );&lt;br /&gt;
		var results = document.createElement( &#039;div&#039; );&lt;br /&gt;
&lt;br /&gt;
		resultsTitle.className = &#039;eql-iconfinder-results-title&#039;;&lt;br /&gt;
		resultsTitle.textContent = &#039;Possible Matches&#039;;&lt;br /&gt;
&lt;br /&gt;
		results.innerHTML =&lt;br /&gt;
			&#039;&amp;lt;div class=&amp;quot;eql-iconfinder-results-empty&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
				&#039;Paste an icon to search the uploaded images on Icon List.&#039; +&lt;br /&gt;
			&#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
		right.appendChild( resultsTitle );&lt;br /&gt;
		right.appendChild( results );&lt;br /&gt;
&lt;br /&gt;
		shell.appendChild( left );&lt;br /&gt;
		shell.appendChild( right );&lt;br /&gt;
		host.appendChild( shell );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;click&#039;, function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;paste&#039;, function ( event ) {&lt;br /&gt;
			var file = imageFromClipboardEvent( event );&lt;br /&gt;
&lt;br /&gt;
			if ( !file ) {&lt;br /&gt;
				status.textContent =&lt;br /&gt;
					&#039;Clipboard does not contain an image. Screen capture the icon, then paste again.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				file,&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;dragover&#039;, function ( event ) {&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		paste.addEventListener( &#039;drop&#039;, function ( event ) {&lt;br /&gt;
			var files;&lt;br /&gt;
&lt;br /&gt;
			event.preventDefault();&lt;br /&gt;
			files = event.dataTransfer &amp;amp;&amp;amp; event.dataTransfer.files;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				!files ||&lt;br /&gt;
				!files.length ||&lt;br /&gt;
				!/^image\//i.test( files[ 0 ].type )&lt;br /&gt;
			) {&lt;br /&gt;
				status.textContent = &#039;Drop an image file here.&#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			processBlob(&lt;br /&gt;
				files[ 0 ],&lt;br /&gt;
				preview,&lt;br /&gt;
				status,&lt;br /&gt;
				results,&lt;br /&gt;
				api&lt;br /&gt;
			);&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		/*&lt;br /&gt;
		 * Focus the paste target immediately when the panel first opens so&lt;br /&gt;
		 * the normal workflow is: click Icon Finder -&amp;gt; Ctrl+V.&lt;br /&gt;
		 */&lt;br /&gt;
		window.setTimeout( function () {&lt;br /&gt;
			paste.focus();&lt;br /&gt;
		}, 0 );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	window.EQLIconFinder = {&lt;br /&gt;
		init: function ( host ) {&lt;br /&gt;
			if ( !host ) {&lt;br /&gt;
				return Promise.reject(&lt;br /&gt;
					new Error( &#039;Icon Finder host element is missing.&#039; )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			addStyles();&lt;br /&gt;
&lt;br /&gt;
			if ( initializedHost !== host ) {&lt;br /&gt;
				buildUi( host );&lt;br /&gt;
				initializedHost = host;&lt;br /&gt;
			} else {&lt;br /&gt;
				var paste = host.querySelector( &#039;.eql-iconfinder-paste&#039; );&lt;br /&gt;
&lt;br /&gt;
				if ( paste ) {&lt;br /&gt;
					window.setTimeout( function () {&lt;br /&gt;
						paste.focus();&lt;br /&gt;
					}, 0 );&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return Promise.resolve();&lt;br /&gt;
		}&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168721</id>
		<title>MediaWiki:BlueprintLoader.js</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=MediaWiki:BlueprintLoader.js&amp;diff=168721"/>
		<updated>2026-08-10T05:38:28Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Update dynamic Blueprint loader and editor tools&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * EQL Wiki - Editor Tools / Dynamic Blueprint Loader&lt;br /&gt;
 *&lt;br /&gt;
 * Source-editor behavior:&lt;br /&gt;
 * - On a new, truly empty page: show the Help:Contents Blueprint selector.&lt;br /&gt;
 * - On every source edit page: show the Icon Finder button.&lt;br /&gt;
 * - Heavy Icon Finder logic is lazy-loaded only when opened.&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
	&#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
	var BLUEPRINT_PAGE = &#039;Help:Contents&#039;;&lt;br /&gt;
	var TOOLS_ID = &#039;eql-editor-tools&#039;;&lt;br /&gt;
	var ICON_FINDER_PAGE = &#039;MediaWiki:IconFinder.js&#039;;&lt;br /&gt;
&lt;br /&gt;
	function htmlToText( html ) {&lt;br /&gt;
		var div = document.createElement( &#039;div&#039; );&lt;br /&gt;
		div.innerHTML = String( html || &#039;&#039; );&lt;br /&gt;
		return ( div.textContent || div.innerText || &#039;&#039; ).trim();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function makeLabel( heading ) {&lt;br /&gt;
		return heading&lt;br /&gt;
			.replace( /\s+Blueprints?\s*$/i, &#039;&#039; )&lt;br /&gt;
			.trim() + &#039; Blueprint&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getEditorContents( $textarea ) {&lt;br /&gt;
		try {&lt;br /&gt;
			return String( $textarea.textSelection( &#039;getContents&#039; ) || &#039;&#039; );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			return String( $textarea.val() || &#039;&#039; );&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setEditorContents( $textarea, text ) {&lt;br /&gt;
		try {&lt;br /&gt;
			$textarea.textSelection( &#039;setContents&#039;, text );&lt;br /&gt;
		} catch ( e ) {&lt;br /&gt;
			$textarea.val( text );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea.trigger( &#039;input&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;change&#039; );&lt;br /&gt;
		$textarea.trigger( &#039;focus&#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function extractBlueprint( sectionText ) {&lt;br /&gt;
		var match;&lt;br /&gt;
		var blueprint;&lt;br /&gt;
&lt;br /&gt;
		sectionText = String( sectionText || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		match = sectionText.match(&lt;br /&gt;
			/&amp;lt;pre(?:\s[^&amp;gt;]*)?&amp;gt;([\s\S]*?)&amp;lt;\/pre&amp;gt;/i&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		if ( !match ) {&lt;br /&gt;
			return null;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		blueprint = match[ 1 ];&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint.replace(&lt;br /&gt;
			/^\s*&amp;lt;nowiki&amp;gt;([\s\S]*?)&amp;lt;\/nowiki&amp;gt;\s*$/i,&lt;br /&gt;
			&#039;$1&#039;&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		blueprint = blueprint&lt;br /&gt;
			.replace( /^\s*\n/, &#039;&#039; )&lt;br /&gt;
			.replace( /\s+$/, &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
		return blueprint + &#039;\n&#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintSections( api ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;tocdata&#039;,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var tocdata =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse.tocdata;&lt;br /&gt;
&lt;br /&gt;
			var sections =&lt;br /&gt;
				tocdata &amp;amp;&amp;amp;&lt;br /&gt;
				Array.isArray( tocdata.sections )&lt;br /&gt;
					? tocdata.sections&lt;br /&gt;
					: [];&lt;br /&gt;
&lt;br /&gt;
			if ( sections.length ) {&lt;br /&gt;
				return sections;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return api.get( {&lt;br /&gt;
				action: &#039;parse&#039;,&lt;br /&gt;
				page: BLUEPRINT_PAGE,&lt;br /&gt;
				prop: &#039;sections&#039;,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			} ).then( function ( fallbackData ) {&lt;br /&gt;
				return (&lt;br /&gt;
					fallbackData &amp;amp;&amp;amp;&lt;br /&gt;
					fallbackData.parse &amp;amp;&amp;amp;&lt;br /&gt;
					Array.isArray( fallbackData.parse.sections )&lt;br /&gt;
				)&lt;br /&gt;
					? fallbackData.parse.sections&lt;br /&gt;
					: [];&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeBlueprintSections( sections ) {&lt;br /&gt;
		return sections&lt;br /&gt;
			.map( function ( section ) {&lt;br /&gt;
				var heading = htmlToText( section.line || &#039;&#039; );&lt;br /&gt;
&lt;br /&gt;
				return {&lt;br /&gt;
					heading: heading,&lt;br /&gt;
					label: makeLabel( heading ),&lt;br /&gt;
					index: String( section.index || &#039;&#039; ),&lt;br /&gt;
					anchor:&lt;br /&gt;
						section.linkAnchor ||&lt;br /&gt;
						section.anchor ||&lt;br /&gt;
						&#039;&#039;&lt;br /&gt;
				};&lt;br /&gt;
			} )&lt;br /&gt;
			.filter( function ( section ) {&lt;br /&gt;
				return (&lt;br /&gt;
					section.index &amp;amp;&amp;amp;&lt;br /&gt;
					/\bBlueprints?\s*$/i.test( section.heading )&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchBlueprintWikitext( api, sectionIndex ) {&lt;br /&gt;
		return api.get( {&lt;br /&gt;
			action: &#039;parse&#039;,&lt;br /&gt;
			page: BLUEPRINT_PAGE,&lt;br /&gt;
			prop: &#039;wikitext&#039;,&lt;br /&gt;
			section: sectionIndex,&lt;br /&gt;
			formatversion: 2&lt;br /&gt;
		} ).then( function ( data ) {&lt;br /&gt;
			var wikitext =&lt;br /&gt;
				data &amp;amp;&amp;amp;&lt;br /&gt;
				data.parse&lt;br /&gt;
					? data.parse.wikitext&lt;br /&gt;
					: &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				wikitext &amp;amp;&amp;amp;&lt;br /&gt;
				typeof wikitext === &#039;object&#039; &amp;amp;&amp;amp;&lt;br /&gt;
				Object.prototype.hasOwnProperty.call( wikitext, &#039;*&#039; )&lt;br /&gt;
			) {&lt;br /&gt;
				wikitext = wikitext[ &#039;*&#039; ];&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			return String( wikitext || &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function addStyles() {&lt;br /&gt;
		if ( document.getElementById( &#039;eql-editor-tools-styles&#039; ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;style&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-editor-tools-styles&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039;{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;margin:0 0 1rem 0;&#039; +&lt;br /&gt;
					&#039;padding:1rem 1.1rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,rgba(16,21,29,.97),rgba(9,13,19,.98));&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.38);&#039; +&lt;br /&gt;
					&#039;border-left:5px solid rgba(216,183,92,.82);&#039; +&lt;br /&gt;
					&#039;border-radius:8px;&#039; +&lt;br /&gt;
					&#039;box-shadow:0 9px 24px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.035);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
					&#039;display:grid;&#039; +&lt;br /&gt;
					&#039;grid-template-columns:minmax(0,1fr) auto;&#039; +&lt;br /&gt;
					&#039;gap:1rem;&#039; +&lt;br /&gt;
					&#039;align-items:end;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-title,&#039; +&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-title{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .35rem 0;&#039; +&lt;br /&gt;
					&#039;color:#d8b75c;&#039; +&lt;br /&gt;
					&#039;font-family:Georgia,Times New Roman,serif;&#039; +&lt;br /&gt;
					&#039;font-size:1.05rem;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;letter-spacing:.035em;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-description{&#039; +&lt;br /&gt;
					&#039;margin:0 0 .7rem 0;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
					&#039;line-height:1.35;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-controls{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-wrap:wrap;&#039; +&lt;br /&gt;
					&#039;align-items:center;&#039; +&lt;br /&gt;
					&#039;gap:.65rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
					&#039;box-sizing:border-box;&#039; +&lt;br /&gt;
					&#039;min-width:260px;&#039; +&lt;br /&gt;
					&#039;max-width:100%;&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .55rem;&#039; +&lt;br /&gt;
					&#039;background:#0b1017;&#039; +&lt;br /&gt;
					&#039;border:1px solid rgba(216,183,92,.36);&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#e2e2cf;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button{&#039; +&lt;br /&gt;
					&#039;min-height:2.35rem;&#039; +&lt;br /&gt;
					&#039;padding:.35rem .85rem;&#039; +&lt;br /&gt;
					&#039;background:linear-gradient(180deg,#806d32,#594817);&#039; +&lt;br /&gt;
					&#039;border:1px solid #b69c4b;&#039; +&lt;br /&gt;
					&#039;border-radius:5px;&#039; +&lt;br /&gt;
					&#039;color:#fff5cc;&#039; +&lt;br /&gt;
					&#039;font-weight:700;&#039; +&lt;br /&gt;
					&#039;cursor:pointer;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:hover:not(:disabled){&#039; +&lt;br /&gt;
					&#039;filter:brightness(1.08);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; button:disabled{&#039; +&lt;br /&gt;
					&#039;cursor:default;&#039; +&lt;br /&gt;
					&#039;opacity:.55;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-source{&#039; +&lt;br /&gt;
					&#039;color:#b9c8ff;&#039; +&lt;br /&gt;
					&#039;font-size:.9rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-blueprint-status{&#039; +&lt;br /&gt;
					&#039;display:block;&#039; +&lt;br /&gt;
					&#039;margin-top:.55rem;&#039; +&lt;br /&gt;
					&#039;min-height:1.2em;&#039; +&lt;br /&gt;
					&#039;color:#aeb8c7;&#039; +&lt;br /&gt;
					&#039;font-size:.86rem;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
					&#039;display:flex;&#039; +&lt;br /&gt;
					&#039;flex-direction:column;&#039; +&lt;br /&gt;
					&#039;align-items:stretch;&#039; +&lt;br /&gt;
					&#039;justify-content:flex-end;&#039; +&lt;br /&gt;
					&#039;min-width:150px;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-toggle{&#039; +&lt;br /&gt;
					&#039;white-space:nowrap;&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host{&#039; +&lt;br /&gt;
					&#039;margin-top:1rem;&#039; +&lt;br /&gt;
					&#039;padding-top:1rem;&#039; +&lt;br /&gt;
					&#039;border-top:1px solid rgba(216,183,92,.22);&#039; +&lt;br /&gt;
				&#039;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;#eql-icon-finder-host[hidden]{display:none!important;}&#039; +&lt;br /&gt;
&lt;br /&gt;
				&#039;@media(max-width:760px){&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-editor-tools-row{&#039; +&lt;br /&gt;
						&#039;grid-template-columns:1fr;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; .eql-iconfinder-launch{&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
					&#039;#&#039; + TOOLS_ID + &#039; select{&#039; +&lt;br /&gt;
						&#039;width:100%;&#039; +&lt;br /&gt;
						&#039;min-width:0;&#039; +&lt;br /&gt;
					&#039;}&#039; +&lt;br /&gt;
				&#039;}&#039;&lt;br /&gt;
		} ).appendTo( document.head );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function loadIconFinderScript() {&lt;br /&gt;
		if ( window.EQLIconFinder ) {&lt;br /&gt;
			return Promise.resolve( window.EQLIconFinder );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( window.eqlIconFinderLoadingPromise ) {&lt;br /&gt;
			return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		window.eqlIconFinderLoadingPromise = new Promise( function ( resolve, reject ) {&lt;br /&gt;
			var script = document.createElement( &#039;script&#039; );&lt;br /&gt;
&lt;br /&gt;
			script.src = mw.util.getUrl( ICON_FINDER_PAGE, {&lt;br /&gt;
				action: &#039;raw&#039;,&lt;br /&gt;
				ctype: &#039;text/javascript&#039;&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			script.async = true;&lt;br /&gt;
&lt;br /&gt;
			script.onload = function () {&lt;br /&gt;
				if ( window.EQLIconFinder ) {&lt;br /&gt;
					resolve( window.EQLIconFinder );&lt;br /&gt;
				} else {&lt;br /&gt;
					reject( new Error( &#039;IconFinder.js loaded without exposing EQLIconFinder.&#039; ) );&lt;br /&gt;
				}&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			script.onerror = function () {&lt;br /&gt;
				reject( new Error( &#039;Could not load IconFinder.js.&#039; ) );&lt;br /&gt;
			};&lt;br /&gt;
&lt;br /&gt;
			document.head.appendChild( script );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return window.eqlIconFinderLoadingPromise;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildIconFinderLaunch( $row, $tools ) {&lt;br /&gt;
		var $launch = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-launch&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-iconfinder-title&#039;,&lt;br /&gt;
			text: &#039;ICON FINDER&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-toggle&#039;,&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Icon Finder&#039;,&lt;br /&gt;
			&#039;aria-expanded&#039;: &#039;false&#039;&lt;br /&gt;
		} ).appendTo( $launch );&lt;br /&gt;
&lt;br /&gt;
		var $host = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: &#039;eql-icon-finder-host&#039;,&lt;br /&gt;
			hidden: true&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var opening = $host.prop( &#039;hidden&#039; );&lt;br /&gt;
&lt;br /&gt;
			if ( !opening ) {&lt;br /&gt;
				$host.prop( &#039;hidden&#039;, true );&lt;br /&gt;
				$button.attr( &#039;aria-expanded&#039;, &#039;false&#039; );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$button.text( &#039;Loading…&#039; );&lt;br /&gt;
&lt;br /&gt;
			loadIconFinderScript()&lt;br /&gt;
				.then( function ( finder ) {&lt;br /&gt;
					return finder.init( $host[ 0 ] );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$host.prop( &#039;hidden&#039;, false );&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Icon Finder:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$host&lt;br /&gt;
						.prop( &#039;hidden&#039;, false )&lt;br /&gt;
						.text( &#039;Icon Finder could not be loaded.&#039; );&lt;br /&gt;
&lt;br /&gt;
					$button.attr( &#039;aria-expanded&#039;, &#039;true&#039; );&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.text( &#039;Icon Finder&#039; );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function buildBlueprintArea( $row, $textarea, api ) {&lt;br /&gt;
		var $area = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-area&#039;&lt;br /&gt;
		} ).appendTo( $row );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-title&#039;,&lt;br /&gt;
			text: &#039;CREATE FROM A BLUEPRINT&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-description&#039;,&lt;br /&gt;
			text:&lt;br /&gt;
				&#039;Load a standard page skeleton from Help:Contents, &#039; +&lt;br /&gt;
				&#039;or continue with the empty editor below.&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $controls = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-controls&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		var $select = $( &#039;&amp;lt;select&amp;gt;&#039;, {&lt;br /&gt;
			&#039;aria-label&#039;: &#039;Select page blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
			value: &#039;&#039;,&lt;br /&gt;
			text: &#039;Loading blueprints...&#039;&lt;br /&gt;
		} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
		var $button = $( &#039;&amp;lt;button&amp;gt;&#039;, {&lt;br /&gt;
			type: &#039;button&#039;,&lt;br /&gt;
			text: &#039;Load Blueprint&#039;,&lt;br /&gt;
			disabled: true&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $sourceLink = $( &#039;&amp;lt;a&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-source&#039;,&lt;br /&gt;
			href: mw.util.getUrl( BLUEPRINT_PAGE ),&lt;br /&gt;
			text: &#039;View Blueprints&#039;,&lt;br /&gt;
			target: &#039;_blank&#039;,&lt;br /&gt;
			rel: &#039;noopener&#039;&lt;br /&gt;
		} ).appendTo( $controls );&lt;br /&gt;
&lt;br /&gt;
		var $status = $( &#039;&amp;lt;span&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-blueprint-status&#039;,&lt;br /&gt;
			&#039;aria-live&#039;: &#039;polite&#039;&lt;br /&gt;
		} ).appendTo( $area );&lt;br /&gt;
&lt;br /&gt;
		fetchBlueprintSections( api )&lt;br /&gt;
			.then( function ( sections ) {&lt;br /&gt;
				var blueprints = normalizeBlueprintSections( sections );&lt;br /&gt;
&lt;br /&gt;
				$select.empty();&lt;br /&gt;
&lt;br /&gt;
				$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
					value: &#039;&#039;,&lt;br /&gt;
					text: blueprints.length ?&lt;br /&gt;
						&#039;Select a blueprint...&#039; :&lt;br /&gt;
						&#039;No blueprints found&#039;&lt;br /&gt;
				} ).appendTo( $select );&lt;br /&gt;
&lt;br /&gt;
				blueprints.forEach( function ( blueprint ) {&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: blueprint.index,&lt;br /&gt;
						text: blueprint.label&lt;br /&gt;
					} )&lt;br /&gt;
						.attr( &#039;data-anchor&#039;, blueprint.anchor )&lt;br /&gt;
						.appendTo( $select );&lt;br /&gt;
				} );&lt;br /&gt;
&lt;br /&gt;
				$select.prop( &#039;disabled&#039;, !blueprints.length );&lt;br /&gt;
			} )&lt;br /&gt;
			.catch( function ( error ) {&lt;br /&gt;
				if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
					console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
				}&lt;br /&gt;
&lt;br /&gt;
				$select.empty().append(&lt;br /&gt;
					$( &#039;&amp;lt;option&amp;gt;&#039;, {&lt;br /&gt;
						value: &#039;&#039;,&lt;br /&gt;
						text: &#039;Could not load blueprints&#039;&lt;br /&gt;
					} )&lt;br /&gt;
				);&lt;br /&gt;
&lt;br /&gt;
				$status.text(&lt;br /&gt;
					&#039;Could not read Blueprint headings from Help:Contents.&#039;&lt;br /&gt;
				);&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
		$select.on( &#039;change&#039;, function () {&lt;br /&gt;
			var $option = $select.find( &#039;option:selected&#039; );&lt;br /&gt;
			var anchor = $option.attr( &#039;data-anchor&#039; ) || &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
&lt;br /&gt;
			if ( anchor ) {&lt;br /&gt;
				$sourceLink.attr(&lt;br /&gt;
					&#039;href&#039;,&lt;br /&gt;
					mw.util.getUrl( BLUEPRINT_PAGE ) + &#039;#&#039; + anchor&lt;br /&gt;
				);&lt;br /&gt;
				$sourceLink.text( &#039;View This Blueprint&#039; );&lt;br /&gt;
			} else {&lt;br /&gt;
				$sourceLink.attr( &#039;href&#039;, mw.util.getUrl( BLUEPRINT_PAGE ) );&lt;br /&gt;
				$sourceLink.text( &#039;View Blueprints&#039; );&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$status.text( &#039;&#039; );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$button.on( &#039;click&#039;, function () {&lt;br /&gt;
			var sectionIndex = $select.val();&lt;br /&gt;
			var existing;&lt;br /&gt;
&lt;br /&gt;
			if ( !sectionIndex ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			existing = getEditorContents( $textarea );&lt;br /&gt;
&lt;br /&gt;
			if (&lt;br /&gt;
				existing.trim() &amp;amp;&amp;amp;&lt;br /&gt;
				!window.confirm(&lt;br /&gt;
					&#039;The editor already contains text. Replace it with the selected blueprint?&#039;&lt;br /&gt;
				)&lt;br /&gt;
			) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			$button.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$select.prop( &#039;disabled&#039;, true );&lt;br /&gt;
			$status.text( &#039;Loading current blueprint from Help:Contents...&#039; );&lt;br /&gt;
&lt;br /&gt;
			fetchBlueprintWikitext( api, sectionIndex )&lt;br /&gt;
				.then( function ( sectionText ) {&lt;br /&gt;
					var blueprint = extractBlueprint( sectionText );&lt;br /&gt;
&lt;br /&gt;
					if ( blueprint === null ) {&lt;br /&gt;
						throw new Error( &#039;No &amp;lt;pre&amp;gt; block found.&#039; );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					setEditorContents( $textarea, blueprint );&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Blueprint loaded. Edit the fields below, then publish normally.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.catch( function ( error ) {&lt;br /&gt;
					if ( window.console &amp;amp;&amp;amp; console.error ) {&lt;br /&gt;
						console.error( &#039;EQL Blueprint Loader:&#039;, error );&lt;br /&gt;
					}&lt;br /&gt;
&lt;br /&gt;
					$status.text(&lt;br /&gt;
						&#039;Could not load this blueprint. Check Help:Contents and make sure the section contains a &amp;lt;pre&amp;gt; block.&#039;&lt;br /&gt;
					);&lt;br /&gt;
				} )&lt;br /&gt;
				.then( function () {&lt;br /&gt;
					$select.prop( &#039;disabled&#039;, false );&lt;br /&gt;
					$button.prop( &#039;disabled&#039;, !$select.val() );&lt;br /&gt;
				} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function init() {&lt;br /&gt;
		var action = mw.config.get( &#039;wgAction&#039; );&lt;br /&gt;
		var $textarea;&lt;br /&gt;
		var $tools;&lt;br /&gt;
		var $row;&lt;br /&gt;
		var $editorHost;&lt;br /&gt;
		var api;&lt;br /&gt;
		var isNewPage;&lt;br /&gt;
		var editorIsEmpty;&lt;br /&gt;
&lt;br /&gt;
		if ( [ &#039;edit&#039;, &#039;submit&#039; ].indexOf( action ) === -1 ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$textarea = $( &#039;#wpTextbox1&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$textarea.length ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( document.getElementById( TOOLS_ID ) ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		api = new mw.Api();&lt;br /&gt;
		isNewPage = Number( mw.config.get( &#039;wgArticleId&#039; ) ) === 0;&lt;br /&gt;
		editorIsEmpty = !getEditorContents( $textarea ).trim();&lt;br /&gt;
&lt;br /&gt;
		addStyles();&lt;br /&gt;
&lt;br /&gt;
		$tools = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			id: TOOLS_ID&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$row = $( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
			class: &#039;eql-editor-tools-row&#039;&lt;br /&gt;
		} ).appendTo( $tools );&lt;br /&gt;
&lt;br /&gt;
		if ( isNewPage &amp;amp;&amp;amp; editorIsEmpty ) {&lt;br /&gt;
			buildBlueprintArea( $row, $textarea, api );&lt;br /&gt;
		} else {&lt;br /&gt;
			$( &#039;&amp;lt;div&amp;gt;&#039;, {&lt;br /&gt;
				class: &#039;eql-editor-tools-spacer&#039;&lt;br /&gt;
			} ).appendTo( $row );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		buildIconFinderLaunch( $row, $tools );&lt;br /&gt;
&lt;br /&gt;
		$editorHost = $textarea.closest( &#039;.wikiEditor-ui&#039; );&lt;br /&gt;
&lt;br /&gt;
		if ( !$editorHost.length ) {&lt;br /&gt;
			$editorHost = $textarea;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$tools.insertBefore( $editorHost );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	mw.loader.using( [&lt;br /&gt;
		&#039;mediawiki.api&#039;,&lt;br /&gt;
		&#039;mediawiki.util&#039;,&lt;br /&gt;
		&#039;jquery.textSelection&#039;&lt;br /&gt;
	] ).then( function () {&lt;br /&gt;
		$( init );&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=The_Baron%27s_Blade&amp;diff=149100</id>
		<title>The Baron&#039;s Blade</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=The_Baron%27s_Blade&amp;diff=149100"/>
		<updated>2026-06-04T01:01:22Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
[[File:TheBaronsBlade.png|200px|thumb|&#039;&#039;&#039;The Baron&#039;s Blade&#039;&#039;&#039;]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = This needs further editing. All I had was source image for a +10 blade, as such the stats block is not entirely accurate for the Item Level slider. Also not sure how to find the Lucy ID for this item.&lt;br /&gt;
&lt;br /&gt;
|itemname    = The Baron&#039;s Blade&lt;br /&gt;
|lucy_img_ID = ?&lt;br /&gt;
|statsblock  = &lt;br /&gt;
LORE ITEM  NO TRADE&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: PRIMARY SECONDARY &amp;lt;br&amp;gt;&lt;br /&gt;
Skill: 1H Slashing &amp;lt;br&amp;gt;&lt;br /&gt;
Atk Delay: 30 &amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 20 &amp;lt;br&amp;gt;&lt;br /&gt;
DEX: 15  STA: 15 &amp;lt;br&amp;gt;&lt;br /&gt;
SV VOID: 10 &amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.5  Size: MEDIUM &amp;lt;br&amp;gt;&lt;br /&gt;
Class: WAR PAL RNG SHD BRD ROG&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Befallen]]&lt;br /&gt;
* [[Baron Telyx V`Zher]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Shadow Knight Equipment]]&lt;br /&gt;
[[Category:Bard Equipment]]&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:Secondary]]&lt;br /&gt;
&lt;br /&gt;
[[Category:1H Slashing]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Befallen]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Spell:_Ice_Comet&amp;diff=149099</id>
		<title>Spell: Ice Comet</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Spell:_Ice_Comet&amp;diff=149099"/>
		<updated>2026-06-04T01:01:21Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Spell: Ice Comet&lt;br /&gt;
|lucy_img_ID = 504&lt;br /&gt;
|statsblock  = &lt;br /&gt;
MAGIC ITEM&amp;lt;br&amp;gt;&lt;br /&gt;
Level Needed: WIZ(49)&amp;lt;br&amp;gt;&lt;br /&gt;
Skill: Evocation&amp;lt;br&amp;gt;&lt;br /&gt;
Mana Cost: 400&amp;lt;br&amp;gt;&lt;br /&gt;
Effect:  [[Ice Comet]]&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.1  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests =&lt;br /&gt;
&lt;br /&gt;
[[Tarton&#039;s Wheel]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Inventory Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Enchanted_Fine_Steel_Bastard_Sword&amp;diff=149098</id>
		<title>Enchanted Fine Steel Bastard Sword</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Enchanted_Fine_Steel_Bastard_Sword&amp;diff=149098"/>
		<updated>2026-06-04T01:01:21Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|itemname    = Enchanted Fine Steel Bastard Sword&lt;br /&gt;
|lucy_img_ID = 577&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Attunable, Placeable &amp;lt;br&amp;gt;&lt;br /&gt;
Slot: PRIMARY &amp;lt;br&amp;gt;&lt;br /&gt;
Skill: 2H Slashing  Atk Delay: 42&amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 20 &amp;lt;br&amp;gt;&lt;br /&gt;
STR: 7&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 8.5  Size: MEDIUM&amp;lt;br&amp;gt;&lt;br /&gt;
Class: WAR PAL RNG SHD BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|notes       = &lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Befallen]]&lt;br /&gt;
*[[Knight V`Tal]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Shadow Knight Equipment]]&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:2H Slashing]]&lt;br /&gt;
[[Category:Befallen]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Enchanted_Fine_Steel_Long_Sword&amp;diff=149097</id>
		<title>Enchanted Fine Steel Long Sword</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Enchanted_Fine_Steel_Long_Sword&amp;diff=149097"/>
		<updated>2026-06-04T01:01:21Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Enchanted Fine Steel Long Sword&lt;br /&gt;
|lucy_img_ID = 590&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Attunable, Placeable &amp;lt;br&amp;gt;&lt;br /&gt;
Slot: PRIMARY SECONDARY&amp;lt;br&amp;gt;&lt;br /&gt;
Skill: 1H Slashing  Atk Delay: 28&amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 7 &amp;lt;br&amp;gt;&lt;br /&gt;
STR: 5 AGI: 2 &amp;lt;br&amp;gt;&lt;br /&gt;
WT: 7.5  Size: MEDIUM&amp;lt;br&amp;gt;&lt;br /&gt;
Class: WAR PAL RNG SHD BRD ROG&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|notes       = &lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Befallen]]&lt;br /&gt;
*[[Knight V`Tal]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Bard Equipment]]&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
[[Category:Shadow Knight Equipment]]&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:Secondary]]&lt;br /&gt;
[[Category:1H Slashing]]&lt;br /&gt;
[[Category:Befallen]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Bracer&amp;diff=149096</id>
		<title>Pristine Studded Leather Bracer</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Bracer&amp;diff=149096"/>
		<updated>2026-06-04T01:01:21Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       &lt;br /&gt;
|itemname    = Pristine Studded Leather Bracer&lt;br /&gt;
|lucy_img_ID = 637&lt;br /&gt;
|statsblock  =&lt;br /&gt;
No Trade&amp;lt;br&amp;gt; &lt;br /&gt;
Slot: WRIST&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 6 HP: 5&amp;lt;br&amp;gt;&lt;br /&gt;
INT: 5 SV Fire: 2&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 1.0  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Class: WAR CLR PAL RNG SHD DRU MNK BRD ROG SHM BST BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Befallen]]&lt;br /&gt;
&lt;br /&gt;
* [[Arisen Thaumaturgist]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Bard Equipment]]&lt;br /&gt;
[[Category:Cleric Equipment]]&lt;br /&gt;
[[Category:Druid Equipment]]&lt;br /&gt;
[[Category:Monk Equipment]]&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
[[Category:Shaman Equipment]]&lt;br /&gt;
[[Category:Shadow Knight Equipment]]&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Wrist]]&lt;br /&gt;
[[Category:Fashion: Leather]]&lt;br /&gt;
[[Category:Befallen]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Sleeves&amp;diff=149095</id>
		<title>Pristine Studded Leather Sleeves</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Sleeves&amp;diff=149095"/>
		<updated>2026-06-04T01:01:20Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       &lt;br /&gt;
|itemname    = Pristine Studded Leather Sleeves&lt;br /&gt;
|lucy_img_ID = 634&lt;br /&gt;
|statsblock  =&lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt; &lt;br /&gt;
Slot: ARMS&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 8 HP: 5&amp;lt;br&amp;gt;&lt;br /&gt;
STR: 3 STA: 2&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 2.0  Size: MEDIUM&amp;lt;br&amp;gt;&lt;br /&gt;
Class: WAR CLR PAL RNG SHD DRU MNK BRD ROG SHM BST BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Befallen]]&lt;br /&gt;
&lt;br /&gt;
* [[Soldier of V`Zher]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Bard Equipment]]&lt;br /&gt;
[[Category:Cleric Equipment]]&lt;br /&gt;
[[Category:Druid Equipment]]&lt;br /&gt;
[[Category:Monk Equipment]]&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
[[Category:Shaman Equipment]]&lt;br /&gt;
[[Category:Shadow Knight Equipment]]&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Chest]]&lt;br /&gt;
[[Category:Fashion: Leather]]&lt;br /&gt;
[[Category:Befallen]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Leggings&amp;diff=149094</id>
		<title>Pristine Studded Leather Leggings</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Leggings&amp;diff=149094"/>
		<updated>2026-06-04T01:01:20Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       &lt;br /&gt;
|itemname    = Pristine Studded Leather Leggings&lt;br /&gt;
|lucy_img_ID = 635&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt; &lt;br /&gt;
Slot: LEGS&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 12&amp;lt;br&amp;gt;&lt;br /&gt;
STA: 5 DEX: 2&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 5.0  Size: MEDIUM&amp;lt;br&amp;gt;&lt;br /&gt;
Class: WAR CLR PAL RNG SHD DRU MNK BRD ROG SHM BST BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Befallen]]&lt;br /&gt;
&lt;br /&gt;
* [[Soldier of V`Zher]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Bard Equipment]]&lt;br /&gt;
[[Category:Cleric Equipment]]&lt;br /&gt;
[[Category:Druid Equipment]]&lt;br /&gt;
[[Category:Monk Equipment]]&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
[[Category:Shaman Equipment]]&lt;br /&gt;
[[Category:Shadow Knight Equipment]]&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Chest]]&lt;br /&gt;
[[Category:Fashion: Leather]]&lt;br /&gt;
[[Category:Befallen]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=White_Dragon_Hide&amp;diff=149093</id>
		<title>White Dragon Hide</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=White_Dragon_Hide&amp;diff=149093"/>
		<updated>2026-06-04T01:01:20Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = White Dragon Hide&lt;br /&gt;
|lucy_img_ID = 552&lt;br /&gt;
|statsblock  = &lt;br /&gt;
NO DROP  QUEST&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 3.0  Size: MEDIUM&amp;lt;br&amp;gt;&lt;br /&gt;
Class: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Dreadlands]]&lt;br /&gt;
&lt;br /&gt;
* [[Gorenaire]]&lt;br /&gt;
&lt;br /&gt;
[[Permafrost]]&lt;br /&gt;
&lt;br /&gt;
* [[Lady Vox]]&lt;br /&gt;
&lt;br /&gt;
|relatedquests = &lt;br /&gt;
&lt;br /&gt;
* [[White Dragonscale Cloak Quest]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Inventory Items]]&lt;br /&gt;
[[Category:Permafrost]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Tunic&amp;diff=149092</id>
		<title>Pristine Studded Leather Tunic</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Tunic&amp;diff=149092"/>
		<updated>2026-06-04T01:01:20Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       &lt;br /&gt;
|itemname    = Pristine Studded Leather Tunic&lt;br /&gt;
|lucy_img_ID = 632&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt; &lt;br /&gt;
Slot: CHEST&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 14&amp;lt;br&amp;gt;&lt;br /&gt;
STR: 2 AGI: 2 DEX: 2&amp;lt;br&amp;gt;&lt;br /&gt;
SV Poison: 5&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 5.0  Size: LARGE&amp;lt;br&amp;gt;&lt;br /&gt;
Class: WAR CLR PAL RNG SHD DRU MNK BRD ROG SHM BST BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Befallen]]&lt;br /&gt;
&lt;br /&gt;
* [[Baron Telyx V`Zher]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Bard Equipment]]&lt;br /&gt;
[[Category:Cleric Equipment]]&lt;br /&gt;
[[Category:Druid Equipment]]&lt;br /&gt;
[[Category:Monk Equipment]]&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
[[Category:Shaman Equipment]]&lt;br /&gt;
[[Category:Shadow Knight Equipment]]&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Chest]]&lt;br /&gt;
[[Category:Fashion: Leather]]&lt;br /&gt;
[[Category:Befallen]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Gloves&amp;diff=149091</id>
		<title>Pristine Studded Leather Gloves</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Pristine_Studded_Leather_Gloves&amp;diff=149091"/>
		<updated>2026-06-04T01:01:19Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Pristine Studded Leather Gloves&lt;br /&gt;
|lucy_img_ID = 636&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: HANDS&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 5 Mana: 5&amp;lt;br&amp;gt;&lt;br /&gt;
DEX: 2 WIS: 2&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.2  Size: TINY&amp;lt;br&amp;gt;&lt;br /&gt;
Class: WAR CLR PAL RNG SHD DRU MNK BRD ROG SHM BST BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Befallen]]&lt;br /&gt;
&lt;br /&gt;
* [[Footman of V`Zher]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Cleric Equipment]]&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Shadowknight Equipment]]&lt;br /&gt;
[[Category:Druid Equipment]]&lt;br /&gt;
[[Category:Monk Equipment]]&lt;br /&gt;
[[Category:Bard Equipment]]&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
[[Category:Shaman Equipment]]&lt;br /&gt;
[[Category:Beastlord Equipment]]&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Hands]]&lt;br /&gt;
[[Category:Befallen]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Shroud_of_the_Sky&amp;diff=149090</id>
		<title>Shroud of the Sky</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Shroud_of_the_Sky&amp;diff=149090"/>
		<updated>2026-06-04T01:01:19Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Custom EQL Plane of Sky reward.&lt;br /&gt;
|itemname    = Shroud of the Sky&lt;br /&gt;
|lucy_img_ID = 769&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: SHOULDERS&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 8&amp;lt;br&amp;gt;&lt;br /&gt;
HP: +25  End: +25&amp;lt;br&amp;gt;&lt;br /&gt;
STR: +7  WIS: +7  DEX: +7&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 2.5  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Click Effect: [[Raging Strength]] (Must Equip)&amp;lt;br&amp;gt;&lt;br /&gt;
Cast Time: 3.5 seconds&amp;lt;br&amp;gt;&lt;br /&gt;
Required Level: 46&amp;lt;br&amp;gt;&lt;br /&gt;
Cooldown: 120 seconds&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
* [[Berserker Test of Burden]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Shoulders]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Cudgel_of_the_Fool&amp;diff=149088</id>
		<title>Cudgel of the Fool</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Cudgel_of_the_Fool&amp;diff=149088"/>
		<updated>2026-06-04T01:01:18Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic}}&lt;br /&gt;
[[File:Cudgel of the Fool.png|200px|thumb|&#039;&#039;&#039;Cudgel of the Fool&#039;&#039;&#039;]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|itemname    = Cudgel of the Fool&lt;br /&gt;
|lucy_img_ID = ?&lt;br /&gt;
|statsblock  = &lt;br /&gt;
LORE ITEM  NO TRADE&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: PRIMARY&amp;lt;br&amp;gt;&lt;br /&gt;
Skill: 2H Blunt  Atk Delay: 52&amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 45  Dmg Bonus: 40&amp;lt;br&amp;gt;&lt;br /&gt;
INT: -7&amp;lt;br&amp;gt;&lt;br /&gt;
Effect: [[Fury of the Chosen]] (Combat)&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 5.7  Size: GIANT&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
?&lt;br /&gt;
|relatedquests = &lt;br /&gt;
{{:Berserker Test of Fools Errand}}&lt;br /&gt;
|playercrafted = &lt;br /&gt;
?&lt;br /&gt;
|soldby = &lt;br /&gt;
?&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:2H Blunt]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Skycleaver&amp;diff=149087</id>
		<title>Skycleaver</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Skycleaver&amp;diff=149087"/>
		<updated>2026-06-04T01:01:18Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Plane of Sky Custom&lt;br /&gt;
|itemname    = Skycleaver&lt;br /&gt;
|lucy_img_ID = ?&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: PRIMARY&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Skill: 2H Slashing  Atk Delay: 35&amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 30  Dmg Bon: 24&amp;lt;br&amp;gt;&lt;br /&gt;
STA: +5  DEX: +10&amp;lt;br&amp;gt;&lt;br /&gt;
SV DISEASE: +5&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 8.0  Size: GIANT&amp;lt;br&amp;gt;&lt;br /&gt;
Combat Effect: Haste (Req Level 30)&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
*Berserker Test of Sharpness&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Molten_Coil&amp;diff=149086</id>
		<title>Molten Coil</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Molten_Coil&amp;diff=149086"/>
		<updated>2026-06-04T01:01:18Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{DISPLAYTITLE:Molten Coil}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Custom Plane of Sky&lt;br /&gt;
|itemname    = Molten Coil&lt;br /&gt;
|lucy_img_ID = ?&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: FINGERS&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 5&amp;lt;br&amp;gt;&lt;br /&gt;
Mana: -55&amp;lt;br&amp;gt;&lt;br /&gt;
STR: +6  STA: +5  AGI: +5  DEX: +6  CHA: +10&amp;lt;br&amp;gt;&lt;br /&gt;
SV FIRE: +5&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.1  Size: TINY&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
* [[Berserker Test of Will]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{DEFAULTSORT:Molten Coil}}&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Fingers]]&lt;br /&gt;
[[Category:?]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Molten_Ring&amp;diff=149085</id>
		<title>Molten Ring</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Molten_Ring&amp;diff=149085"/>
		<updated>2026-06-04T01:01:18Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{DISPLAYTITLE:Molten Coil}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Custom Plane of Sky&lt;br /&gt;
|itemname    = Molten Coil&lt;br /&gt;
|lucy_img_ID = ?&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: FINGERS&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 5&amp;lt;br&amp;gt;&lt;br /&gt;
Mana: -55&amp;lt;br&amp;gt;&lt;br /&gt;
STR: +6  STA: +5  AGI: +5  DEX: +6  CHA: +10&amp;lt;br&amp;gt;&lt;br /&gt;
SV FIRE: +5&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.1  Size: TINY&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
* [[Berserker Test of Will]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{DEFAULTSORT:Molten Coil}}&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Fingers]]&lt;br /&gt;
[[Category:?]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Blood-Drawn_Runes&amp;diff=149084</id>
		<title>Blood-Drawn Runes</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Blood-Drawn_Runes&amp;diff=149084"/>
		<updated>2026-06-04T01:01:17Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Plane of Sky Custom&lt;br /&gt;
|itemname    = Blood-Drawn Runes&lt;br /&gt;
|lucy_img_ID = 797&lt;br /&gt;
|statsblock  = &lt;br /&gt;
No Trade, Quest&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: CHEST&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
HP: +55  End: +25&amp;lt;br&amp;gt;&lt;br /&gt;
STR: +10  STA: +10  AGI: +5  DEX: +10&amp;lt;br&amp;gt;&lt;br /&gt;
HP Regen: +10&amp;lt;br&amp;gt;&lt;br /&gt;
Size: LARGE&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
* [[Berserker Test of Blood]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Chest]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Spiroc_Beak_Earcuff&amp;diff=149083</id>
		<title>Spiroc Beak Earcuff</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Spiroc_Beak_Earcuff&amp;diff=149083"/>
		<updated>2026-06-04T01:01:17Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Custom EQL Plane of Sky reward.&lt;br /&gt;
|itemname    = Spiroc Beak Earcuff&lt;br /&gt;
|lucy_img_ID = ?&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: EAR&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BST&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 4&amp;lt;br&amp;gt;&lt;br /&gt;
HP: +10  Mana: +10  End: +10&amp;lt;br&amp;gt;&lt;br /&gt;
WIS: +5  CHA: +5&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.1  Size: TINY&amp;lt;br&amp;gt;&lt;br /&gt;
Click Effect: Vigor of Zehkes (Must Equip)&amp;lt;br&amp;gt;&lt;br /&gt;
Cast Time: Instant&amp;lt;br&amp;gt;&lt;br /&gt;
Required Level: 46&amp;lt;br&amp;gt;&lt;br /&gt;
Cooldown: 120 seconds&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
* [[Beastlord Test of Aviak]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Beastlord Equipment]]&lt;br /&gt;
[[Category:Ear]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Sash_of_Ferocity&amp;diff=149082</id>
		<title>Sash of Ferocity</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Sash_of_Ferocity&amp;diff=149082"/>
		<updated>2026-06-04T01:01:17Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Custom EQL Plane of Sky reward.&lt;br /&gt;
|itemname    = Sash of Ferocity&lt;br /&gt;
|lucy_img_ID = ?&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: WAIST&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 10&amp;lt;br&amp;gt;&lt;br /&gt;
STR: +6  DEX: +5&amp;lt;br&amp;gt;&lt;br /&gt;
SV MAGIC: +5&amp;lt;br&amp;gt;&lt;br /&gt;
Haste: +41%&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.1  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
* [[Berserker Test of Ferocity]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Waist]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Griffin-Hide_Armguards&amp;diff=149081</id>
		<title>Griffin-Hide Armguards</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Griffin-Hide_Armguards&amp;diff=149081"/>
		<updated>2026-06-04T01:01:17Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Custom EQL Plane of Sky reward.&lt;br /&gt;
|itemname    = Griffin-Hide Armguards&lt;br /&gt;
|lucy_img_ID = 634&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: ARMS&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BST&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 6&amp;lt;br&amp;gt;&lt;br /&gt;
STA: +4  WIS: +3&amp;lt;br&amp;gt;&lt;br /&gt;
SV MAGIC: +5  SV FIRE: +5  SV COLD: +5  SV DISEASE: +5  SV POISON: +5&amp;lt;br&amp;gt;&lt;br /&gt;
End: +15&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 2.5  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
* [[Beastlord Test of Harpy]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Beastlord Equipment]]&lt;br /&gt;
[[Category:Arms]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Diaphonous_Waistband&amp;diff=149080</id>
		<title>Diaphonous Waistband</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Diaphonous_Waistband&amp;diff=149080"/>
		<updated>2026-06-04T01:01:16Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Custom EQL Plane of Sky reward.&lt;br /&gt;
|itemname    = Diaphonous Waistband&lt;br /&gt;
|lucy_img_ID = 718&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Lore Equipped, No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: WAIST&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BST&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 5&amp;lt;br&amp;gt;&lt;br /&gt;
STR: +5  WIS: +10  DEX: +10&amp;lt;br&amp;gt;&lt;br /&gt;
Haste: +41%&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.1  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
* [[Beastlord Test of Wind]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Beastlord Equipment]]&lt;br /&gt;
[[Category:Waist]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Griffon-Hide_Armguards&amp;diff=149079</id>
		<title>Griffon-Hide Armguards</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Griffon-Hide_Armguards&amp;diff=149079"/>
		<updated>2026-06-04T01:01:16Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Custom EQL Plane of Sky reward.&lt;br /&gt;
|itemname    = Griffin-Hide Armguards&lt;br /&gt;
|lucy_img_ID = 634&lt;br /&gt;
|statsblock  = &lt;br /&gt;
LORE ITEM  NO TRADE&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: ARMS&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BST&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 6&amp;lt;br&amp;gt;&lt;br /&gt;
STA: +4  WIS: +3&amp;lt;br&amp;gt;&lt;br /&gt;
SV MAGIC: +5  SV FIRE: +5  SV COLD: +5  SV DISEASE: +5  SV POISON: +5&amp;lt;br&amp;gt;&lt;br /&gt;
End: +15&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 2.5  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
* [[Beastlord Test of Harpy]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Beastlord Equipment]]&lt;br /&gt;
[[Category:Arms]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Spirit_Render&amp;diff=149078</id>
		<title>Spirit Render</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Spirit_Render&amp;diff=149078"/>
		<updated>2026-06-04T01:01:16Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Sky Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       =&lt;br /&gt;
Custom EQL Plane of Sky reward.&lt;br /&gt;
|itemname    = Spirit Render&lt;br /&gt;
|lucy_img_ID = 975&lt;br /&gt;
|statsblock  = &lt;br /&gt;
No Trade  &amp;lt;br&amp;gt;&lt;br /&gt;
Slot: Secondary&amp;lt;br&amp;gt;&lt;br /&gt;
Skill: Hand to Hand  Atk Delay: 23&amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 10&amp;lt;br&amp;gt;&lt;br /&gt;
HP: 25 Mana: 25&amp;lt;br&amp;gt;&lt;br /&gt;
Effect: [[Sha&#039;s Lethargy]] (Combat, Casting Time: Instant) at Level 46&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.1  Size: Tiny&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BST&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
&lt;br /&gt;
* [[BeastlordPlane of Sky Tests|Beastlord Test of Claw]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Beastlord Equipment]]&lt;br /&gt;
[[Category:Secondary]]&lt;br /&gt;
[[Category:Hand to Hand]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Windhowl&amp;diff=149077</id>
		<title>Windhowl</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Windhowl&amp;diff=149077"/>
		<updated>2026-06-04T01:01:16Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Sky Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       =&lt;br /&gt;
Custom EQL Plane of Sky reward.&lt;br /&gt;
|itemname    = Windhowl&lt;br /&gt;
|lucy_img_ID = 975&lt;br /&gt;
|statsblock  = &lt;br /&gt;
No Trade  &amp;lt;br&amp;gt;&lt;br /&gt;
Slot: Primary&amp;lt;br&amp;gt;&lt;br /&gt;
Skill: Hand to Hand  Atk Delay: 22&amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 12 &amp;lt;br&amp;gt;&lt;br /&gt;
HP: 55 End: 25&amp;lt;br&amp;gt;&lt;br /&gt;
STR: +4  DEX: +4&amp;lt;br&amp;gt;&lt;br /&gt;
Effect: [[Herikol&#039;s Soothing]] (Combat, Casting Time: Instant) at Level 46&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.1  Size: Tiny&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BST&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
&lt;br /&gt;
* [[BeastlordPlane of Sky Tests|Beastlord Test of Claw]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Beastlord Equipment]]&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:Hand to Hand]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Baton_of_the_Sky&amp;diff=149076</id>
		<title>Baton of the Sky</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Baton_of_the_Sky&amp;diff=149076"/>
		<updated>2026-06-04T01:01:15Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Sky Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|itemname    = Baton of the Sky&lt;br /&gt;
|lucy_img_ID = 822&lt;br /&gt;
|statsblock  = &lt;br /&gt;
MAGIC ITEM  LORE ITEM  NO DROP&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: PRIMARY&amp;lt;br&amp;gt;&lt;br /&gt;
Skill: 2H Blunt  Atk Delay: 40&amp;lt;br&amp;gt; &lt;br /&gt;
DMG: 33&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 25&amp;lt;br&amp;gt;&lt;br /&gt;
Mana: 75&amp;lt;br&amp;gt;&lt;br /&gt;
STR: +10  STA: +10 WIS: +15&amp;lt;br&amp;gt;&lt;br /&gt;
Effect:  [[Blessing of the Lord Commander]] (Must Equip)&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;amp;nbsp; Casting Time: Instant&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;amp;nbsp; Required Level: 49&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;amp;nbsp; Cooldown: 600 seconds&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 8.0  Size: MEDIUM&amp;lt;br&amp;gt;&lt;br /&gt;
Class: CLR&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
&lt;br /&gt;
* [[Cleric Plane of Sky Tests|Cleric Test of The Weak]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Cleric Equipment]]&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:2H Blunt]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Azarack_Skin_Wristwraps&amp;diff=149075</id>
		<title>Azarack Skin Wristwraps</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Azarack_Skin_Wristwraps&amp;diff=149075"/>
		<updated>2026-06-04T01:01:15Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Sky Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = This item is the result of a quest.&lt;br /&gt;
|itemname    = Azarack Skin Wristwraps&lt;br /&gt;
|lucy_img_ID = 637&lt;br /&gt;
|statsblock  = &lt;br /&gt;
No Trade&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BST&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Wrist&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 5 HP: 35 ENDUR: 10&lt;br /&gt;
STR: 5 STA: 5 DEX: 5&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 1.0  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Click Effect: [[Whirl Bolt]] (Must Equip) - Cast Time: 1.0 seconds, Required Level: 46, Cooldown: 240 seconds&lt;br /&gt;
|relatedquests =&lt;br /&gt;
&lt;br /&gt;
* [[Beastlord Plane of Sky Tests]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Beastlord Equipment]]&lt;br /&gt;
[[Category:Wrist]]&lt;br /&gt;
[[Category:Plane of Sky]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Kerasian_Axe_of_Ire&amp;diff=149074</id>
		<title>Kerasian Axe of Ire</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Kerasian_Axe_of_Ire&amp;diff=149074"/>
		<updated>2026-06-04T01:01:15Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:sss.png|200px|thumb|&#039;&#039;&#039;Kerasian Axe of Ire&#039;&#039;&#039;]]&lt;br /&gt;
{{EpicQuests Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = {{Item Lore|Kerasian Axe of Ire}}&lt;br /&gt;
|itemname    = Kerasian Axe of Ire&lt;br /&gt;
|lucy_img_ID = 1455&lt;br /&gt;
|statsblock  = &lt;br /&gt;
MAGIC ITEM  LORE ITEM  NO DROP  &amp;lt;br&amp;gt;&lt;br /&gt;
Slot: PRIMARY &amp;lt;br&amp;gt;&lt;br /&gt;
Skill: 2H Slashing  Atk Delay: 37&amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 46 AC: 5 &amp;lt;br&amp;gt;&lt;br /&gt;
TR: +15  DEX: +15  STA: +20  HP: +125 ENDUR: +100&amp;lt;br&amp;gt;&lt;br /&gt;
SV FIRE: +8  SV DISEASE: +10  SV COLD: +15  SV MAGIC: +7  SV POISON: +10 Attack: +40&amp;lt;br&amp;gt;&lt;br /&gt;
Haste: 40% &amp;lt;br&amp;gt;&lt;br /&gt;
Required level of 46.&amp;lt;br&amp;gt;&lt;br /&gt;
Effect:  [[Rage of Keras|&amp;lt;span class=&#039;itemeff&#039;&amp;gt;Kerasian Axe of Ire&amp;lt;/span&amp;gt;]] (Worn) at Level 46&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 6.0  Size: TINY&amp;lt;br&amp;gt;&lt;br /&gt;
Class: BER&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
|relatedquests = &lt;br /&gt;
&lt;br /&gt;
* [[Berserker Epic Quest]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:2H Slashing]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Boots_of_the_Long_Road&amp;diff=149073</id>
		<title>Boots of the Long Road</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Boots_of_the_Long_Road&amp;diff=149073"/>
		<updated>2026-06-04T01:01:14Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Lore: Good enough for one more adventure &amp;lt;br&amp;gt;&lt;br /&gt;
Note: This item can be augmented (not currently displayed here)&lt;br /&gt;
|itemname    = Boots of the Long Road&lt;br /&gt;
|lucy_img_ID = 633&lt;br /&gt;
|statsblock  =&lt;br /&gt;
No Trade  &amp;lt;br&amp;gt; &lt;br /&gt;
Slot: FEET&amp;lt;br&amp;gt;&lt;br /&gt;
AC: 4&amp;lt;br&amp;gt;&lt;br /&gt;
HP: 5&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 1.5  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Class: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Click Effect: Spirit of the Traveler (Must Equip)&lt;br /&gt;
Slot: Ornamentation: empty&amp;lt;br&amp;gt;&lt;br /&gt;
Cast Time: Instant&amp;lt;br&amp;gt;&lt;br /&gt;
Cooldown: 10 seconds&lt;br /&gt;
|dropsfrom = None&lt;br /&gt;
&lt;br /&gt;
|soldby = &lt;br /&gt;
&lt;br /&gt;
{{ItemWhereTable|&lt;br /&gt;
{{ItemWhereRow   | [[Northern Felwithe]]                      | [[Lanadin_(Adventuring_Supplies)|Lanadin (Adventuring Supplies)]]       |               | (35, -15) }}&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
| relatedquests =&lt;br /&gt;
&lt;br /&gt;
* Tutorial reward&lt;br /&gt;
&lt;br /&gt;
| playercrafted =&lt;br /&gt;
&lt;br /&gt;
* None&lt;br /&gt;
&lt;br /&gt;
| recipes = &lt;br /&gt;
&lt;br /&gt;
* None&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Bard Equipment]]&lt;br /&gt;
[[Category:Beastlord Equipment]]&lt;br /&gt;
[[Category:Berserker Equipment]]&lt;br /&gt;
[[Category:Cleric Equipment]]&lt;br /&gt;
[[Category:Druid Equipment]]&lt;br /&gt;
[[Category:Enchanter Equipment]]&lt;br /&gt;
[[Category:Magician Equipment]]&lt;br /&gt;
[[Category:Monk Equipment]]&lt;br /&gt;
[[Category:Necromancer Equipment]]&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
[[Category:Shadow Knight Equipment]]&lt;br /&gt;
[[Category:Shaman Equipment]]&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Wizard Equipment]]&lt;br /&gt;
[[Category:Feet]]&lt;br /&gt;
[[Category:Vendor Sold]]&lt;br /&gt;
[[Category:Fashion: Leather]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Skeletal_Rod&amp;diff=149072</id>
		<title>Skeletal Rod</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Skeletal_Rod&amp;diff=149072"/>
		<updated>2026-06-04T01:01:14Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       =&lt;br /&gt;
|merchant_value = 0pp&lt;br /&gt;
|itemname    = Skeletal Rod&lt;br /&gt;
|lucy_img_ID = 84216&lt;br /&gt;
|statsblock  = &lt;br /&gt;
Slot: PRIMARY SECONDARY&amp;lt;br&amp;gt;&lt;br /&gt;
Skill: 1H Blunt  Atk Delay: 27&amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 4 &amp;lt;br&amp;gt;&lt;br /&gt;
WT: 4.9  Size: MEDIUM&amp;lt;br&amp;gt;&lt;br /&gt;
Class: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom =&lt;br /&gt;
[[Befallen]]&lt;br /&gt;
* [[Skeleton Lrodd]]&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Bard Equipment]]&lt;br /&gt;
[[Category:Cleric Equipment]]&lt;br /&gt;
[[Category:Druid Equipment]]&lt;br /&gt;
[[Category:Monk Equipment]]&lt;br /&gt;
[[Category:Paladin Equipment]]&lt;br /&gt;
[[Category:Ranger Equipment]]&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
[[Category:Shaman Equipment]]&lt;br /&gt;
[[Category:Shadow Knight Equipment]]&lt;br /&gt;
[[Category:Warrior Equipment]]&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:Secondary]]&lt;br /&gt;
[[Category:1H Blunt]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Zimbittle%27s_Pouch&amp;diff=149071</id>
		<title>Zimbittle&#039;s Pouch</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Zimbittle%27s_Pouch&amp;diff=149071"/>
		<updated>2026-06-04T01:01:14Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Zimbittle&#039;s Pouch&lt;br /&gt;
|lucy_img_ID = 539&lt;br /&gt;
|statsblock  = &lt;br /&gt;
WT: 1.0  Weight Reduction: 0%&amp;lt;br&amp;gt;&lt;br /&gt;
Capacity: 4  Size Capacity: MEDIUM&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Innothule Swamp]]&lt;br /&gt;
&lt;br /&gt;
* [[Zimbittle]]&lt;br /&gt;
&lt;br /&gt;
|relatedquests = &lt;br /&gt;
&lt;br /&gt;
* [[Marda&#039;s Secret Mission]]&lt;br /&gt;
&lt;br /&gt;
|soldby = &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Containers]]&lt;br /&gt;
[[Category:Inventory Items]]&lt;br /&gt;
[[Category:Vendor Sold]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Zeriphra%27s_Dagger_of_Blood&amp;diff=149070</id>
		<title>Zeriphra&#039;s Dagger of Blood</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Zeriphra%27s_Dagger_of_Blood&amp;diff=149070"/>
		<updated>2026-06-04T01:01:14Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Velious Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Interim Rogue Dagger Drop from [[Vulak`Aerr]].  It replaces {{:Mrylokar&#039;s Dagger of Blood}} and then is replaced by {{:Mrylokar&#039;s Dagger of Vengeance}}.&lt;br /&gt;
|itemname    = Zeriphra&#039;s Dagger of Blood&lt;br /&gt;
|lucy_img_ID = 592&lt;br /&gt;
|statsblock  = &lt;br /&gt;
MAGIC ITEM  LORE ITEM  NO DROP  &amp;lt;br&amp;gt;&lt;br /&gt;
Slot: RANGE PRIMARY SECONDARY&amp;lt;br&amp;gt;&lt;br /&gt;
Skill: Piercing  Atk Delay: 17&amp;lt;br&amp;gt;&lt;br /&gt;
DMG: 11&amp;lt;br&amp;gt;&lt;br /&gt;
STR: +15  DEX: +10  HP: +70&amp;lt;br&amp;gt;&lt;br /&gt;
SV FIRE: +10  SV DISEASE: +10  SV COLD: +10  SV MAGIC: +10  SV POISON: +10&amp;lt;br&amp;gt;&lt;br /&gt;
Effect: [[Feast of Blood]] (Combat)&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 1.0  Size: TINY&amp;lt;br&amp;gt;&lt;br /&gt;
Class: ROG&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Temple of Veeshan]]&lt;br /&gt;
&lt;br /&gt;
* [[Vulak`Aerr]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Rogue Equipment]]&lt;br /&gt;
[[Category:Primary]]&lt;br /&gt;
[[Category:Secondary]]&lt;br /&gt;
[[Category:Range]]&lt;br /&gt;
[[Category:Piercing]]&lt;br /&gt;
[[Category:Temple of Veeshan]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Yvonne%27s_Guide_to_Alchemy&amp;diff=149069</id>
		<title>Yvonne&#039;s Guide to Alchemy</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Yvonne%27s_Guide_to_Alchemy&amp;diff=149069"/>
		<updated>2026-06-04T01:01:13Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Yvonne&#039;s Guide to Alchemy&lt;br /&gt;
|lucy_img_ID = 778&lt;br /&gt;
|statsblock  = &lt;br /&gt;
WT: 1.0  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
|soldby = &lt;br /&gt;
&lt;br /&gt;
{{ItemWhereTable|&lt;br /&gt;
{{ItemWhereRow   | [[Cabilis|West Cabilis]]    | [[Jaxxtz]]              |                           | (-41, 513)  }}&lt;br /&gt;
{{ItemWhereRow   | [[Paineel]]                 | [[Yvonne Ani]]          | (Apparently Not on p99)   | (944, 753)  }}&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
|bookcontents = &lt;br /&gt;
&lt;br /&gt;
Herbs, and their uses.&lt;br /&gt;
&lt;br /&gt;
Many do not believe in the power of the herb, just as I have no tolerance for the ignorant. Here are some of the more common herbs and the uses I have found for them. Perhaps by testing things out for yourself you can come up with some useful concoctions.&lt;br /&gt;
&lt;br /&gt;
Lucerne: Years ago my brother Piter very weak from practicing his summons. A little of this fixed him right up.&lt;br /&gt;
&lt;br /&gt;
Sage Leaf: I have found this to be useful for keeping your wits about you. I put this in tea for the nighttime patrolling guards in Tox, keeps them awake.&lt;br /&gt;
&lt;br /&gt;
Birthwart: Often given to babies with ear infections, this herb keeps the inner ear fluids in proper order.&lt;br /&gt;
&lt;br /&gt;
Fenugreek: This herb has many uses. It sharpens reflexes and improves hand-eye coordination. I often give it to the young Shadow Knights when they are testing to become true seekers of the dark truth.&lt;br /&gt;
&lt;br /&gt;
Maidenhair Fern: This little fern seems to have the most wonderful effects with ones emotions and I often use it for making my secret love potions.&lt;br /&gt;
&lt;br /&gt;
Mandrake Root: This is a very potent herb. It is said that even the essence of magic itself shies away from the extract of this root.&lt;br /&gt;
&lt;br /&gt;
Allspice: I have used this herb more times then you can imagine. Whenever I burn myself on the side of my cauldron I use Allspice extract to soothe the burn and quell the pain.&lt;br /&gt;
&lt;br /&gt;
Benzoin: I don&#039;t know for sure but this herb is rumored to actually raise the body temperature and is used to treat frostbite.&lt;br /&gt;
&lt;br /&gt;
Nightshade: Although poisonous by nature, if used correctly it can actually prevent the effects of most poisons.&lt;br /&gt;
&lt;br /&gt;
Mullein: Many people hang this herb above their beds. It is believed to keep the minions of Bertoxxulous away.&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Inventory Items]]&lt;br /&gt;
[[Category:Vendor Sold]]&lt;br /&gt;
[[Category:Book]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Young_Ebon_Drake_Bile&amp;diff=149068</id>
		<title>Young Ebon Drake Bile</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Young_Ebon_Drake_Bile&amp;diff=149068"/>
		<updated>2026-06-04T01:01:13Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{delete}}{{DNE}}&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Confirmed to exist on p99? No known classic use. Used in May2002 Ak&#039;Anon armor quests[https://web.archive.org/web/20020703222318/http://eqbeastiary.allakhazam.com/search.shtml?id=8809].&lt;br /&gt;
|itemname    = Young Ebon Drake Bile&lt;br /&gt;
|lucy_img_ID = 1003&lt;br /&gt;
|statsblock  = &lt;br /&gt;
NO DROP&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.1  Size: TINY&amp;lt;br&amp;gt;&lt;br /&gt;
Class: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|dropsfrom = &lt;br /&gt;
&lt;br /&gt;
[[Steamfont Mountains]]&lt;br /&gt;
&lt;br /&gt;
* [[a lesser ebon drake]]&lt;br /&gt;
* [[a young ebon drake]]&lt;br /&gt;
* [[an ebon drakeling]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Inventory Items]]&lt;br /&gt;
[[Category:Steamfont Mountains]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Yellow_Dye&amp;diff=149067</id>
		<title>Yellow Dye</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Yellow_Dye&amp;diff=149067"/>
		<updated>2026-06-04T01:01:13Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Yellow Dye&lt;br /&gt;
|lucy_img_ID = 1158&lt;br /&gt;
|statsblock  = &lt;br /&gt;
WT: 0.1  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Class: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|playercrafted = &lt;br /&gt;
&lt;br /&gt;
* [[Non-Tradeskill]] (Trivial: ?)&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Inventory Items]]&lt;br /&gt;
[[Category:Player Crafted]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Yelesom%27s_Tools&amp;diff=149066</id>
		<title>Yelesom&#039;s Tools</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Yelesom%27s_Tools&amp;diff=149066"/>
		<updated>2026-06-04T01:01:13Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Yelesom&#039;s Tools&lt;br /&gt;
|lucy_img_ID = 716&lt;br /&gt;
|statsblock  = &lt;br /&gt;
MAGIC ITEM LORE ITEM NO TRADE&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 25.5  Size: LARGE&amp;lt;br&amp;gt;&lt;br /&gt;
Class: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
|relatedquests = &lt;br /&gt;
&lt;br /&gt;
* [[The Geologist&#039;s Purloined Tools]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|playercrafted = &lt;br /&gt;
&lt;br /&gt;
* [[Non-Tradeskill]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Inventory Items]]&lt;br /&gt;
[[Category:Player Crafted]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Yelesom%27s_Reports&amp;diff=149065</id>
		<title>Yelesom&#039;s Reports</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Yelesom%27s_Reports&amp;diff=149065"/>
		<updated>2026-06-04T01:01:12Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Classic Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Yelesom&#039;s Reports&lt;br /&gt;
|lucy_img_ID = 865&lt;br /&gt;
|statsblock  = &lt;br /&gt;
LORE ITEM  NO DROP&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 1.0  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Class: None&amp;lt;br&amp;gt;&lt;br /&gt;
Race: None&amp;lt;br&amp;gt;&lt;br /&gt;
|relatedquests = &lt;br /&gt;
&lt;br /&gt;
* [[The Geologist&#039;s Purloined Tools]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Inventory Items]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quest Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Wurm_Egg&amp;diff=149064</id>
		<title>Wurm Egg</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Wurm_Egg&amp;diff=149064"/>
		<updated>2026-06-04T01:01:12Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Velious Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Wurm Egg&lt;br /&gt;
|lucy_img_ID = 969a&lt;br /&gt;
|statsblock  = &lt;br /&gt;
This is a banquet sized meal!&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 0.6  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Class: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
|recipes = &lt;br /&gt;
&lt;br /&gt;
* [[Baking]]&lt;br /&gt;
[[Clump of Dough]]&lt;br /&gt;
&lt;br /&gt;
|foraged = &lt;br /&gt;
&lt;br /&gt;
* [[Skyshrine]]&lt;br /&gt;
* [[Temple of Veeshan]]&lt;br /&gt;
* [[Western Wastes]]&lt;br /&gt;
&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Inventory Items]]&lt;br /&gt;
[[Category:Foraged]]&lt;br /&gt;
[[Category:Skyshrine]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Wulf_Fish&amp;diff=149063</id>
		<title>Wulf Fish</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Wulf_Fish&amp;diff=149063"/>
		<updated>2026-06-04T01:01:12Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kunark Era}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = &lt;br /&gt;
|itemname    = Wulf Fish&lt;br /&gt;
|lucy_img_ID = 775&lt;br /&gt;
|statsblock  = &lt;br /&gt;
This is a meal!&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 1.0  Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Class: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
Race: ALL&amp;lt;br&amp;gt;&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Inventory Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://eqlwiki.com/index.php?title=Writ_of_Innoruuk&amp;diff=149062</id>
		<title>Writ of Innoruuk</title>
		<link rel="alternate" type="text/html" href="https://eqlwiki.com/index.php?title=Writ_of_Innoruuk&amp;diff=149062"/>
		<updated>2026-06-04T01:01:12Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Maintenance: migrate item page to single Itempage transclusion format&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{:Does Not Exist}}{{Nerfed}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;onlyinclude&amp;gt;{{Itempage&lt;br /&gt;
|notes       = Obtained during a GM event.&lt;br /&gt;
|itemname    = Writ of Innoruuk&lt;br /&gt;
|lucy_img_ID = 789&lt;br /&gt;
|statsblock  = &lt;br /&gt;
MAGIC ITEM LORE ITEM&amp;lt;br&amp;gt;&lt;br /&gt;
Slot: PRIMARY&amp;lt;br&amp;gt;&lt;br /&gt;
WIS: +15 INT: +10&amp;lt;br&amp;gt;&lt;br /&gt;
SV FIRE: +5 SV DISEASE: +5 SV COLD: +5 SV MAGIC: +5 SV POISON: +5&amp;lt;br&amp;gt;&lt;br /&gt;
WT: 5.0 Size: SMALL&amp;lt;br&amp;gt;&lt;br /&gt;
Class: CLR SHM NEC WIZ MAG ENC&amp;lt;br&amp;gt;&lt;br /&gt;
Race: HUM DEF TRL OGR&amp;lt;br&amp;gt;&lt;br /&gt;
|gmitem = &lt;br /&gt;
&lt;br /&gt;
* Given out in a GM event.&lt;br /&gt;
}}&amp;lt;/onlyinclude&amp;gt;&lt;br /&gt;
[[Category:Inventory Items]]&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
</feed>