diff --git a/html-api-debugger/byte-preview.mjs b/html-api-debugger/byte-preview.mjs
new file mode 100644
index 0000000..4044d63
--- /dev/null
+++ b/html-api-debugger/byte-preview.mjs
@@ -0,0 +1,808 @@
+/**
+ * Own exact-byte iframe document URLs.
+ */
+export class ByteDocumentPreview {
+ /**
+ * @param {{src: string}} iframe Iframe-like navigation target.
+ * @param {{createObjectURL: (blob: Blob) => string, revokeObjectURL: (url: string) => void}} [urlApi=URL] Object URL API.
+ * @param {typeof Blob} [BlobConstructor=Blob] Blob constructor.
+ */
+ constructor( iframe, urlApi = URL, BlobConstructor = Blob ) {
+ this.iframe = iframe;
+ this.urlApi = urlApi;
+ this.BlobConstructor = BlobConstructor;
+ /** @type {string|null} */
+ this.currentUrl = null;
+ }
+
+ /**
+ * Navigate to a new UTF-8 HTML document made from exact bytes.
+ *
+ * @param {Uint8Array} bytes Exact document bytes.
+ * @returns {string} New object URL.
+ */
+ load( bytes ) {
+ if ( ! ( bytes instanceof Uint8Array ) ) {
+ throw new TypeError( 'Expected a Uint8Array.' );
+ }
+
+ const buffer = new ArrayBuffer( bytes.byteLength );
+ new Uint8Array( buffer ).set( bytes );
+ const blob = new this.BlobConstructor( [ buffer ], {
+ type: 'text/html;charset=utf-8',
+ } );
+ const nextUrl = this.urlApi.createObjectURL( blob );
+
+ try {
+ this.iframe.src = nextUrl;
+ } catch ( error ) {
+ this.urlApi.revokeObjectURL( nextUrl );
+ throw error;
+ }
+
+ const supersededUrl = this.currentUrl;
+ this.currentUrl = nextUrl;
+ if ( supersededUrl !== null ) {
+ this.urlApi.revokeObjectURL( supersededUrl );
+ }
+
+ return nextUrl;
+ }
+
+ /**
+ * Determine whether a load event belongs to the current document.
+ *
+ * @param {string} url Loaded URL.
+ * @returns {boolean} Whether the URL is current.
+ */
+ isCurrent( url ) {
+ return this.currentUrl !== null && url === this.currentUrl;
+ }
+
+ /** Revoke the final owned URL. Safe to call repeatedly. */
+ dispose() {
+ if ( this.currentUrl === null ) {
+ return;
+ }
+ const finalUrl = this.currentUrl;
+ this.currentUrl = null;
+ this.urlApi.revokeObjectURL( finalUrl );
+ }
+}
+
+/**
+ * Split bytes at an HTML API byte span.
+ *
+ * @param {Uint8Array} bytes Exact source bytes.
+ * @param {number} start Byte offset.
+ * @param {number} length Byte length.
+ * @returns {{before: Uint8Array, current: Uint8Array, after: Uint8Array}} Byte slices.
+ */
+export function splitByteSpan( bytes, start, length ) {
+ if ( ! ( bytes instanceof Uint8Array ) ) {
+ throw new TypeError( 'Expected a Uint8Array.' );
+ }
+ if (
+ ! Number.isFinite( start ) ||
+ ! Number.isInteger( start ) ||
+ ! Number.isFinite( length ) ||
+ ! Number.isInteger( length )
+ ) {
+ throw new TypeError( 'Byte span offsets must be finite integers.' );
+ }
+ if ( start < 0 || length < 0 || start > bytes.length - length ) {
+ throw new RangeError( 'Byte span falls outside the source bytes.' );
+ }
+
+ const end = start + length;
+ return {
+ before: bytes.subarray( 0, start ),
+ current: bytes.subarray( start, end ),
+ after: bytes.subarray( end ),
+ };
+}
+
+/**
+ * Resolve the element whose native `innerHTML` setter parses a fragment.
+ *
+ * The parsed context document usually exposes its authored final element via
+ * the tree. Empty HEAD, BODY, and HTML contexts need the original authored
+ * context projection to distinguish the browser-created empty elements.
+ *
+ * @param {Document} document Parsed exact-byte context document.
+ * @param {string} contextText Safe Unicode projection of the context bytes.
+ * @returns {Element} Native fragment parsing context.
+ */
+export function resolveFragmentTarget( document, contextText ) {
+ if ( typeof contextText !== 'string' ) {
+ throw new TypeError( 'Fragment context text must be a string.' );
+ }
+
+ const authored = findAuthoredDocumentRoots( contextText );
+ const body = document.body;
+ const head = document.head;
+ if ( body?.localName === 'frameset' ) {
+ return lastElementDescendant( body );
+ }
+ if ( body !== null && ( authored.body || body.hasChildNodes() ) ) {
+ return lastElementDescendant( body );
+ }
+ if ( head !== null && ( authored.head || head.hasChildNodes() ) ) {
+ return lastElementDescendant( head );
+ }
+ return document.documentElement;
+}
+
+const ASCII_WHITESPACE = new Set( [ '\t', '\n', '\f', '\r', ' ' ] );
+const RAW_TEXT_ELEMENTS = new Set( [
+ 'iframe',
+ 'noembed',
+ 'noframes',
+ 'style',
+ 'xmp',
+] );
+const RCDATA_ELEMENTS = new Set( [ 'textarea', 'title' ] );
+
+/** @param {string} character */
+function isAsciiAlpha( character ) {
+ return /^[A-Za-z]$/u.test( character );
+}
+
+/** @param {string} character */
+function isTagDelimiter( character ) {
+ return character === '>' || character === '/' || ASCII_WHITESPACE.has( character );
+}
+
+/**
+ * Read a start or end tag through its closing angle bracket.
+ *
+ * @param {string} source Source text.
+ * @param {number} position Position immediately after `<` or ``.
+ * @returns {{name: string, end: number}|null} Token boundary, if complete.
+ */
+function readTag( source, position ) {
+ if ( ! isAsciiAlpha( source[ position ] ?? '' ) ) {
+ return null;
+ }
+
+ const nameStart = position;
+ while (
+ position < source.length &&
+ ! isTagDelimiter( source.charAt( position ) )
+ ) {
+ ++position;
+ }
+ const name = source.slice( nameStart, position ).toLowerCase();
+ let state = 'before-attribute-name';
+ while ( position < source.length ) {
+ const character = source.charAt( position );
+ switch ( state ) {
+ case 'before-attribute-name':
+ if ( ASCII_WHITESPACE.has( character ) ) {
+ ++position;
+ } else if ( character === '/' ) {
+ state = 'self-closing';
+ ++position;
+ } else if ( character === '>' ) {
+ return { name, end: position + 1 };
+ } else if ( character === '=' ) {
+ state = 'attribute-name';
+ ++position;
+ } else {
+ state = 'attribute-name';
+ }
+ break;
+ case 'attribute-name':
+ if ( ASCII_WHITESPACE.has( character ) ) {
+ state = 'after-attribute-name';
+ ++position;
+ } else if ( character === '/' ) {
+ state = 'self-closing';
+ ++position;
+ } else if ( character === '=' ) {
+ state = 'before-attribute-value';
+ ++position;
+ } else if ( character === '>' ) {
+ return { name, end: position + 1 };
+ } else {
+ ++position;
+ }
+ break;
+ case 'after-attribute-name':
+ if ( ASCII_WHITESPACE.has( character ) ) {
+ ++position;
+ } else if ( character === '/' ) {
+ state = 'self-closing';
+ ++position;
+ } else if ( character === '=' ) {
+ state = 'before-attribute-value';
+ ++position;
+ } else if ( character === '>' ) {
+ return { name, end: position + 1 };
+ } else {
+ state = 'attribute-name';
+ }
+ break;
+ case 'before-attribute-value':
+ if ( ASCII_WHITESPACE.has( character ) ) {
+ ++position;
+ } else if ( character === '"' ) {
+ state = 'double-quoted-attribute-value';
+ ++position;
+ } else if ( character === "'" ) {
+ state = 'single-quoted-attribute-value';
+ ++position;
+ } else if ( character === '>' ) {
+ return { name, end: position + 1 };
+ } else {
+ state = 'unquoted-attribute-value';
+ }
+ break;
+ case 'double-quoted-attribute-value':
+ if ( character === '"' ) {
+ state = 'after-quoted-attribute-value';
+ }
+ ++position;
+ break;
+ case 'single-quoted-attribute-value':
+ if ( character === "'" ) {
+ state = 'after-quoted-attribute-value';
+ }
+ ++position;
+ break;
+ case 'unquoted-attribute-value':
+ if ( ASCII_WHITESPACE.has( character ) ) {
+ state = 'before-attribute-name';
+ ++position;
+ } else if ( character === '>' ) {
+ return { name, end: position + 1 };
+ } else {
+ ++position;
+ }
+ break;
+ case 'after-quoted-attribute-value':
+ if ( ASCII_WHITESPACE.has( character ) ) {
+ state = 'before-attribute-name';
+ ++position;
+ } else if ( character === '/' ) {
+ state = 'self-closing';
+ ++position;
+ } else if ( character === '>' ) {
+ return { name, end: position + 1 };
+ } else {
+ state = 'before-attribute-name';
+ }
+ break;
+ case 'self-closing':
+ if ( character === '>' ) {
+ return { name, end: position + 1 };
+ }
+ state = 'before-attribute-name';
+ break;
+ }
+ }
+ return null;
+}
+
+/** @param {string} source @param {number} position */
+function consumeBogusComment( source, position ) {
+ const end = source.indexOf( '>', position );
+ return end === -1 ? source.length : end + 1;
+}
+
+/**
+ * Consume an HTML comment using the tokenizer's abrupt and nested end states.
+ *
+ * @param {string} source Source text.
+ * @param {number} position Position immediately after `
x',
+ ),
+ titledHead.children[ 0 ],
+ 'comment text cannot author BODY',
+);
+assert.equal(
+ resolveFragmentTarget(
+ /** @type {any} */ ( titledDocument ),
+ 'x',
+ ),
+ titledDocument.body,
+ 'an explicit empty BODY outranks a populated HEAD',
+);
+assert.equal(
+ resolveFragmentTarget(
+ /** @type {any} */ ( titledDocument ),
+ '',
+ ),
+ titledHead.children[ 0 ],
+ 'an incomplete appropriate RCDATA end tag consumes through EOF',
+);
+
+for ( const context of [
+ '',
+ '',
+ '',
+ '
',
+ '
',
+ '',
+ '',
+] ) {
+ assert.equal(
+ resolveFragmentTarget( /** @type {any} */ ( emptyContextDocument ), context ),
+ emptyContextDocument.documentElement,
+ `${ context } cannot falsely author BODY`,
+ );
+}
+
+assert.equal(
+ resolveFragmentTarget(
+ /** @type {any} */ ( emptyContextDocument ),
+ '',
+ ),
+ emptyContextDocument.body,
+ 'an abrupt empty comment exposes the following authored BODY',
+);
+assert.equal(
+ resolveFragmentTarget(
+ /** @type {any} */ ( emptyContextDocument ),
+ '