yukey

format: one dprint pass over the carried files

commit beb8a3a130857eb1ff202c248c3458e540d80e56 · parent 56b0416af1 · browse this tree
Ximianth, 2026-10-10 05:16:51 UTC

No semantic change intended; unit tests (348) and the four e2e scenarios report the same counts as before.
This is the commit that makes the pre-commit hook pass on the full tree.
diff --git a/packages/e2e/harness.ts b/packages/e2e/harness.ts
index 2a50241..abf1dfc 100644
--- a/packages/e2e/harness.ts
+++ b/packages/e2e/harness.ts
@@ -107,8 +107,8 @@ export class Harness {
 			accessSync(CHROME_BIN, constants.X_OK);
 		} catch {
 			throw new Error(
-				`chrome binary missing at ${CHROME_BIN}; ` +
-					"install headless-shell there, or point YUKEY_E2E_CHROME at a chromium binary",
+				`chrome binary missing at ${CHROME_BIN}; `
+					+ "install headless-shell there, or point YUKEY_E2E_CHROME at a chromium binary",
 			);
 		}
 
diff --git a/packages/e2e/scenarios/ip-drive.ts b/packages/e2e/scenarios/ip-drive.ts
index e3e93db..cfb64b0 100644
--- a/packages/e2e/scenarios/ip-drive.ts
+++ b/packages/e2e/scenarios/ip-drive.ts
@@ -72,8 +72,7 @@ async function run(ctx: ScenarioContext): Promise<void> {
 		return JSON.parse(box as string);
 	};
 
-	const COL =
-		"document.querySelector('sw-gallery').shadowRoot.querySelector('swg-infopop-column').shadowRoot";
+	const COL = "document.querySelector('sw-gallery').shadowRoot.querySelector('swg-infopop-column').shadowRoot";
 
 	await openGallery(
 		ctx,
@@ -272,8 +271,7 @@ async function run(ctx: ScenarioContext): Promise<void> {
 	check("pointerdown-closes", s.mode === "closed", JSON.stringify(s.mode));
 
 	// --- edges: clamping and the flip ---
-	const EDGES =
-		"document.querySelector('sw-gallery').shadowRoot.querySelector('swg-infopop-edges').shadowRoot";
+	const EDGES = "document.querySelector('sw-gallery').shadowRoot.querySelector('swg-infopop-edges').shadowRoot";
 	pt = await centerOf(`${EDGES}.querySelectorAll('sw-button')[1]`); // far right
 	await cdp.mouseMove(pt.x, pt.y);
 	await sleep(500);
diff --git a/packages/e2e/scenarios/lc-drive.ts b/packages/e2e/scenarios/lc-drive.ts
index fc4cbf4..1cd0c1c 100644
--- a/packages/e2e/scenarios/lc-drive.ts
+++ b/packages/e2e/scenarios/lc-drive.ts
@@ -118,8 +118,7 @@ async function run(ctx: ScenarioContext): Promise<void> {
 	// Determinism: stop both demos' own churn timers; the scenario churns by hand.
 	await cdp.js(`clearInterval(${demo(0)}.timer); clearInterval(${demo(1)}.timer)`);
 
-	const st = async (i = 0): Promise<LcState> =>
-		JSON.parse((await cdp.js(stateJs(demo(i)))) as string);
+	const st = async (i = 0): Promise<LcState> => JSON.parse((await cdp.js(stateJs(demo(i)))) as string);
 	const row = (s: LcState, key: string): LcRow | null => s.rows.find((r) => r.key === key) ?? null;
 
 	// Data-side churn, like the real queue does between renders.
@@ -795,10 +794,10 @@ async function run(ctx: ScenarioContext): Promise<void> {
 	s1 = await st(1);
 	check(
 		"commit-flows-after-refusal",
-		!s1.textMode &&
-			s1.inputValue === "" &&
-			s1.dataIds.length === countBefore10 + 1 &&
-			s1.log.includes("rejected draft appended"),
+		!s1.textMode
+			&& s1.inputValue === ""
+			&& s1.dataIds.length === countBefore10 + 1
+			&& s1.log.includes("rejected draft appended"),
 		JSON.stringify([s1.textMode, s1.inputValue, s1.dataIds, s1.log]),
 	);
 }
diff --git a/packages/e2e/scenarios/mc-drive.ts b/packages/e2e/scenarios/mc-drive.ts
index 2776940..c89aa70 100644
--- a/packages/e2e/scenarios/mc-drive.ts
+++ b/packages/e2e/scenarios/mc-drive.ts
@@ -335,7 +335,7 @@ async function run(ctx: ScenarioContext): Promise<void> {
 
 	// Click the beacon row (a plain activatable row: earlier Enter and Space
 	// left its counter at 2, so a working click reads 3).
-	let pt = await portCenter('[data-mc-row="beacon"]');
+	let pt = await portCenter("[data-mc-row=\"beacon\"]");
 	await cdp.click(pt.x, pt.y);
 	s = await st();
 	port = s.columns[0] as McColumn;
@@ -344,7 +344,7 @@ async function run(ctx: ScenarioContext): Promise<void> {
 
 	// Click the surviving crate's aux ×: the synthesis carries the aux position,
 	// so the click removes exactly as Enter-on-aux does.
-	pt = await portCenter('[data-mc-row="crate-cells"] [data-mc-aux]');
+	pt = await portCenter("[data-mc-row=\"crate-cells\"] [data-mc-aux]");
 	await cdp.click(pt.x, pt.y);
 	s = await st();
 	port = s.columns[0] as McColumn;
diff --git a/packages/e2e/scenarios/sw-drive.ts b/packages/e2e/scenarios/sw-drive.ts
index 0471c0f..8ede259 100644
--- a/packages/e2e/scenarios/sw-drive.ts
+++ b/packages/e2e/scenarios/sw-drive.ts
@@ -157,8 +157,7 @@ async function run(ctx: ScenarioContext): Promise<void> {
 	);
 
 	// Mouse click on the dock button (trusted click via CDP).
-	const SA_ROOT =
-		'document.querySelector("sw-gallery").shadowRoot.querySelector("swg-button-standalone")';
+	const SA_ROOT = "document.querySelector(\"sw-gallery\").shadowRoot.querySelector(\"swg-button-standalone\")";
 	let pt = await centerOf(
 		`[...${SA_ROOT}.shadowRoot.querySelectorAll("sw-button")].find(x => x.label === "Dock at station")`,
 	);
@@ -223,8 +222,7 @@ async function run(ctx: ScenarioContext): Promise<void> {
 	await cdp.key("ArrowDown");
 	s = await st();
 	check("disabled-row-focusable", ops(s).rowFocused === "purge", JSON.stringify(ops(s)));
-	const MAIN_SCROLL =
-		"document.querySelector('sw-gallery').shadowRoot.querySelector('main').scrollTop";
+	const MAIN_SCROLL = "document.querySelector('sw-gallery').shadowRoot.querySelector('main').scrollTop";
 	const before = await cdp.js(MAIN_SCROLL);
 	await cdp.key(" ");
 	s = await st();
@@ -447,8 +445,9 @@ async function run(ctx: ScenarioContext): Promise<void> {
 	};
 	const flashCleared = async (): Promise<boolean> => {
 		for (let i = 0; i < 30; i++) {
-			if ((await cdp.js("!window.__flash.el.classList.contains('sw-pressed')")) === true)
+			if ((await cdp.js("!window.__flash.el.classList.contains('sw-pressed')")) === true) {
 				return true;
+			}
 			await sleep(20);
 		}
 		return false;
@@ -488,8 +487,9 @@ async function run(ctx: ScenarioContext): Promise<void> {
 	for (let i = 0; i < 30; i++) {
 		if (
 			(await cdp.js(`(${SA_BUTTON("Dock at station")}).classList.contains('sw-pressed')`)) !== true
-		)
+		) {
 			break;
+		}
 		await sleep(20);
 	}
 
@@ -544,8 +544,9 @@ async function run(ctx: ScenarioContext): Promise<void> {
 			(await cdp.js(
 				"document.readyState === 'complete' && !!document.querySelector('sw-gallery')?.shadowRoot?.querySelector('swg-megacolumn-demo')",
 			)) === true
-		)
+		) {
 			break;
+		}
 		await sleep(100);
 	}
 	landed = false;
diff --git a/packages/yukey/src/adopt-into-root.ts b/packages/yukey/src/adopt-into-root.ts
index 2d05745..2ad1ec9 100644
--- a/packages/yukey/src/adopt-into-root.ts
+++ b/packages/yukey/src/adopt-into-root.ts
@@ -13,12 +13,14 @@
 import type { CSSResult } from "lit";
 
 export function adoptIntoRoot(root: Node, styles: CSSResult, marker: string): void {
-	if (!(root instanceof ShadowRoot || root instanceof Document))
+	if (!(root instanceof ShadowRoot || root instanceof Document)) {
 		throw new Error(`adopt-into-root (${marker}): not inside any document or shadow root`);
+	}
 	const sheet = styles.styleSheet;
 	if (sheet !== undefined) {
-		if (!root.adoptedStyleSheets.includes(sheet))
+		if (!root.adoptedStyleSheets.includes(sheet)) {
 			root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
+		}
 		return;
 	}
 	const holder = root instanceof Document ? root.head : root;
diff --git a/packages/yukey/src/focus-stack.ts b/packages/yukey/src/focus-stack.ts
index 5f2fcc2..e768222 100644
--- a/packages/yukey/src/focus-stack.ts
+++ b/packages/yukey/src/focus-stack.ts
@@ -119,8 +119,9 @@ export class FocusStack {
 	}
 
 	register(surface: FocusSurface): void {
-		if (this.#surfaces.has(surface.id))
+		if (this.#surfaces.has(surface.id)) {
 			throw new Error(`focus-stack: surface "${surface.id}" is already registered`);
+		}
 		this.#surfaces.set(surface.id, surface);
 	}
 
@@ -137,8 +138,9 @@ export class FocusStack {
 		const containing: { id: string; root: HTMLElement }[] = [];
 		for (const surface of this.#surfaces.values()) {
 			const root = this.#liveRoot(surface.id);
-			if (root !== null && composedContains(root, active))
+			if (root !== null && composedContains(root, active)) {
 				containing.push({ id: surface.id, root });
+			}
 		}
 		// Surfaces containing one focus point form a containment chain, so this
 		// pairwise comparator is a total order on them.
@@ -149,8 +151,9 @@ export class FocusStack {
 	// A user-opened surface is about to seat focus: record where focus came from.
 	// Call while focus still sits at the origin (before the seat moves it).
 	entered(id: string): void {
-		if (!this.#surfaces.has(id))
+		if (!this.#surfaces.has(id)) {
 			throw new Error(`focus-stack: entered("${id}") but no such surface is registered`);
+		}
 		const top = this.#entries[this.#entries.length - 1];
 		if (top?.entered === id) return; // replace-not-push: the journey origin holds
 		this.#entries = this.#entries.filter((e) => e.entered !== id); // one entry per id
@@ -186,10 +189,11 @@ export class FocusStack {
 			if (this.#focusSurface(candidate)) return;
 		}
 		const fallback = this.#fallback?.() ?? null;
-		if (fallback === null)
+		if (fallback === null) {
 			throw new Error(
 				`focus-stack: "${id}" closed holding focus with no live origin and no fallback`,
 			);
+		}
 		fallback.focus();
 	}
 
diff --git a/packages/yukey/src/gallery/sw-gallery.ts b/packages/yukey/src/gallery/sw-gallery.ts
index af62b8a..8e3bbb8 100644
--- a/packages/yukey/src/gallery/sw-gallery.ts
+++ b/packages/yukey/src/gallery/sw-gallery.ts
@@ -89,7 +89,8 @@ export class SwGallery extends LitElement {
 		}
 	`;
 
-	@state() private currentId = "";
+	@state()
+	private currentId = "";
 
 	// Hash -> selection.  Bound once (constructor, not connectedCallback) because the
 	// shell lives for the page's whole life; there's nothing to unbind.  Tab is
@@ -126,11 +127,14 @@ export class SwGallery extends LitElement {
 		return html`
 			<nav>
 				<div class="head">starweave gallery</div>
-				${entries.map(
-					(e) => html`<a href=${`#${e.id}`} class=${e.id === this.currentId ? "current" : ""}
+				${
+			entries.map(
+				(e) =>
+					html`<a href=${`#${e.id}`} class=${e.id === this.currentId ? "current" : ""}
 						>${e.title}</a
 					>`,
-				)}
+			)
+		}
 			</nav>
 			<main>${current ? this.renderEntry(current) : this.renderIndex()}</main>
 			<sw-infopop></sw-infopop>
@@ -141,12 +145,15 @@ export class SwGallery extends LitElement {
 		return html`
 			<h1>${entry.title}</h1>
 			<div class="description">${entry.description}</div>
-			${entry.variants.map(
-				(v) => html`<div class="variant">
+			${
+			entry.variants.map(
+				(v) =>
+					html`<div class="variant">
 					<div class="caption">${v.caption}</div>
 					<div class="stage ${v.wide ? "wide" : ""}">${v.render()}</div>
 				</div>`,
-			)}
+			)
+		}
 		`;
 	}
 
diff --git a/packages/yukey/src/infopop-place.test.ts b/packages/yukey/src/infopop-place.test.ts
index 51ad68c..c574383 100644
--- a/packages/yukey/src/infopop-place.test.ts
+++ b/packages/yukey/src/infopop-place.test.ts
@@ -1,14 +1,7 @@
 // Tests for the pure placement rule (infopop-place.ts).  Plain node env: geometry in, geometry out.
 
 import { describe, expect, it } from "vitest";
-import {
-	ANCHOR_GAP,
-	type Placement,
-	placeInfopop,
-	type Rect,
-	type Size,
-	VIEWPORT_MARGIN,
-} from "./infopop-place.js";
+import { ANCHOR_GAP, placeInfopop, type Placement, type Rect, type Size, VIEWPORT_MARGIN } from "./infopop-place.js";
 
 const viewport: Size = { w: 1000, h: 800 };
 const popup: Size = { w: 200, h: 60 };
@@ -19,10 +12,10 @@ function anchorAt(x: number, y: number, w = 100, h = 24): Rect {
 
 function insideViewport(p: Placement): boolean {
 	return (
-		p.x >= VIEWPORT_MARGIN &&
-		p.y >= VIEWPORT_MARGIN &&
-		p.x + popup.w <= viewport.w - VIEWPORT_MARGIN &&
-		p.y + popup.h <= viewport.h - VIEWPORT_MARGIN
+		p.x >= VIEWPORT_MARGIN
+		&& p.y >= VIEWPORT_MARGIN
+		&& p.x + popup.w <= viewport.w - VIEWPORT_MARGIN
+		&& p.y + popup.h <= viewport.h - VIEWPORT_MARGIN
 	);
 }
 
diff --git a/packages/yukey/src/megacolumn-controller.test.ts b/packages/yukey/src/megacolumn-controller.test.ts
index d8155d0..86c1853 100644
--- a/packages/yukey/src/megacolumn-controller.test.ts
+++ b/packages/yukey/src/megacolumn-controller.test.ts
@@ -144,7 +144,7 @@ describe("closed rows (aria-hidden)", () => {
 		expect(ctl.nav.focusedId).toBe("c");
 	});
 
-	it('aria-hidden="false" does NOT close a row: matched by value, never presence', () => {
+	it("aria-hidden=\"false\" does NOT close a row: matched by value, never presence", () => {
 		// aria-hidden="false" is known to behave inconsistently in browsers; a
 		// presence check would silently read it as hidden and make a visible row
 		// unreachable.  The scan matches the value "true" only.
@@ -538,8 +538,7 @@ describe("scrolling the focused row into view", () => {
 	afterEach(() => {
 		scrolls.mockRestore();
 	});
-	const scrolledRows = (): unknown[] =>
-		scrolls.mock.contexts.map((el) => (el as HTMLElement).dataset?.mcRow);
+	const scrolledRows = (): unknown[] => scrolls.mock.contexts.map((el) => (el as HTMLElement).dataset?.mcRow);
 
 	it("scrolls the newly focused row on arrow movement, nearest-edge and instant", () => {
 		const { key } = makeRig(threeRows);
@@ -868,7 +867,7 @@ describe("the megacolumn-focus-request event (programmatic focus moves)", () =>
 
 	it("moves logical focus to the named row and renders the move", () => {
 		const { ctl, root, container } = makeRig(threeRows);
-		const rowC = root.querySelector<HTMLElement>('[data-mc-row="c"]');
+		const rowC = root.querySelector<HTMLElement>("[data-mc-row=\"c\"]");
 		if (rowC === null) throw new Error("unreachable: rig just rendered row c");
 		const e = request(rowC, "c");
 		expect(ctl.nav.focusedId).toBe("c");
@@ -1277,13 +1276,13 @@ describe("column-scoped hotkey markings (data-mc-hotkey)", () => {
 			makeRig(`
 				<div data-mc-row="a" data-mc-hotkey="d">a</div>
 				<div data-mc-row="b" data-mc-hotkey="d">b</div>
-			`),
+			`)
 		).toThrow(/duplicate hotkey marking "d".*"a".*"b"/);
 		expect(() =>
 			makeRig(`
 				<div data-mc-row="a" data-mc-hotkey="d">a</div>
 				<div data-mc-row="b" data-mc-hotkey="D">b</div>
-			`),
+			`)
 		).toThrow(/duplicate hotkey marking/);
 	});
 
@@ -1297,16 +1296,16 @@ describe("column-scoped hotkey markings (data-mc-hotkey)", () => {
 	});
 
 	it("throws on a marking outside any row", () => {
-		expect(() =>
-			makeRig(`<span data-mc-hotkey="d">stray</span><div data-mc-row="a">a</div>`),
-		).toThrow(/outside any row/);
+		expect(() => makeRig(`<span data-mc-hotkey="d">stray</span><div data-mc-row="a">a</div>`)).toThrow(
+			/outside any row/,
+		);
 	});
 
 	it("throws on a marking on an aux control (x/Delete are the aux norms)", () => {
 		expect(() =>
 			makeRig(`
 				<div data-mc-row="a"><span data-mc-main>m</span><span data-mc-aux data-mc-hotkey="j">x</span></div>
-			`),
+			`)
 		).toThrow(/aux/);
 	});
 });
diff --git a/packages/yukey/src/megacolumn-controller.ts b/packages/yukey/src/megacolumn-controller.ts
index 6b578cb..0b7ee16 100644
--- a/packages/yukey/src/megacolumn-controller.ts
+++ b/packages/yukey/src/megacolumn-controller.ts
@@ -201,7 +201,7 @@ type Host = ReactiveControllerHost & { renderRoot: DocumentFragment | HTMLElemen
 //   - Match by VALUE, never by attribute presence: aria-hidden="false" behaves
 //     inconsistently in browsers and must never read as hidden here.
 function closedToSight(el: HTMLElement): boolean {
-	return el.closest('[aria-hidden="true"]') !== null;
+	return el.closest("[aria-hidden=\"true\"]") !== null;
 }
 
 export class MegacolumnController implements ReactiveController {
@@ -275,14 +275,16 @@ export class MegacolumnController implements ReactiveController {
 			// The container must stay the only tab stop: a stray natively-focusable
 			// element inside a row would fracture the focus model.  Widgets that truly
 			// need real focus must be marked data-mc-input (text entry) instead.
-			for (const candidate of el.querySelectorAll(
-				"button, select, textarea, input, a[href], [tabindex]",
-			)) {
+			for (
+				const candidate of el.querySelectorAll(
+					"button, select, textarea, input, a[href], [tabindex]",
+				)
+			) {
 				if (candidate.matches("[data-mc-input]")) continue;
 				if (candidate instanceof HTMLElement && candidate.tabIndex === -1) continue;
 				throw new Error(
-					`megacolumn: row "${id}" contains a native tab stop (${candidate.tagName.toLowerCase()}); ` +
-						"give it tabindex=-1 or mark it data-mc-input",
+					`megacolumn: row "${id}" contains a native tab stop (${candidate.tagName.toLowerCase()}); `
+						+ "give it tabindex=-1 or mark it data-mc-input",
 				);
 			}
 			// aria-activedescendant needs a referencable id; derive one from the row id.
@@ -349,25 +351,28 @@ export class MegacolumnController implements ReactiveController {
 		for (const el of container.querySelectorAll<HTMLElement>("[data-mc-hotkey]")) {
 			if (closedToSight(el)) continue;
 			const marking = el.dataset.mcHotkey ?? "";
-			if (!/^[a-z0-9]$/i.test(marking))
+			if (!/^[a-z0-9]$/i.test(marking)) {
 				throw new Error(
 					`megacolumn: hotkey marking "${marking}" must be a single [a-z0-9] character`,
 				);
+			}
 			const char = marking.toLowerCase();
 			const row = el.closest<HTMLElement>("[data-mc-row]");
-			if (row === null)
+			if (row === null) {
 				throw new Error(`megacolumn: hotkey marking "${marking}" is outside any row`);
-			if (el.closest("[data-mc-aux]") !== null)
+			}
+			if (el.closest("[data-mc-aux]") !== null) {
 				throw new Error(
-					`megacolumn: row "${row.dataset.mcRow}" marks its aux control with "${marking}"; ` +
-						"aux controls answer to x/Delete already",
+					`megacolumn: row "${row.dataset.mcRow}" marks its aux control with "${marking}"; `
+						+ "aux controls answer to x/Delete already",
 				);
+			}
 			const prior = this.#hotkeys.get(char);
 			if (prior !== undefined) {
 				const priorRow = prior.closest<HTMLElement>("[data-mc-row]");
 				throw new Error(
-					`megacolumn: duplicate hotkey marking "${char}" on rows ` +
-						`"${priorRow?.dataset.mcRow}" and "${row.dataset.mcRow}"`,
+					`megacolumn: duplicate hotkey marking "${char}" on rows `
+						+ `"${priorRow?.dataset.mcRow}" and "${row.dataset.mcRow}"`,
 				);
 			}
 			this.#hotkeys.set(char, el);
@@ -673,8 +678,9 @@ export class MegacolumnController implements ReactiveController {
 	private controlElement(row: HTMLElement, position: RowPosition): HTMLElement {
 		if (position === "aux") {
 			const aux = row.querySelector<HTMLElement>("[data-mc-aux]");
-			if (aux === null)
+			if (aux === null) {
 				throw new Error(`megacolumn: row "${row.dataset.mcRow}" lost its [data-mc-aux]`);
+			}
 			return aux;
 		}
 		return row.querySelector<HTMLElement>("[data-mc-main]") ?? row;
diff --git a/packages/yukey/src/megacolumn-nav.ts b/packages/yukey/src/megacolumn-nav.ts
index 0fc7dfd..ed4805f 100644
--- a/packages/yukey/src/megacolumn-nav.ts
+++ b/packages/yukey/src/megacolumn-nav.ts
@@ -130,8 +130,9 @@ export class MegacolumnNav {
 		const row = this.rowById(id);
 		if (row === undefined) throw new Error(`focusRow: unknown row id: ${id}`);
 		if (!isFocusable(row)) throw new Error(`focusRow: row is not focusable: ${id}`);
-		if (position === "aux" && row.hasAux !== true)
+		if (position === "aux" && row.hasAux !== true) {
 			throw new Error(`focusRow: row has no aux control: ${id}`);
+		}
 		this.#focusedId = id;
 		this.#position = position;
 		this.#textMode = false;
@@ -177,13 +178,15 @@ export class MegacolumnNav {
 		// and letting them scroll the page would be surprising -- but nothing moves.
 		// Other keys pass: there is no row widget to forward to.
 		if (this.#focusedId === null) {
-			if (key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End")
+			if (key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End") {
 				return { kind: "nav" };
+			}
 			return { kind: "pass" };
 		}
 		const row = this.rowById(this.#focusedId);
-		if (row === undefined)
+		if (row === undefined) {
 			throw new Error(`focused row vanished without setRows: ${this.#focusedId}`);
+		}
 
 		switch (key) {
 			case "ArrowDown":
diff --git a/packages/yukey/src/megacolumn.gallery.ts b/packages/yukey/src/megacolumn.gallery.ts
index 506631d..4db74d3 100644
--- a/packages/yukey/src/megacolumn.gallery.ts
+++ b/packages/yukey/src/megacolumn.gallery.ts
@@ -8,11 +8,7 @@ import { css, html, LitElement, type TemplateResult } from "lit";
 import { customElement, property, state } from "lit/decorators.js";
 import { ifDefined } from "lit/directives/if-defined.js";
 import type { GalleryEntry } from "./gallery/entry.js";
-import {
-	MegacolumnController,
-	type MegacolumnKeyDetail,
-	megacolumnStyles,
-} from "./megacolumn-controller.js";
+import { MegacolumnController, type MegacolumnKeyDetail, megacolumnStyles } from "./megacolumn-controller.js";
 import { flashPress } from "./press-flash.js";
 
 interface CargoItem {
@@ -141,20 +137,27 @@ export class SwgMegacolumnDemo extends LitElement {
 	// biome-ignore lint/correctness/noUnusedPrivateClassMembers: wires itself via addController
 	private mc = new MegacolumnController(this);
 
-	@property() label = "megacolumn";
+	@property()
+	label = "megacolumn";
 	// The churn demo only runs when asked: two identical always-churning columns
 	// side by side would be visual noise.
-	@property({ type: Boolean }) churn = false;
+	@property({ type: Boolean })
+	churn = false;
 
-	@state() private beacons = 0;
-	@state() private power = 40;
-	@state() private cargo: CargoItem[] = [
+	@state()
+	private beacons = 0;
+	@state()
+	private power = 40;
+	@state()
+	private cargo: CargoItem[] = [
 		{ id: "crate-ore", label: "ore crate" },
 		{ id: "crate-struts", label: "strut bundle" },
 		{ id: "crate-cells", label: "fuel cells" },
 	];
-	@state() private tasks: TaskItem[] = [];
-	@state() private lastAction = "";
+	@state()
+	private tasks: TaskItem[] = [];
+	@state()
+	private lastAction = "";
 	private taskSeq = 0;
 	private timer: number | undefined;
 
@@ -225,8 +228,7 @@ export class SwgMegacolumnDemo extends LitElement {
 			// The two-control norm: x/Delete remove from anywhere in the row (the
 			// hotkey guess the guidelines reward); Enter/Space only remove from the
 			// aux control -- on main they inspect (activation must not be destructive).
-			const remove =
-				detail.key === "x" || detail.key === "Delete" || (activate && detail.position === "aux");
+			const remove = detail.key === "x" || detail.key === "Delete" || (activate && detail.position === "aux");
 			if (remove) {
 				this.cargo = this.cargo.filter((c) => c.id !== cargoItem.id);
 				this.lastAction = `${cargoItem.label} jettisoned`;
@@ -291,17 +293,20 @@ export class SwgMegacolumnDemo extends LitElement {
 				</div>
 
 				<div class="section">cargo manifest</div>
-				${this.cargo.map(
-					// Cargo rows carry no hotkey markings on purpose: they churn, and a
-					// letter that vanishes with its row is a broken affordance.  Letters
-					// mark landmarks; lists are for arrows.
-					(item) => html`
+				${
+			this.cargo.map(
+				// Cargo rows carry no hotkey markings on purpose: they churn, and a
+				// letter that vanishes with its row is a broken affordance.  Letters
+				// mark landmarks; lists are for arrows.
+				(item) =>
+					html`
 						<div data-mc-row=${item.id} data-info="a crate in the hold; Enter inspects it">
 							<span class="grow" data-mc-main>${item.label}</span>
 							<span data-mc-aux title="jettison" data-info="jettisons this crate (x or Delete from anywhere in the row)">×</span>
 						</div>
 					`,
-				)}
+			)
+		}
 
 				<div class="section">comms</div>
 				<div data-mc-row="hail" data-mc-hotkey="h">
@@ -327,15 +332,17 @@ export class SwgMegacolumnDemo extends LitElement {
 				</div>
 
 				${
-					this.churn
-						? html`
+			this.churn
+				? html`
 							<div class="section">task queue (live)</div>
-							${this.tasks.map(
-								(t) => html`<div data-mc-row=${t.id}><span class="grow">${t.label}</span></div>`,
-							)}
-						`
-						: ""
+							${
+					this.tasks.map(
+						(t) => html`<div data-mc-row=${t.id}><span class="grow">${t.label}</span></div>`,
+					)
 				}
+						`
+				: ""
+		}
 			</div>
 			<div class="log">${this.lastAction}</div>
 		`;
@@ -423,7 +430,8 @@ export class SwgMegacolumnScrollDemo extends LitElement {
 	// biome-ignore lint/correctness/noUnusedPrivateClassMembers: wires itself via addController
 	private mc = new MegacolumnController(this);
 
-	@state() private lastAction = "";
+	@state()
+	private lastAction = "";
 
 	private onMegacolumnKey(e: Event): void {
 		const { detail } = e as CustomEvent<MegacolumnKeyDetail>;
@@ -441,8 +449,10 @@ export class SwgMegacolumnScrollDemo extends LitElement {
 				aria-label="relay bank"
 				@megacolumn-key=${this.onMegacolumnKey}
 			>
-				${relayIds.map(
-					(nn) => html`
+				${
+			relayIds.map(
+				(nn) =>
+					html`
 						<div
 							data-mc-row=${`relay-${nn}`}
 							data-mc-hotkey=${ifDefined(relayHotkeys.get(nn))}
@@ -451,7 +461,8 @@ export class SwgMegacolumnScrollDemo extends LitElement {
 							<span>${relayLabel(nn)}</span>
 						</div>
 					`,
-				)}
+			)
+		}
 			</div>
 			<div class="log">${this.lastAction}</div>
 		`;
@@ -461,28 +472,27 @@ export class SwgMegacolumnScrollDemo extends LitElement {
 const entry: GalleryEntry = {
 	id: "megacolumn",
 	title: "megacolumn focus model",
-	description:
-		"The keyboard navigation machinery every megacolumn screen builds on: " +
-		"one real focusable per column, row focus as logical state (MegacolumnNav), " +
-		"rendered via aria-activedescendant and the focus-ring tokens (MegacolumnController).  " +
-		"This demo is the reference implementation of the data-mc-* DOM contract.",
+	description: "The keyboard navigation machinery every megacolumn screen builds on: "
+		+ "one real focusable per column, row focus as logical state (MegacolumnNav), "
+		+ "rendered via aria-activedescendant and the focus-ring tokens (MegacolumnController).  "
+		+ "This demo is the reference implementation of the data-mc-* DOM contract.",
 	variants: [
 		{
-			caption:
-				"Two independent columns: Tab moves between them (each column is ONE tab stop); " +
-				"up/down move the row focus inside, jumping the section gaps.  " +
-				"The gold letters are hotkey jumps: b/d/r/h/e/s/v/a/g move focus straight to their row " +
-				"(from anywhere in the column), then Enter/Space commits -- try g from the top, or b from the bottom.  " +
-				"Also try: Enter/Space on the action rows; left/right on the reactor slider; " +
-				"right-arrow onto a crate's × then Enter (or just x/Delete from anywhere in the row -- " +
-				"crates churn, so they get no letters: letters mark landmarks, arrows work lists); " +
-				"Enter on the hail row to type -- every key goes to the input, hotkeys included -- then Enter sends, Escape abandons; " +
-				"Home/End for the column edges.  Tab away and back: the row focus is where you left it.  " +
-				"Rows carry data-info, so i (or a lingering hover) explains any of them -- " +
-				"including the offline row, which stays a nav stop and a jump target (e lands there; " +
-				"i is how you ask why it's off) even though activation is dead there.",
+			caption: "Two independent columns: Tab moves between them (each column is ONE tab stop); "
+				+ "up/down move the row focus inside, jumping the section gaps.  "
+				+ "The gold letters are hotkey jumps: b/d/r/h/e/s/v/a/g move focus straight to their row "
+				+ "(from anywhere in the column), then Enter/Space commits -- try g from the top, or b from the bottom.  "
+				+ "Also try: Enter/Space on the action rows; left/right on the reactor slider; "
+				+ "right-arrow onto a crate's × then Enter (or just x/Delete from anywhere in the row -- "
+				+ "crates churn, so they get no letters: letters mark landmarks, arrows work lists); "
+				+ "Enter on the hail row to type -- every key goes to the input, hotkeys included -- then Enter sends, Escape abandons; "
+				+ "Home/End for the column edges.  Tab away and back: the row focus is where you left it.  "
+				+ "Rows carry data-info, so i (or a lingering hover) explains any of them -- "
+				+ "including the offline row, which stays a nav stop and a jump target (e lands there; "
+				+ "i is how you ask why it's off) even though activation is dead there.",
 			wide: true,
-			render: () => html`
+			render: () =>
+				html`
 				<div style="display:flex; gap:24px; align-items:flex-start;">
 					<swg-megacolumn-demo style="flex:1" label="port console"></swg-megacolumn-demo>
 					<swg-megacolumn-demo style="flex:1" label="starboard console"></swg-megacolumn-demo>
@@ -490,20 +500,19 @@ const entry: GalleryEntry = {
 			`,
 		},
 		{
-			caption:
-				"Focus memory under churn: the task queue loses its head and grows a tail every few seconds.  " +
-				"Focus a task row and watch focus fall to the nearest surviving neighbor when the row vanishes; " +
-				"the rest of the column is unbothered.",
+			caption: "Focus memory under churn: the task queue loses its head and grows a tail every few seconds.  "
+				+ "Focus a task row and watch focus fall to the nearest surviving neighbor when the row vanishes; "
+				+ "the rest of the column is unbothered.",
 			render: () => html`<swg-megacolumn-demo label="ops console" churn></swg-megacolumn-demo>`,
 		},
 		{
 			caption:
-				"Scroll containment: 28 static relays in a 240px box -- the thing to WATCH is that the page never scrolls, only the box.  " +
-				"Home/End slam between the edges; arrows creep across the boundary rows and the box gives way one row at a time " +
-				"(nearest-edge scrolling: no centering, no glide); " +
-				"the gold digits jump -- 1/0/2/8 land on relays 01/10/20/28, crossing most of the scroller in one keystroke, " +
-				"the ring arriving in view every time.  " +
-				"Enter pings a relay; a few rows carry data-info, so i works mid-scroll.",
+				"Scroll containment: 28 static relays in a 240px box -- the thing to WATCH is that the page never scrolls, only the box.  "
+				+ "Home/End slam between the edges; arrows creep across the boundary rows and the box gives way one row at a time "
+				+ "(nearest-edge scrolling: no centering, no glide); "
+				+ "the gold digits jump -- 1/0/2/8 land on relays 01/10/20/28, crossing most of the scroller in one keystroke, "
+				+ "the ring arriving in view every time.  "
+				+ "Enter pings a relay; a few rows carry data-info, so i works mid-scroll.",
 			render: () => html`<swg-megacolumn-scroll-demo></swg-megacolumn-scroll-demo>`,
 		},
 	],
diff --git a/packages/yukey/src/press-flash.ts b/packages/yukey/src/press-flash.ts
index 08e1fd8..7d84403 100644
--- a/packages/yukey/src/press-flash.ts
+++ b/packages/yukey/src/press-flash.ts
@@ -20,11 +20,12 @@ export const PRESSED_CLASS = "sw-pressed";
 export function pressMs(el: Element): number {
 	const raw = getComputedStyle(el).getPropertyValue("--sw-press-ms").trim();
 	const m = /^(\d+(?:\.\d+)?)(m?s)$/.exec(raw);
-	if (m === null)
+	if (m === null) {
 		throw new Error(
-			`press-flash: --sw-press-ms did not resolve on <${el.tagName.toLowerCase()}> ` +
-				`(got "${raw}"); are the design tokens installed?`,
+			`press-flash: --sw-press-ms did not resolve on <${el.tagName.toLowerCase()}> `
+				+ `(got "${raw}"); are the design tokens installed?`,
 		);
+	}
 	const n = Number(m[1]);
 	return m[2] === "s" ? n * 1000 : n;
 }
diff --git a/packages/yukey/src/sw-button.gallery.ts b/packages/yukey/src/sw-button.gallery.ts
index d03fd7a..fae6f8a 100644
--- a/packages/yukey/src/sw-button.gallery.ts
+++ b/packages/yukey/src/sw-button.gallery.ts
@@ -5,11 +5,7 @@
 import { css, html, LitElement, type TemplateResult } from "lit";
 import { customElement, property, state } from "lit/decorators.js";
 import type { GalleryEntry } from "./gallery/entry.js";
-import {
-	MegacolumnController,
-	type MegacolumnKeyDetail,
-	megacolumnStyles,
-} from "./megacolumn-controller.js";
+import { MegacolumnController, type MegacolumnKeyDetail, megacolumnStyles } from "./megacolumn-controller.js";
 import "./sw-button.js";
 import { flashPress } from "./press-flash.js";
 import type { SwInfoDetail } from "./sw-info.js";
@@ -52,8 +48,10 @@ export class SwgButtonStandalone extends LitElement {
 		`,
 	];
 
-	@state() private last = "";
-	@state() private lastWasInfo = false;
+	@state()
+	private last = "";
+	@state()
+	private lastWasInfo = false;
 
 	private act(what: string): void {
 		this.last = what;
@@ -137,15 +135,21 @@ export class SwgButtonColumn extends LitElement {
 	// biome-ignore lint/correctness/noUnusedPrivateClassMembers: wires itself via addController
 	private mc = new MegacolumnController(this);
 
-	@property() label = "console";
+	@property()
+	label = "console";
 	// Hotkeys are per-instance so the scoping demo can give two columns
 	// overlapping letters without tripping the per-column duplicate throw.
-	@property() dockKey = "d";
-	@property() beaconKey = "b";
+	@property()
+	dockKey = "d";
+	@property()
+	beaconKey = "b";
 
-	@state() private cargo = ["ore crate", "strut bundle"];
-	@state() private last = "";
-	@state() private lastWasInfo = false;
+	@state()
+	private cargo = ["ore crate", "strut bundle"];
+	@state()
+	private last = "";
+	@state()
+	private lastWasInfo = false;
 
 	private act(what: string): void {
 		this.last = what;
@@ -205,8 +209,10 @@ export class SwgButtonColumn extends LitElement {
 						@click=${() => this.act("purged (should never happen)")}
 					></sw-button>
 				</div>
-				${this.cargo.map(
-					(item) => html`
+				${
+			this.cargo.map(
+				(item) =>
+					html`
 						<div data-mc-row=${item}>
 							<span class="grow" data-mc-main>${item}</span>
 							<span data-mc-aux>
@@ -214,14 +220,15 @@ export class SwgButtonColumn extends LitElement {
 									label="×"
 									info="jettisons this crate (x or Delete from anywhere in the row)"
 									@click=${() => {
-										this.cargo = this.cargo.filter((c) => c !== item);
-										this.act(`${item} jettisoned`);
-									}}
+						this.cargo = this.cargo.filter((c) => c !== item);
+						this.act(`${item} jettisoned`);
+					}}
 								></sw-button>
 							</span>
 						</div>
 					`,
-				)}
+			)
+		}
 			</div>
 			<div class="log ${this.lastWasInfo ? "info" : ""}">${this.last}</div>
 		`;
@@ -231,40 +238,37 @@ export class SwgButtonColumn extends LitElement {
 const entry: GalleryEntry = {
 	id: "sw-button",
 	title: "sw-button",
-	description:
-		"The button that makes the keyboard norms structural: hotkey marking " +
-		"(letter-in-label in --sw-hotkey-accent, else the 70%-offset slot, an icon for " +
-		"complex buttons), the i info hook (sw-info events, rendered by the shell's " +
-		"sw-infopop -- press i on anything here and watch), token hit targets, and " +
-		"dual-context behavior -- a native focusable standalone, a megacolumn-key " +
-		"consumer inside rows.",
+	description: "The button that makes the keyboard norms structural: hotkey marking "
+		+ "(letter-in-label in --sw-hotkey-accent, else the 70%-offset slot, an icon for "
+		+ "complex buttons), the i info hook (sw-info events, rendered by the shell's "
+		+ "sw-infopop -- press i on anything here and watch), token hit targets, and "
+		+ "dual-context behavior -- a native focusable standalone, a megacolumn-key "
+		+ "consumer inside rows.",
 	variants: [
 		{
-			caption:
-				"Standalone: real tab stops with native Enter/Space.  All marking forms: " +
-				"letter-in-label (Dock's D), the 70% slot (units' 7), the complex-button icon " +
-				"(transfer), the active in-effect option (cruise: washed, aria-current, still " +
-				"clickable), and disabled (dimmed, unclickable, but i still answers while focused).  " +
-				"A button's hotkey and i work while it holds focus; watch the log.",
+			caption: "Standalone: real tab stops with native Enter/Space.  All marking forms: "
+				+ "letter-in-label (Dock's D), the 70% slot (units' 7), the complex-button icon "
+				+ "(transfer), the active in-effect option (cruise: washed, aria-current, still "
+				+ "clickable), and disabled (dimmed, unclickable, but i still answers while focused).  "
+				+ "A button's hotkey and i work while it holds focus; watch the log.",
 			render: () => html`<swg-button-standalone></swg-button-standalone>`,
 		},
 		{
-			caption:
-				"Inside a megacolumn: one tab stop for the whole column, buttons as rows.  " +
-				"The marked letters are jumps: d or b from any row moves focus to that button " +
-				"(no arrowing needed), and Enter/Space then activates -- you see where you are " +
-				"before committing.  Try: d, then Enter; i for info on the focused control; " +
-				"on a crate row, x or Delete jettisons via the aux ×, or right-arrow onto it and Enter.  " +
-				"The disabled row still focuses and answers i, but nothing activates it.",
+			caption: "Inside a megacolumn: one tab stop for the whole column, buttons as rows.  "
+				+ "The marked letters are jumps: d or b from any row moves focus to that button "
+				+ "(no arrowing needed), and Enter/Space then activates -- you see where you are "
+				+ "before committing.  Try: d, then Enter; i for info on the focused control; "
+				+ "on a crate row, x or Delete jettisons via the aux ×, or right-arrow onto it and Enter.  "
+				+ "The disabled row still focuses and answers i, but nothing activates it.",
 			render: () => html`<swg-button-column label="ops console"></swg-button-column>`,
 		},
 		{
-			caption:
-				"Hotkey scope IS the focused column: both columns mark d for dock, and d jumps only " +
-				"in the column that holds focus.  Tab between them and try d then Enter in each; " +
-				"the other column never hears it.",
+			caption: "Hotkey scope IS the focused column: both columns mark d for dock, and d jumps only "
+				+ "in the column that holds focus.  Tab between them and try d then Enter in each; "
+				+ "the other column never hears it.",
 			wide: true,
-			render: () => html`
+			render: () =>
+				html`
 				<div style="display:flex; gap:24px; align-items:flex-start;">
 					<swg-button-column style="flex:1" label="port console" dockKey="d" beaconKey="b"></swg-button-column>
 					<swg-button-column style="flex:1" label="starboard console" dockKey="d" beaconKey="c"></swg-button-column>
diff --git a/packages/yukey/src/sw-button.ts b/packages/yukey/src/sw-button.ts
index 75da923..54142c5 100644
--- a/packages/yukey/src/sw-button.ts
+++ b/packages/yukey/src/sw-button.ts
@@ -161,10 +161,14 @@ export class SwButton extends LitElement {
 		}
 	`;
 
-	@property() label = "";
-	@property() icon = "";
-	@property({ type: Boolean, reflect: true }) disabled = false;
-	@property({ type: Boolean }) active = false;
+	@property()
+	label = "";
+	@property()
+	icon = "";
+	@property({ type: Boolean, reflect: true })
+	disabled = false;
+	@property({ type: Boolean })
+	active = false;
 
 	// The info content, mirrored synchronously onto data-info: that attribute is
 	// the carrier contract sw-infopop's hover trigger finds elements by
@@ -193,8 +197,9 @@ export class SwButton extends LitElement {
 		return this.#hotkey;
 	}
 	set hotkey(v: string) {
-		if (v !== "" && !/^[a-z0-9]$/i.test(v))
+		if (v !== "" && !/^[a-z0-9]$/i.test(v)) {
 			throw new Error(`sw-button: hotkey "${v}" must be a single [a-z0-9] character`);
+		}
 		const norm = v.toLowerCase();
 		this.#hotkey = norm;
 		if (norm === "") delete this.dataset.mcHotkey;
@@ -226,11 +231,12 @@ export class SwButton extends LitElement {
 	connectedCallback(): void {
 		super.connectedCallback();
 		this.#row = this.closest("[data-mc-row]");
-		if (this.#row === null && this.closest("[data-mc-container]") !== null)
+		if (this.#row === null && this.closest("[data-mc-container]") !== null) {
 			throw new Error(
-				"sw-button: inside a megacolumn container but outside any row; " +
-					"a loose tab stop would fracture the single-tab-stop model",
+				"sw-button: inside a megacolumn container but outside any row; "
+					+ "a loose tab stop would fracture the single-tab-stop model",
 			);
+		}
 		if (this.#row !== null) {
 			this.#row.addEventListener("megacolumn-key", this.#onMegacolumnKey);
 		} else {
@@ -383,7 +389,9 @@ export class SwButton extends LitElement {
 
 	#content(marked: number): TemplateResult {
 		if (marked === -1) return html`${this.label}`;
-		return html`${this.label.slice(0, marked)}<span class="hk">${this.label[marked]}</span>${this.label.slice(marked + 1)}`;
+		return html`${this.label.slice(0, marked)}<span class="hk">${this.label[marked]}</span>${
+			this.label.slice(marked + 1)
+		}`;
 	}
 }
 
diff --git a/packages/yukey/src/sw-infopop.gallery.ts b/packages/yukey/src/sw-infopop.gallery.ts
index 93efd76..15e5a42 100644
--- a/packages/yukey/src/sw-infopop.gallery.ts
+++ b/packages/yukey/src/sw-infopop.gallery.ts
@@ -6,11 +6,7 @@
 import { css, html, LitElement, type TemplateResult } from "lit";
 import { customElement, property, state } from "lit/decorators.js";
 import type { GalleryEntry } from "./gallery/entry.js";
-import {
-	MegacolumnController,
-	type MegacolumnKeyDetail,
-	megacolumnStyles,
-} from "./megacolumn-controller.js";
+import { MegacolumnController, type MegacolumnKeyDetail, megacolumnStyles } from "./megacolumn-controller.js";
 import "./sw-button.js";
 
 // A megacolumn where every row flavor carries info: sw-buttons answer i
@@ -77,10 +73,13 @@ export class SwgInfopopColumn extends LitElement {
 	// biome-ignore lint/correctness/noUnusedPrivateClassMembers: wires itself via addController
 	private mc = new MegacolumnController(this);
 
-	@property() label = "info everywhere";
+	@property()
+	label = "info everywhere";
 
-	@state() private power = 40;
-	@state() private last = "";
+	@state()
+	private power = 40;
+	@state()
+	private last = "";
 
 	private onMegacolumnKey(e: Event): void {
 		const { key, rowId } = (e as CustomEvent<MegacolumnKeyDetail>).detail;
@@ -111,8 +110,8 @@ export class SwgInfopopColumn extends LitElement {
 						hotkey="d"
 						info="an sw-button answers i itself; hover reads the same data-info mirror"
 						@click=${() => {
-							this.last = "docked";
-						}}
+			this.last = "docked";
+		}}
 					></sw-button>
 				</div>
 				<div data-mc-row="survey" data-info="a plain text row: the megacolumn answers i for its data-info">
@@ -166,29 +165,26 @@ export class SwgInfopopEdges extends LitElement {
 const entry: GalleryEntry = {
 	id: "sw-infopop",
 	title: "sw-infopop",
-	description:
-		"The info norm's popup: keyboard i and mouse hover raise the SAME authored " +
-		"content (one data-info attribute per element, two triggers).  Keyboard-raised " +
-		"popups pin until any key or click; hover popups follow the pointer and never " +
-		"displace a pinned one.  The instance shown is the gallery shell's own popup: " +
-		"one per app, listening at its root.",
+	description: "The info norm's popup: keyboard i and mouse hover raise the SAME authored "
+		+ "content (one data-info attribute per element, two triggers).  Keyboard-raised "
+		+ "popups pin until any key or click; hover popups follow the pointer and never "
+		+ "displace a pinned one.  The instance shown is the gallery shell's own popup: "
+		+ "one per app, listening at its root.",
 	variants: [
 		{
-			caption:
-				"Every row flavor carries info.  Keyboard: focus the column, press i on each row -- " +
-				"the sw-button answers itself, the plain rows are answered by the megacolumn's data-info " +
-				"fallback, and on the probe row, right-arrow onto the × for its own story.  " +
-				"i again on the same row dismisses (a toggle); i after moving to another row follows.  " +
-				"Mouse: hover the same places and linger; the identical text appears.  " +
-				"Precedence: press i, then wave the mouse over other rows -- the pinned popup holds " +
-				"until a key or click dismisses it.  Navigation keys close the popup AND still navigate.",
+			caption: "Every row flavor carries info.  Keyboard: focus the column, press i on each row -- "
+				+ "the sw-button answers itself, the plain rows are answered by the megacolumn's data-info "
+				+ "fallback, and on the probe row, right-arrow onto the × for its own story.  "
+				+ "i again on the same row dismisses (a toggle); i after moving to another row follows.  "
+				+ "Mouse: hover the same places and linger; the identical text appears.  "
+				+ "Precedence: press i, then wave the mouse over other rows -- the pinned popup holds "
+				+ "until a key or click dismisses it.  Navigation keys close the popup AND still navigate.",
 			render: () => html`<swg-infopop-column></swg-infopop-column>`,
 		},
 		{
-			caption:
-				"Edge behavior: these buttons hug the stage's sides, so the below-centered placement " +
-				"clamps horizontally to stay inside the viewport.  Hover or focus-and-i near a viewport " +
-				"edge (shrink the window height to see the bottom flip: the popup jumps above the anchor).",
+			caption: "Edge behavior: these buttons hug the stage's sides, so the below-centered placement "
+				+ "clamps horizontally to stay inside the viewport.  Hover or focus-and-i near a viewport "
+				+ "edge (shrink the window height to see the bottom flip: the popup jumps above the anchor).",
 			wide: true,
 			render: () => html`<swg-infopop-edges></swg-infopop-edges>`,
 		},
diff --git a/packages/yukey/src/sw-infopop.ts b/packages/yukey/src/sw-infopop.ts
index 8734822..d879d7d 100644
--- a/packages/yukey/src/sw-infopop.ts
+++ b/packages/yukey/src/sw-infopop.ts
@@ -80,7 +80,8 @@ export class SwInfopop extends LitElement {
 		}
 	`;
 
-	@state() private content = "";
+	@state()
+	private content = "";
 	#mode: Mode = "closed";
 	#anchor: HTMLElement | null = null;
 	// The [data-info] carrier currently under the pointer (tracked in every mode,
diff --git a/packages/yukey/src/sw-list.gallery.ts b/packages/yukey/src/sw-list.gallery.ts
index 1e587c8..69b0b29 100644
--- a/packages/yukey/src/sw-list.gallery.ts
+++ b/packages/yukey/src/sw-list.gallery.ts
@@ -12,11 +12,7 @@ import { css, html, LitElement, nothing, type TemplateResult } from "lit";
 import { customElement, property, state } from "lit/decorators.js";
 import { classMap } from "lit/directives/class-map.js";
 import type { GalleryEntry } from "./gallery/entry.js";
-import {
-	MegacolumnController,
-	type MegacolumnKeyDetail,
-	megacolumnStyles,
-} from "./megacolumn-controller.js";
+import { MegacolumnController, type MegacolumnKeyDetail, megacolumnStyles } from "./megacolumn-controller.js";
 import { flashPress } from "./press-flash.js";
 import type { SwListAddDetail } from "./sw-list.js";
 import type { LiveListPhase } from "./sw-live-list.js";
@@ -156,13 +152,17 @@ export class SwgLivecolumnDemo extends LitElement {
 	// biome-ignore lint/correctness/noUnusedPrivateClassMembers: wires itself via addController
 	private mc = new MegacolumnController(this);
 
-	@property() label = "ops queue";
+	@property()
+	label = "ops queue";
 	// Refill keeps the queue steady (a completed head is replaced); without it the
 	// queue drains to the empty state and only player prepends fill it.
-	@property({ type: Boolean }) refill = false;
+	@property({ type: Boolean })
+	refill = false;
 
-	@state() private tasks: QueueTask[] = [];
-	@state() private lastAction = "";
+	@state()
+	private tasks: QueueTask[] = [];
+	@state()
+	private lastAction = "";
 	private seq = 0;
 	private timer: number | undefined;
 
@@ -186,10 +186,9 @@ export class SwgLivecolumnDemo extends LitElement {
 
 	private newTask(origin: "player" | "assignment" = "assignment"): QueueTask {
 		this.seq += 1;
-		const chores =
-			origin === "player"
-				? ["survey the anomaly", "escort the hauler", "scout the far gate"]
-				: ["move to waypoint", "load ore", "unload struts", "dock", "refuel"];
+		const chores = origin === "player"
+			? ["survey the anomaly", "escort the hauler", "scout the far gate"]
+			: ["move to waypoint", "load ore", "unload struts", "dock", "refuel"];
 		return {
 			id: `t${this.seq}`,
 			label: `${chores[this.seq % chores.length]} (#${this.seq})`,
@@ -231,16 +230,16 @@ export class SwgLivecolumnDemo extends LitElement {
 		// own prepend/status rows decline everything here (the add-row's typing
 		// never reaches this handler; text entry dominates).
 		if (
-			detail.rowId === "prepend" ||
-			detail.rowId === "queue-header" ||
-			detail.rowId === "queue-add" ||
-			detail.rowId === "status"
-		)
+			detail.rowId === "prepend"
+			|| detail.rowId === "queue-header"
+			|| detail.rowId === "queue-add"
+			|| detail.rowId === "status"
+		) {
 			return;
+		}
 		const task = this.tasks.find((t) => t.id === detail.rowId);
 		const activate = detail.key === "Enter" || detail.key === " ";
-		const remove =
-			detail.key === "x" || detail.key === "Delete" || (activate && detail.position === "aux");
+		const remove = detail.key === "x" || detail.key === "Delete" || (activate && detail.position === "aux");
 		if (remove) {
 			// A ghost's task is already out of the data: removing again is a no-op,
 			// and saying so quietly beats pretending something happened.
@@ -249,10 +248,9 @@ export class SwgLivecolumnDemo extends LitElement {
 				// Acknowledge on the aux x itself -- the control whose activation removal
 				// IS -- in its own danger-tinted pressed style.  The glyph element survives
 				// the phase flip to ✓ (see renderTask), so the flash paints through it.
-				const aux =
-					e.target instanceof Element
-						? e.target.closest("[data-mc-row]")?.querySelector("[data-mc-aux]")
-						: null;
+				const aux = e.target instanceof Element
+					? e.target.closest("[data-mc-row]")?.querySelector("[data-mc-aux]")
+					: null;
 				if (aux instanceof HTMLElement) flashPress(aux);
 				this.tasks = this.tasks.filter((t) => t.id !== task.id);
 			}
@@ -260,8 +258,7 @@ export class SwgLivecolumnDemo extends LitElement {
 			return;
 		}
 		if (activate) {
-			this.lastAction =
-				task === undefined ? "inspected a completed task" : `${task.label} inspected`;
+			this.lastAction = task === undefined ? "inspected a completed task" : `${task.label} inspected`;
 			e.preventDefault();
 			// Acknowledge on the row's main control (inside the focused row).
 			if (e.target instanceof HTMLElement) flashPress(e.target);
@@ -283,10 +280,10 @@ export class SwgLivecolumnDemo extends LitElement {
 		return html`<div class=${classMap({ task: true, committed: done })}>
 			<span class="origin ${t.origin}">${t.origin}</span>
 			<span class="grow" data-mc-main data-info=${
-				done
-					? "a completed task, held briefly; Enter still inspects it"
-					: "a queued task; Enter inspects it"
-			}>${t.label}</span>
+			done
+				? "a completed task, held briefly; Enter still inspects it"
+				: "a queued task; Enter inspects it"
+		}>${t.label}</span>
 			<span class=${classMap({ glyph: true, gone: done })}
 				data-mc-aux=${done ? nothing : ""}
 				title=${done ? nothing : "remove"}
@@ -295,8 +292,7 @@ export class SwgLivecolumnDemo extends LitElement {
 		</div>`;
 	};
 
-	private renderQueueEmpty = (): TemplateResult =>
-		html`<div class="idle">queue empty (prepend something)</div>`;
+	private renderQueueEmpty = (): TemplateResult => html`<div class="idle">queue empty (prepend something)</div>`;
 
 	render(): TemplateResult {
 		return html`
@@ -356,48 +352,45 @@ export class SwgLivecolumnDemo extends LitElement {
 const entry: GalleryEntry = {
 	id: "sw-list",
 	title: "sw-list: the live megacolumn's list region",
-	description:
-		"The list region component, composed here into a megacolumn console: header row, live rows " +
-		"(sw-live-list inside), ghosted add-row, and the t/n list-scoped jumps -- the list norms as " +
-		"things the component does.  The queue churns and stays fully keyboard-drivable: ghost rows " +
-		"stay navigable while they hold (a stationary ghost is safe for the keyboard exactly as it " +
-		"is for the mouse); collapsing rows leave navigation and focus falls to a neighbor under a " +
-		"brief decisive-key lockout; key activity pins the FOCUSED ghost's geometry (the keyboard " +
-		"aims by identity, so only the aimed-at row needs holding still).  The mouse side is the " +
-		"default click-guard: motion proceeds under the pointer, and only a click on a row that is " +
-		"mid-displacement dies quietly.  This is the rehearsal for the infocard's task queue.",
+	description: "The list region component, composed here into a megacolumn console: header row, live rows "
+		+ "(sw-live-list inside), ghosted add-row, and the t/n list-scoped jumps -- the list norms as "
+		+ "things the component does.  The queue churns and stays fully keyboard-drivable: ghost rows "
+		+ "stay navigable while they hold (a stationary ghost is safe for the keyboard exactly as it "
+		+ "is for the mouse); collapsing rows leave navigation and focus falls to a neighbor under a "
+		+ "brief decisive-key lockout; key activity pins the FOCUSED ghost's geometry (the keyboard "
+		+ "aims by identity, so only the aimed-at row needs holding still).  The mouse side is the "
+		+ "default click-guard: motion proceeds under the pointer, and only a click on a row that is "
+		+ "mid-displacement dies quietly.  This is the rehearsal for the infocard's task queue.",
 	variants: [
 		{
-			caption:
-				"A steady queue: the head completes every few seconds (ghosts ✓, holds, collapses) and new " +
-				"assignment work arrives.  Try: arrow onto a task and just WORK it -- the ghost under your " +
-				"focus holds while you keep pressing keys (the key-activity pin), and collapses moments after " +
-				"you pause; ghosts you are NOT on collapse on their own schedule, each at its own moment.  " +
-				"Removals flash the × itself, danger-tinted, before the row ghosts (x/Delete removes a task from anywhere in its row; " +
-				"right-arrow reaches the ×; Enter on a ghost still inspects it), and if focus was on a ghost " +
-				"when it collapsed, focus falls to the next row under a brief decisive-key lockout -- hammer " +
-				"Enter across the collapse and note nothing mis-fires.  The LIST NORMS are sw-list's own " +
-				"behavior now: a header row (arrow onto it and the " +
-				"whole region washes -- group identity without a nested container; i there describes the list as " +
-				"a list) and the add-row at the tail.  Two ways to add, both earning the direct marking under " +
-				"the easy-undo doctrine: the prepend " +
-				"button is hotkey-direct -- the exception to letters-move-focus: p acts immediately from any row " +
-				"(focus following to the orders row), justified because a prepend is cheaply undone -- x the new " +
-				"head and it is gone.  The add-row's a dives straight into typing (direct on a text row means " +
-				"what Enter means there), justified because nothing touches the queue until the Enter commit " +
-				"(the owner hears a cancelable sw-list-add and appends; watch the task animate in) and Escape " +
-				"abandons the draft.  While focus is anywhere " +
-				"in the list -- header, tasks, add-row -- t jumps back up to the header and n jumps past the " +
-				"list to the next stop after it: list-scoped keys sw-list wires itself, deliberately not " +
-				"gold-marked (they mean the same thing in every list).",
+			caption: "A steady queue: the head completes every few seconds (ghosts ✓, holds, collapses) and new "
+				+ "assignment work arrives.  Try: arrow onto a task and just WORK it -- the ghost under your "
+				+ "focus holds while you keep pressing keys (the key-activity pin), and collapses moments after "
+				+ "you pause; ghosts you are NOT on collapse on their own schedule, each at its own moment.  "
+				+ "Removals flash the × itself, danger-tinted, before the row ghosts (x/Delete removes a task from anywhere in its row; "
+				+ "right-arrow reaches the ×; Enter on a ghost still inspects it), and if focus was on a ghost "
+				+ "when it collapsed, focus falls to the next row under a brief decisive-key lockout -- hammer "
+				+ "Enter across the collapse and note nothing mis-fires.  The LIST NORMS are sw-list's own "
+				+ "behavior now: a header row (arrow onto it and the "
+				+ "whole region washes -- group identity without a nested container; i there describes the list as "
+				+ "a list) and the add-row at the tail.  Two ways to add, both earning the direct marking under "
+				+ "the easy-undo doctrine: the prepend "
+				+ "button is hotkey-direct -- the exception to letters-move-focus: p acts immediately from any row "
+				+ "(focus following to the orders row), justified because a prepend is cheaply undone -- x the new "
+				+ "head and it is gone.  The add-row's a dives straight into typing (direct on a text row means "
+				+ "what Enter means there), justified because nothing touches the queue until the Enter commit "
+				+ "(the owner hears a cancelable sw-list-add and appends; watch the task animate in) and Escape "
+				+ "abandons the draft.  While focus is anywhere "
+				+ "in the list -- header, tasks, add-row -- t jumps back up to the header and n jumps past the "
+				+ "list to the next stop after it: list-scoped keys sw-list wires itself, deliberately not "
+				+ "gold-marked (they mean the same thing in every list).",
 			render: () => html`<swg-livecolumn-demo refill></swg-livecolumn-demo>`,
 		},
 		{
-			caption:
-				"Drain to empty: nothing refills this queue.  Prepend a few tasks (p), watch them complete and " +
-				"drain away; the empty state is the owner's renderEmpty render-prop (mega mode has no slots).  " +
-				"Focus stays sane through the last row's death: the column itself keeps focus and the arrows " +
-				"revive it the moment a row exists again.",
+			caption: "Drain to empty: nothing refills this queue.  Prepend a few tasks (p), watch them complete and "
+				+ "drain away; the empty state is the owner's renderEmpty render-prop (mega mode has no slots).  "
+				+ "Focus stays sane through the last row's death: the column itself keeps focus and the arrows "
+				+ "revive it the moment a row exists again.",
 			render: () => html`<swg-livecolumn-demo label="drain queue"></swg-livecolumn-demo>`,
 		},
 	],
diff --git a/packages/yukey/src/sw-list.test.ts b/packages/yukey/src/sw-list.test.ts
index c49f0e2..d63c578 100644
--- a/packages/yukey/src/sw-list.test.ts
+++ b/packages/yukey/src/sw-list.test.ts
@@ -42,11 +42,16 @@ class SwtListOwner extends LitElement {
 	static styles = [megacolumnStyles];
 	// biome-ignore lint/correctness/noUnusedPrivateClassMembers: wires itself via addController
 	private mc = new MegacolumnController(this);
-	@property({ attribute: false }) items: Item[] = [];
-	@property() header = "the test list";
-	@property({ attribute: false }) renderHeader?: () => TemplateResult;
-	@property({ type: Boolean }) addable = true;
-	@property({ type: Boolean }) withAfter = true;
+	@property({ attribute: false })
+	items: Item[] = [];
+	@property()
+	header = "the test list";
+	@property({ attribute: false })
+	renderHeader?: () => TemplateResult;
+	@property({ type: Boolean })
+	addable = true;
+	@property({ type: Boolean })
+	withAfter = true;
 
 	render(): TemplateResult {
 		return html`<div data-mc-container aria-label="test column">
@@ -60,8 +65,7 @@ class SwtListOwner extends LitElement {
 				add-info="appends a test item"
 				.items=${this.items}
 				.keyOf=${(it: Item) => it.id}
-				.renderItem=${(it: Item, _phase: LiveListPhase) =>
-					html`<div class="row-content">${it.label}</div>`}
+				.renderItem=${(it: Item, _phase: LiveListPhase) => html`<div class="row-content">${it.label}</div>`}
 			></sw-list>
 			${this.withAfter ? html`<div data-mc-row="after">after</div>` : nothing}
 		</div>`;
@@ -415,7 +419,7 @@ describe("the list-scoped keys (t and n)", () => {
 			o.items = items("a");
 		});
 		await rig.settle();
-		rig.root.querySelector('[data-mc-row="before"]')?.setAttribute("data-mc-hotkey", "t");
+		rig.root.querySelector("[data-mc-row=\"before\"]")?.setAttribute("data-mc-hotkey", "t");
 		rig.container.dispatchEvent(new CustomEvent("megacolumn-rescan", { bubbles: true }));
 		await focusItem(rig, "a");
 		rig.key("t");
diff --git a/packages/yukey/src/sw-list.ts b/packages/yukey/src/sw-list.ts
index 3f4ce61..02a56b7 100644
--- a/packages/yukey/src/sw-list.ts
+++ b/packages/yukey/src/sw-list.ts
@@ -172,19 +172,32 @@ const swListStyles = css`
 
 @customElement("sw-list")
 export class SwList<T = unknown> extends LitElement {
-	@property() name = "list";
-	@property() header = "";
-	@property({ attribute: "header-info" }) headerInfo = "";
-	@property({ attribute: false }) renderHeader?: () => TemplateResult;
-	@property({ type: Boolean }) addable = false;
-	@property({ attribute: "add-label" }) addLabel = "add…";
-	@property({ attribute: "add-hotkey" }) addHotkey = "a";
-	@property({ attribute: "add-info" }) addInfo = "";
-	@property({ attribute: false }) items: T[] = [];
-	@property({ attribute: false }) keyOf!: (item: T) => string;
-	@property({ attribute: false }) renderItem!: (item: T, phase: LiveListPhase) => TemplateResult;
-	@property({ attribute: false }) renderEmpty?: () => TemplateResult;
-	@property() resetKey = "";
+	@property()
+	name = "list";
+	@property()
+	header = "";
+	@property({ attribute: "header-info" })
+	headerInfo = "";
+	@property({ attribute: false })
+	renderHeader?: () => TemplateResult;
+	@property({ type: Boolean })
+	addable = false;
+	@property({ attribute: "add-label" })
+	addLabel = "add…";
+	@property({ attribute: "add-hotkey" })
+	addHotkey = "a";
+	@property({ attribute: "add-info" })
+	addInfo = "";
+	@property({ attribute: false })
+	items: T[] = [];
+	@property({ attribute: false })
+	keyOf!: (item: T) => string;
+	@property({ attribute: false })
+	renderItem!: (item: T, phase: LiveListPhase) => TemplateResult;
+	@property({ attribute: false })
+	renderEmpty?: () => TemplateResult;
+	@property()
+	resetKey = "";
 
 	// The column this list belongs to; n walks its rows.  Set at connect (which
 	// fails closed when there is none).
@@ -201,10 +214,11 @@ export class SwList<T = unknown> extends LitElement {
 
 	// Light DOM, no slots (see the header).  Styles adopt into the owner's root.
 	protected override createRenderRoot(): HTMLElement {
-		if (this.children.length > 0)
+		if (this.children.length > 0) {
 			throw new Error(
 				"sw-list: renders into the light DOM and has no slots; rows come from renderItem",
 			);
+		}
 		adoptIntoRoot(this.getRootNode(), swListStyles, "data-sw-list");
 		return this;
 	}
@@ -222,15 +236,17 @@ export class SwList<T = unknown> extends LitElement {
 	}
 
 	override willUpdate(): void {
-		if (!/^[a-z0-9-]+$/i.test(this.name))
+		if (!/^[a-z0-9-]+$/i.test(this.name)) {
 			throw new Error(
-				`sw-list: name "${this.name}" must be a non-empty [a-z0-9-]+ token ` +
-					"(it namespaces the structural row ids)",
+				`sw-list: name "${this.name}" must be a non-empty [a-z0-9-]+ token `
+					+ "(it namespaces the structural row ids)",
 			);
-		if (this.addable && !/^[a-z0-9]$/i.test(this.addHotkey))
+		}
+		if (this.addable && !/^[a-z0-9]$/i.test(this.addHotkey)) {
 			throw new Error(
 				`sw-list: add-hotkey "${this.addHotkey}" must be a single [a-z0-9] character`,
 			);
+		}
 	}
 
 	protected override updated(): void {
@@ -327,14 +343,14 @@ export class SwList<T = unknown> extends LitElement {
 	override render(): TemplateResult {
 		return html`
 			${
-				this.header === ""
-					? nothing
-					: html`<div
+			this.header === ""
+				? nothing
+				: html`<div
 							data-mc-row="${this.name}-header"
 							class="sw-list-header"
 							data-info=${this.#composedHeaderInfo()}
 						>${this.renderHeader?.() ?? this.header}</div>`
-			}
+		}
 			<sw-live-list
 				megacolumn
 				.items=${this.items}
@@ -344,8 +360,8 @@ export class SwList<T = unknown> extends LitElement {
 				.resetKey=${this.resetKey}
 			></sw-live-list>
 			${
-				this.addable
-					? html`<div
+			this.addable
+				? html`<div
 							data-mc-row="${this.name}-add"
 							class="sw-list-add"
 							data-info=${this.#composedAddInfo()}
@@ -353,8 +369,8 @@ export class SwList<T = unknown> extends LitElement {
 							<span data-mc-hotkey=${this.addHotkey} data-mc-hotkey-direct>+ ${this.#markedAddLabel()}</span>
 							<input data-mc-input type="text" @keydown=${this.#onAddKeydown} />
 						</div>`
-					: nothing
-			}
+				: nothing
+		}
 		`;
 	}
 
@@ -369,9 +385,8 @@ export class SwList<T = unknown> extends LitElement {
 
 	#composedAddInfo(): string {
 		const top = this.header === "" ? "the list's top" : "the list's header";
-		const norms =
-			`${this.addHotkey} or Enter dives into typing, Enter commits, Escape abandons; ` +
-			`t jumps back to ${top}`;
+		const norms = `${this.addHotkey} or Enter dives into typing, Enter commits, Escape abandons; `
+			+ `t jumps back to ${top}`;
 		return this.addInfo === "" ? norms : `${this.addInfo}; ${norms}`;
 	}
 
@@ -381,7 +396,9 @@ export class SwList<T = unknown> extends LitElement {
 	#markedAddLabel(): TemplateResult {
 		const idx = this.addLabel.toLowerCase().indexOf(this.addHotkey.toLowerCase());
 		if (idx === -1) return html`${this.addLabel} <span class="sw-list-hk">${this.addHotkey}</span>`;
-		return html`${this.addLabel.slice(0, idx)}<span class="sw-list-hk">${this.addLabel[idx]}</span>${this.addLabel.slice(idx + 1)}`;
+		return html`${this.addLabel.slice(0, idx)}<span class="sw-list-hk">${this.addLabel[idx]}</span>${
+			this.addLabel.slice(idx + 1)
+		}`;
 	}
 }
 
diff --git a/packages/yukey/src/sw-live-list.gallery.ts b/packages/yukey/src/sw-live-list.gallery.ts
index 3e6178f..3bac560 100644
--- a/packages/yukey/src/sw-live-list.gallery.ts
+++ b/packages/yukey/src/sw-live-list.gallery.ts
@@ -49,8 +49,10 @@ const keyOf = (item: DemoRow): string => item.id;
 // one enters at the tail; the interplay with a hovering pointer is the demo.
 @customElement("swg-live-list-churn")
 export class SwgLiveListChurn extends LitElement {
-	@property() pointerSafety: "click-guard" | "freeze" = "click-guard";
-	@state() private items: DemoRow[] = [];
+	@property()
+	pointerSafety: "click-guard" | "freeze" = "click-guard";
+	@state()
+	private items: DemoRow[] = [];
 	private seq = 0;
 	private timer: number | undefined;
 
@@ -89,35 +91,33 @@ export class SwgLiveListChurn extends LitElement {
 const entry: GalleryEntry = {
 	id: "sw-live-list",
 	title: "sw-live-list",
-	description:
-		"A keyed live list whose rows vanish safely under a pointer.  " +
-		"The owner supplies keying and rendering; this component owns row lifecycle: " +
-		"departed rows ghost and hold before collapsing, insertions grow in.  " +
-		"Click safety is target-scoped by default (pointerSafety: click-guard): motion proceeds " +
-		"under the pointer, and a click is swallowed only while its target row is mid-displacement " +
-		"(or was, within the last 50ms) -- stale clicks die, everything else stays live.  " +
-		"Freeze mode is the opt-in alternative: geometry holds still under the pointer instead.",
+	description: "A keyed live list whose rows vanish safely under a pointer.  "
+		+ "The owner supplies keying and rendering; this component owns row lifecycle: "
+		+ "departed rows ghost and hold before collapsing, insertions grow in.  "
+		+ "Click safety is target-scoped by default (pointerSafety: click-guard): motion proceeds "
+		+ "under the pointer, and a click is swallowed only while its target row is mid-displacement "
+		+ "(or was, within the last 50ms) -- stale clicks die, everything else stays live.  "
+		+ "Freeze mode is the opt-in alternative: geometry holds still under the pointer instead.",
 	variants: [
 		{
-			caption:
-				"Self-churning queue, default click safety (click-guard): the head row completes every few " +
-				"seconds and new work enters at the tail.  Watch a row ghost (✓, dimmed), hold, then collapse -- " +
-				"with the pointer resting inside, nothing waits for it.  Try clicking a row the instant one above " +
-				"it collapses: the click on the still-moving row dies quietly (no surprise target), while rows " +
-				"above the motion answer clicks throughout.",
+			caption: "Self-churning queue, default click safety (click-guard): the head row completes every few "
+				+ "seconds and new work enters at the tail.  Watch a row ghost (✓, dimmed), hold, then collapse -- "
+				+ "with the pointer resting inside, nothing waits for it.  Try clicking a row the instant one above "
+				+ "it collapses: the click on the still-moving row dies quietly (no surprise target), while rows "
+				+ "above the motion answer clicks throughout.",
 			render: () => html`<swg-live-list-churn></swg-live-list-churn>`,
 		},
 		{
-			caption:
-				"The same churn in freeze mode (pointerSafety: freeze, the opt-in): rest the pointer inside " +
-				"the list and geometry freezes -- ghosts hold and insertions wait invisibly until the pointer " +
-				"leaves, and the deferred motion plays out one beat apart.  For surfaces that want quiescence " +
-				"under the pointer; the price is a world that visibly waits for the mouse.",
+			caption: "The same churn in freeze mode (pointerSafety: freeze, the opt-in): rest the pointer inside "
+				+ "the list and geometry freezes -- ghosts hold and insertions wait invisibly until the pointer "
+				+ "leaves, and the deferred motion plays out one beat apart.  For surfaces that want quiescence "
+				+ "under the pointer; the price is a world that visibly waits for the mouse.",
 			render: () => html`<swg-live-list-churn pointerSafety="freeze"></swg-live-list-churn>`,
 		},
 		{
 			caption: "Empty state: the owner provides the empty slot's content.",
-			render: () => html`<sw-live-list
+			render: () =>
+				html`<sw-live-list
 				.items=${[] as DemoRow[]}
 				.keyOf=${keyOf}
 				.renderItem=${renderRow}
diff --git a/packages/yukey/src/sw-live-list.test.ts b/packages/yukey/src/sw-live-list.test.ts
index cdc8a52..07334ab 100644
--- a/packages/yukey/src/sw-live-list.test.ts
+++ b/packages/yukey/src/sw-live-list.test.ts
@@ -101,7 +101,7 @@ const fire = (el: Element, type: "click" | "mousedown"): void => {
 	el.dispatchEvent(new MouseEvent(type, { bubbles: true, composed: true, cancelable: true }));
 };
 
-const countItemClicks = (list: SwLiveList<Item>): (() => number) => {
+const countItemClicks = (list: SwLiveList<Item>): () => number => {
 	let n = 0;
 	list.addEventListener("item-click", () => {
 		n += 1;
@@ -455,8 +455,7 @@ describe("click-guard (the default pointerSafety): motion proceeds, stale clicks
 		vi.advanceTimersByTime(list.holdMs); // b collapsing
 		await list.updateComplete;
 		vi.advanceTimersByTime(100);
-		const slot = (key: string): Element | null =>
-			list.shadowRoot?.querySelector(`[data-key="${key}"]`) ?? null;
+		const slot = (key: string): Element | null => list.shadowRoot?.querySelector(`[data-key="${key}"]`) ?? null;
 		const c = slot("c");
 		const a = slot("a");
 		if (c === null || a === null) throw new Error("rows missing");
diff --git a/packages/yukey/src/sw-live-list.ts b/packages/yukey/src/sw-live-list.ts
index 2a702be..a71a99e 100644
--- a/packages/yukey/src/sw-live-list.ts
+++ b/packages/yukey/src/sw-live-list.ts
@@ -228,39 +228,50 @@ export class SwLiveList<T = unknown> extends LitElement {
 		lifecycleStyles,
 	];
 
-	@property({ attribute: false }) items: T[] = [];
-	@property({ attribute: false }) keyOf!: (item: T) => string;
-	@property({ attribute: false }) renderItem!: (item: T, phase: LiveListPhase) => TemplateResult;
+	@property({ attribute: false })
+	items: T[] = [];
+	@property({ attribute: false })
+	keyOf!: (item: T) => string;
+	@property({ attribute: false })
+	renderItem!: (item: T, phase: LiveListPhase) => TemplateResult;
 	// The empty state, rendered when no row is visible.  In shadow mode the default
 	// slot alternative still works; mega mode has no shadow root, so this render-prop
 	// is the only way there.
-	@property({ attribute: false }) renderEmpty?: () => TemplateResult;
+	@property({ attribute: false })
+	renderEmpty?: () => TemplateResult;
 	// Owner-supplied styles for rendered items, adopted into this shadow root
 	// (the owner's own stylesheet can't reach in here).  Shadow mode only:
 	// mega-mode rows live in the owner's tree, already inside its stylesheet's reach.
-	@property({ attribute: false }) itemStyles?: CSSResult;
+	@property({ attribute: false })
+	itemStyles?: CSSResult;
 	// Identity of the data source.  When it changes, all row state drops and the list
 	// renders fresh -- no exit animations for the old identity's rows.
-	@property() resetKey = "";
+	@property()
+	resetKey = "";
 	// Which strategy answers the click-safety goal (see the header).  "click-guard"
 	// (the default): motion proceeds and stale clicks on moving-target rows are
 	// swallowed.  "freeze": geometry defers while the pointer is inside the host.
 	// Runtime-togglable: a host may raise "freeze" as a quasi-lock and drop it again;
 	// leaving freeze releases whatever the hover pin was deferring.
-	@property({ attribute: "pointer-safety" }) pointerSafety: "click-guard" | "freeze" =
-		"click-guard";
+	@property({ attribute: "pointer-safety" })
+	pointerSafety: "click-guard" | "freeze" = "click-guard";
 	// Timing knobs, defaulted per the vanishing-rows study.
 	// (graceMs is freeze-mode machinery: how long the pointer-events lockout
 	// outlives the last motion.  Click-guard's tail is CLICK_GUARD_MS, a constant:
 	// it is a click-race window, not a feel knob.)
-	@property({ type: Number }) holdMs = 1200;
-	@property({ type: Number }) collapseMs = 300;
-	@property({ type: Number }) graceMs = 200;
-	@property({ type: Number }) enterMs = 300;
+	@property({ type: Number })
+	holdMs = 1200;
+	@property({ type: Number })
+	collapseMs = 300;
+	@property({ type: Number })
+	graceMs = 200;
+	@property({ type: Number })
+	enterMs = 300;
 	// How long the key-activity pin outlives the last keydown (mega mode; see header).
 	// Short: it bridges the gaps inside one burst of typing, and the focused row's
 	// protection should die almost as soon as the fingers stop.
-	@property({ type: Number }) keyPinMs = 200;
+	@property({ type: Number })
+	keyPinMs = 200;
 
 	// Row state, insertion-ordered (= display order).  Deliberately not @state:
 	// mutations call requestUpdate explicitly, and items changes re-render anyway.
@@ -283,7 +294,8 @@ export class SwLiveList<T = unknown> extends LitElement {
 	private keyPinned = false;
 	private keyPinTimer: number | undefined;
 
-	@query(".sw-ll-items") private itemsEl?: HTMLElement;
+	@query(".sw-ll-items")
+	private itemsEl?: HTMLElement;
 
 	constructor() {
 		super();
@@ -309,11 +321,12 @@ export class SwLiveList<T = unknown> extends LitElement {
 	protected override createRenderRoot(): HTMLElement | DocumentFragment {
 		if (!this.hasAttribute("megacolumn")) return super.createRenderRoot();
 		this.mega = true;
-		if (this.children.length > 0)
+		if (this.children.length > 0) {
 			throw new Error(
-				"sw-live-list: megacolumn mode renders into the light DOM and has no slots; " +
-					"use the renderEmpty render-prop",
+				"sw-live-list: megacolumn mode renders into the light DOM and has no slots; "
+					+ "use the renderEmpty render-prop",
 			);
+		}
 		adoptIntoRoot(this.getRootNode(), megaModeStyles, "data-sw-live-list");
 		return this;
 	}
@@ -324,8 +337,9 @@ export class SwLiveList<T = unknown> extends LitElement {
 			// The key-activity pin listens on the column's one real focusable: every
 			// keydown a user aims anywhere in the column lands there first.
 			const container = this.closest<HTMLElement>("[data-mc-container]");
-			if (container === null)
+			if (container === null) {
 				throw new Error("sw-live-list: megacolumn mode outside any [data-mc-container]");
+			}
 			this.megaContainer = container;
 			container.addEventListener("keydown", this.onKeyActivity);
 		}
@@ -343,16 +357,18 @@ export class SwLiveList<T = unknown> extends LitElement {
 	}
 
 	override willUpdate(changed: PropertyValues<this>): void {
-		if (this.mega && this.itemStyles !== undefined)
+		if (this.mega && this.itemStyles !== undefined) {
 			throw new Error(
-				"sw-live-list: itemStyles has no effect in megacolumn mode; " +
-					"style rows from the owner's own stylesheet",
+				"sw-live-list: itemStyles has no effect in megacolumn mode; "
+					+ "style rows from the owner's own stylesheet",
 			);
-		if (this.pointerSafety !== "click-guard" && this.pointerSafety !== "freeze")
+		}
+		if (this.pointerSafety !== "click-guard" && this.pointerSafety !== "freeze") {
 			throw new Error(
-				`sw-live-list: unknown pointerSafety "${this.pointerSafety}" ` +
-					'(the modes are "click-guard" and "freeze")',
+				`sw-live-list: unknown pointerSafety "${this.pointerSafety}" `
+					+ "(the modes are \"click-guard\" and \"freeze\")",
 			);
+		}
 		// Leaving freeze mode releases whatever the hover pin deferred (the pointer
 		// may never leave); entering it starts deferring from here on.  releaseDeferred
 		// re-checks every deferral condition itself, so one unconditional call covers
@@ -667,10 +683,10 @@ export class SwLiveList<T = unknown> extends LitElement {
 				this.beginEnter(row);
 				changed = true;
 			} else if (
-				row.phase === "committed" &&
-				row.holdExpired &&
-				row.timer === undefined && // not already scheduled by an earlier release
-				!this.collapseDeferred(row)
+				row.phase === "committed"
+				&& row.holdExpired
+				&& row.timer === undefined // not already scheduled by an earlier release
+				&& !this.collapseDeferred(row)
 			) {
 				backlog.push(row);
 			}
@@ -755,11 +771,13 @@ export class SwLiveList<T = unknown> extends LitElement {
 				style=${vars}
 				tabindex="-1"
 			>
-				${repeat(
-					rowList,
-					(r) => r.key,
-					(r) => this.renderRow(r),
-				)}
+				${
+			repeat(
+				rowList,
+				(r) => r.key,
+				(r) => this.renderRow(r),
+			)
+		}
 			</div>
 			${empty}
 		`;
@@ -770,8 +788,7 @@ export class SwLiveList<T = unknown> extends LitElement {
 		// and (mega mode) is also what the controller's scan skips: one attribute,
 		// closed to sight and closed to navigation together.
 		const closed = row.phase === "pending" || row.phase === "collapsing";
-		const phase: LiveListPhase =
-			row.phase === "committed" || row.phase === "collapsing" ? "committed" : "live";
+		const phase: LiveListPhase = row.phase === "committed" || row.phase === "collapsing" ? "committed" : "live";
 		// Mega mode stamps row identity for the megacolumn scan (see the header).
 		// classMap (not string interpolation) because the megacolumn controller stamps
 		// its focus classes on these elements imperatively, and an interpolated class
diff --git a/packages/yukey/src/tab-cycle.test.ts b/packages/yukey/src/tab-cycle.test.ts
index a8f8cef..e610f51 100644
--- a/packages/yukey/src/tab-cycle.test.ts
+++ b/packages/yukey/src/tab-cycle.test.ts
@@ -6,12 +6,7 @@
 // attribute-based (see the header of tab-cycle.ts).
 
 import { beforeEach, describe, expect, it } from "vitest";
-import {
-	containTab,
-	deepActiveElement,
-	deepTabbables,
-	installTabContainment,
-} from "./tab-cycle.js";
+import { containTab, deepActiveElement, deepTabbables, installTabContainment } from "./tab-cycle.js";
 
 function host(innerHtml: string): HTMLElement {
 	const el = document.createElement("div");
diff --git a/packages/yukey/src/tokens.gallery.ts b/packages/yukey/src/tokens.gallery.ts
index f9aaecf..ec6c03e 100644
--- a/packages/yukey/src/tokens.gallery.ts
+++ b/packages/yukey/src/tokens.gallery.ts
@@ -137,7 +137,8 @@ export class SwgTokens extends LitElement {
 		}
 	`;
 
-	@property() face: "palette" | "spacing" | "type" | "focus" = "palette";
+	@property()
+	face: "palette" | "spacing" | "type" | "focus" = "palette";
 
 	render(): TemplateResult {
 		switch (this.face) {
@@ -165,13 +166,16 @@ export class SwgTokens extends LitElement {
 		const colorGroups = tokenGroups
 			.map((g) => ({ ...g, tokens: g.tokens.filter((t) => t.kind === "color") }))
 			.filter((g) => g.tokens.length > 0);
-		return html`${colorGroups.map(
-			(g) => html`<div class="group">
+		return html`${
+			colorGroups.map(
+				(g) =>
+					html`<div class="group">
 				<div class="group-title">${g.title}</div>
 				<div class="group-about">${g.about}</div>
 				${g.tokens.map((t) => SwgTokens.paletteRow(t))}
 			</div>`,
-		)}`;
+			)
+		}`;
 	}
 
 	private renderSpacing(): TemplateResult {
@@ -182,14 +186,17 @@ export class SwgTokens extends LitElement {
 			<div class="group">
 				<div class="group-title">${spacing.title}</div>
 				<div class="group-about">${spacing.about}</div>
-				${spacing.tokens.map(
-					(t) => html`<div class="trow">
+				${
+			spacing.tokens.map(
+				(t) =>
+					html`<div class="trow">
 						<div><div class="bar" style="width: var(${t.name});"></div></div>
 						<span class="tname">${t.name}</span>
 						<span class="tvalue">${t.value}</span>
 						<span class="tabout">${t.about}</span>
 					</div>`,
-				)}
+			)
+		}
 			</div>
 			<div class="group">
 				<div class="group-title">${hit.title}</div>
@@ -216,23 +223,29 @@ export class SwgTokens extends LitElement {
 		return html`<div class="group">
 			<div class="group-title">${type.title}</div>
 			<div class="group-about">${type.about}</div>
-			${sizes.map(
-				(t) => html`<div class="trow">
+			${
+			sizes.map(
+				(t) =>
+					html`<div class="trow">
 					<span class="tvalue">${t.value}</span>
 					<span class="tname">${t.name}</span>
 					<span class="specimen" style="font-size: var(${t.name});">Kestrel Reach 74/74</span>
 					<span class="tabout">${t.about}</span>
 				</div>`,
-			)}
-			${rest.map(
-				(t) => html`<div class="trow">
+			)
+		}
+			${
+			rest.map(
+				(t) =>
+					html`<div class="trow">
 					<span class="tvalue">${t.value}</span>
 					<span class="tname">${t.name}</span>
 					<span class="specimen" style="letter-spacing: var(${t.name}); text-transform: uppercase;"
 						>Kestrel Reach</span>
 					<span class="tabout">${t.about}</span>
 				</div>`,
-			)}
+			)
+		}
 		</div>`;
 	}
 
@@ -269,13 +282,16 @@ export class SwgTokens extends LitElement {
 			<div class="group">
 				<div class="group-title">${motion.title}</div>
 				<div class="group-about">${motion.about}</div>
-				${motion.tokens.map(
-					(t) => html`<div class="trow">
+				${
+			motion.tokens.map(
+				(t) =>
+					html`<div class="trow">
 						<span class="tvalue">${t.value}</span>
 						<span class="tname">${t.name}</span>
 						<span class="tabout">${t.about}</span>
 					</div>`,
-				)}
+			)
+		}
 				<div class="demo-row">
 					<sw-button label="press me" hotkey="p"></sw-button>
 					<span class="note">
@@ -292,16 +308,14 @@ export class SwgTokens extends LitElement {
 const entry: GalleryEntry = {
 	id: "tokens",
 	title: "design tokens",
-	description:
-		"The visual vocabulary everything else is built from: --sw-* custom properties, " +
-		"rendered live from their definitions in tokens.ts.  Names are semantic roles and are " +
-		"the durable part; values are the iteration surface -- edit tokens.ts, rebuild, " +
-		"and this page shows the new truth.  Intent notes live in docs/precepts-aesthetics.md.",
+	description: "The visual vocabulary everything else is built from: --sw-* custom properties, "
+		+ "rendered live from their definitions in tokens.ts.  Names are semantic roles and are "
+		+ "the durable part; values are the iteration surface -- edit tokens.ts, rebuild, "
+		+ "and this page shows the new truth.  Intent notes live in docs/precepts-aesthetics.md.",
 	variants: [
 		{
-			caption:
-				"The palette, by role.  Swatches sit on a checkerboard so translucent washes read as " +
-				"translucent; wash values derive from their base color, so retinting a base retints its washes.",
+			caption: "The palette, by role.  Swatches sit on a checkerboard so translucent washes read as "
+				+ "translucent; wash values derive from their base color, so retinting a base retints its washes.",
 			wide: true,
 			render: () => html`<swg-tokens face="palette"></swg-tokens>`,
 		},
@@ -316,9 +330,8 @@ const entry: GalleryEntry = {
 			render: () => html`<swg-tokens face="type"></swg-tokens>`,
 		},
 		{
-			caption:
-				"Keyboard visibility: the focus ring (try tabbing through), the two hotkey-marking forms, " +
-				"and the activation flash (press things; watch them acknowledge).",
+			caption: "Keyboard visibility: the focus ring (try tabbing through), the two hotkey-marking forms, "
+				+ "and the activation flash (press things; watch them acknowledge).",
 			wide: true,
 			render: () => html`<swg-tokens face="focus"></swg-tokens>`,
 		},
diff --git a/packages/yukey/src/tokens.ts b/packages/yukey/src/tokens.ts
index 10d4cb4..42ecbce 100644
--- a/packages/yukey/src/tokens.ts
+++ b/packages/yukey/src/tokens.ts
@@ -53,8 +53,7 @@ export const tokenGroups: TokenGroup[] = [
 				name: "--sw-bg-panel-solid",
 				value: "#0a1220",
 				kind: "color",
-				about:
-					"opaque panel surface: cards, sticky headers, toasts -- anywhere bleed-through would muddy",
+				about: "opaque panel surface: cards, sticky headers, toasts -- anywhere bleed-through would muddy",
 			},
 			{
 				name: "--sw-bg-panel",
@@ -141,9 +140,8 @@ export const tokenGroups: TokenGroup[] = [
 	{
 		id: "accent",
 		title: "accent",
-		about:
-			"The interactive blue family: selected and hovered things, meters, active chrome.  " +
-			"Washes derive from the accent, so retinting it retints them.",
+		about: "The interactive blue family: selected and hovered things, meters, active chrome.  "
+			+ "Washes derive from the accent, so retinting it retints them.",
 		tokens: [
 			{
 				name: "--sw-accent",
@@ -161,10 +159,9 @@ export const tokenGroups: TokenGroup[] = [
 				name: "--sw-wash-pressed",
 				value: "hsl(from var(--sw-accent) h calc(s * 1.5) calc(l * 1.2) / 0.5)",
 				kind: "color",
-				about:
-					"background wash for a control being activated: held under a pointer, " +
-					"or flashing for --sw-press-ms after triggering.  Deliberately more saturated " +
-					"and brighter than the accent it derives from: activation must outrank selection at a glance",
+				about: "background wash for a control being activated: held under a pointer, "
+					+ "or flashing for --sw-press-ms after triggering.  Deliberately more saturated "
+					+ "and brighter than the accent it derives from: activation must outrank selection at a glance",
 			},
 			{
 				name: "--sw-wash-selected",
@@ -195,8 +192,7 @@ export const tokenGroups: TokenGroup[] = [
 				name: "--sw-good",
 				value: "#8fd4c0",
 				kind: "color",
-				about:
-					"the working-economy teal: producing, positive flow, and the map's route system (promises of movement)",
+				about: "the working-economy teal: producing, positive flow, and the map's route system (promises of movement)",
 			},
 			{
 				name: "--sw-warn",
@@ -245,17 +241,15 @@ export const tokenGroups: TokenGroup[] = [
 	{
 		id: "focus",
 		title: "keyboard & focus",
-		about:
-			"The guidelines demand focus be visible and hotkeys be marked; these tokens are how.  " +
-			"The focus-model and button stages consume them heavily.",
+		about: "The guidelines demand focus be visible and hotkeys be marked; these tokens are how.  "
+			+ "The focus-model and button stages consume them heavily.",
 		tokens: [
 			{
 				name: "--sw-hotkey-accent",
 				value: "#e8c86a",
 				kind: "color",
-				about:
-					"the significant color for hotkey marking: the letter-in-label tint, the 70%-offset hotkey slot.  " +
-					"Deliberately the only gold in the palette: hotkeys must pop",
+				about: "the significant color for hotkey marking: the letter-in-label tint, the 70%-offset hotkey slot.  "
+					+ "Deliberately the only gold in the palette: hotkeys must pop",
 			},
 			{
 				name: "--sw-selection",
@@ -291,37 +285,33 @@ export const tokenGroups: TokenGroup[] = [
 				name: "--sw-scroll-peek",
 				value: "28px",
 				kind: "size",
-				about:
-					"how much neighbor stays visible past the focused row when navigation scrolls: " +
-					"about one row, so the next row is always in view and a scroller's edge is " +
-					"only flush when the list truly ends there",
+				about: "how much neighbor stays visible past the focused row when navigation scrolls: "
+					+ "about one row, so the next row is always in view and a scroller's edge is "
+					+ "only flush when the list truly ends there",
 			},
 		],
 	},
 	{
 		id: "motion",
 		title: "motion",
-		about:
-			"Interaction-feedback durations.  System parameters, not per-element choices: " +
-			"slowing one here slows it everywhere, which is what an accessibility mode needs.",
+		about: "Interaction-feedback durations.  System parameters, not per-element choices: "
+			+ "slowing one here slows it everywhere, which is what an accessibility mode needs.",
 		tokens: [
 			{
 				name: "--sw-press-ms",
 				value: "140ms",
 				kind: "duration",
-				about:
-					"how long the activation flash lingers on a triggered control.  " +
-					"Below ~100ms a flash risks being missed outright; 180ms already reads " +
-					"as sluggish.  Tuned by hand at the 2026-08-23 playtest",
+				about: "how long the activation flash lingers on a triggered control.  "
+					+ "Below ~100ms a flash risks being missed outright; 180ms already reads "
+					+ "as sluggish.  Tuned by hand at the 2026-08-23 playtest",
 			},
 		],
 	},
 	{
 		id: "spacing",
 		title: "spacing",
-		about:
-			"The gap ramp.  Gaps carry meaning: xs binds a cluster, sm relates neighbors, " +
-			"lg separates groups, xl+ separates sections (the gaps megacolumn focus jumps across).",
+		about: "The gap ramp.  Gaps carry meaning: xs binds a cluster, sm relates neighbors, "
+			+ "lg separates groups, xl+ separates sections (the gaps megacolumn focus jumps across).",
 		tokens: [
 			{
 				name: "--sw-space-xs",