From 0bbc7fcc41cb6c4a07cf217faaab34edd52eaf96 Mon Sep 17 00:00:00 2001 From: Miso Tanaka <22117028+miso-develop@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:55:32 +0900 Subject: [PATCH 1/8] style(web): keep shared header sticky and normalize section boundaries --- web/src/style.css | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/web/src/style.css b/web/src/style.css index 52d75ce..6326bb0 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -11,7 +11,11 @@ button, input, select { font: inherit; } a { color: inherit; } #site-header { - padding: 20px 24px 0; + position: sticky; + top: 0; + z-index: 100; + padding: 20px 24px 8px; + background: #f5f5f5; } .site-nav-shell { @@ -115,6 +119,24 @@ h3 { margin-top: 24px; } border-top: 1px solid #e5e5e5; } +.security-subsection { + margin-top: 28px; + padding-top: 24px; + border-top: 1px solid #e5e5e5; +} + +.security-subsection > h3 { + margin-top: 0; +} + +.flash-stack { + margin-top: 28px; +} + +.flash-stack > .panel:first-child { + margin-top: 0; +} + #app .shell > section.panel + section.panel { margin-top: 40px; padding-top: 30px; @@ -431,7 +453,7 @@ button:disabled, input:disabled, select:disabled { padding: 12px; } - #site-header { padding: 12px 12px 0; } + #site-header { padding: 12px 12px 8px; } #app, #flash-app, #help-app { padding: 12px; } .shell { padding: 24px; } .status div, .build-identity div { grid-template-columns: 1fr; gap: 4px; } From e2d9a75b1417f1758c720838fd88e8093d78f24e Mon Sep 17 00:00:00 2001 From: Miso Tanaka <22117028+miso-develop@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:55:36 +0900 Subject: [PATCH 2/8] style(web): add shared Recovery subsection boundaries --- web/src/security/security-panel.ts | 50 ++++++++++++++++-------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/web/src/security/security-panel.ts b/web/src/security/security-panel.ts index f3c38f5..ca77c61 100644 --- a/web/src/security/security-panel.ts +++ b/web/src/security/security-panel.ts @@ -37,31 +37,35 @@ section.innerHTML = `

-

Recovery Package

-

Recovery Packages are encrypted but security-sensitive offline Passphrase-guessing targets. Do not upload them, attach them to Issues/PRs, or commit them to a repository.

-
- -
- - - - -
- - +
+

Recovery Package

+

Recovery Packages are encrypted but security-sensitive offline Passphrase-guessing targets. Do not upload them, attach them to Issues/PRs, or commit them to a repository.

+
+ +
+ + + + +
+ + +
-

Change Recovery Passphrase

-

${RECOVERY_PASSPHRASE_CHANGE_NOTICE}

- - - - - - -
- - +
+

Change Recovery Passphrase

+

${RECOVERY_PASSPHRASE_CHANGE_NOTICE}

+ + + + + + +
+ + +
`; From b256684071e8443d667adba69b7cf0d184152644 Mon Sep 17 00:00:00 2001 From: Miso Tanaka <22117028+miso-develop@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:55:39 +0900 Subject: [PATCH 3/8] style(web): remove redundant Firmware status panel boundary --- web/src/flasher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/flasher.ts b/web/src/flasher.ts index 722986f..fc857d2 100644 --- a/web/src/flasher.ts +++ b/web/src/flasher.ts @@ -22,7 +22,7 @@ app.innerHTML = `

Flash the CI-built M5StickS3 firmware directly from this site. Firmware images are same-origin static files; authenticator credentials are never part of a firmware package.

-
+
`; From e2313250f40850a2c4a345444140179550ed1e88 Mon Sep 17 00:00:00 2001 From: Miso Tanaka <22117028+miso-develop@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:56:27 +0900 Subject: [PATCH 4/8] test(web): enforce sticky header and single-divider layout contracts --- .../browser/post-provisioning-layout-smoke.ts | 119 +++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/web/tests/browser/post-provisioning-layout-smoke.ts b/web/tests/browser/post-provisioning-layout-smoke.ts index 5b77e8d..9a60479 100644 --- a/web/tests/browser/post-provisioning-layout-smoke.ts +++ b/web/tests/browser/post-provisioning-layout-smoke.ts @@ -35,6 +35,7 @@ export async function runPostProvisioningLayoutSmoke(): Promise { await verifyProductionProvisioningLifecycle(); await verifyProductionFirmwareGeometry(); await verifySharedRouteGeometryPolicy(); + await verifyStickyHeaderPolicy(); document.body.dataset.postProvisioningLayoutStatus = "pass"; } catch (error) { document.body.dataset.postProvisioningLayoutError = error instanceof Error ? error.message : "Unknown layout smoke failure"; @@ -318,6 +319,25 @@ async function verifyProductionProvisioningLifecycle(): Promise { assert(parseFloat(style.paddingTop) >= 32, `Provisioning section ${index + 1} requires at least 32px boundary padding`); } }); + const recoverySubsection = required(shell, '[data-security-subsection="recovery-package"]'); + const passphraseSubsection = required(shell, '[data-security-subsection="change-passphrase"]'); + for (const [name, subsection] of [ + ["Recovery Package", recoverySubsection], + ["Change Recovery Passphrase", passphraseSubsection], + ] as const) { + const heading = subsection.querySelector(":scope > h3"); + assert(heading?.textContent === name, `${name} must retain its semantic h3 heading`); + const style = getComputedStyle(subsection); + assert(style.borderTopStyle !== "none" && parseFloat(style.borderTopWidth) >= 1, `${name} must have one visible subsection divider`); + assert(parseFloat(style.marginTop) > 0 && parseFloat(style.paddingTop) > 0, `${name} divider must retain subsection spacing`); + assert(subsection.querySelector(":scope > hr") === null, `${name} must not duplicate the CSS divider with an hr`); + assert(getComputedStyle(heading).marginTop === "0px", `${name} heading must start immediately after the shared divider spacing`); + } + assert( + getComputedStyle(recoverySubsection).borderTopColor === getComputedStyle(passphraseSubsection).borderTopColor, + "Recovery subsections must share the same neutral divider treatment", + ); + const regularDivider = getComputedStyle(majorSections[0]!).borderTopColor; const dangerDivider = getComputedStyle(required(shell, "section.danger")).borderTopColor; assert(dangerDivider !== regularDivider, "Factory Reset must retain its danger-tinted divider semantics"); @@ -561,6 +581,27 @@ async function verifyProductionFirmwareGeometry(): Promise { assert(status.querySelectorAll("button").length === 2, "Successful Firmware resolution must render First install and Update controls"); assert(buildIdentity.textContent?.includes(FIRMWARE_SMOKE_BUILD_COMMIT) === true, "Successful Firmware resolution must display the pinned fixture build identity"); + const flashChoices = Array.from(status.querySelectorAll(":scope > section.panel")); + assert(flashChoices.length === 2, "Firmware status must contain exactly First install and Update peer panels"); + const statusStyle = getComputedStyle(status); + assert( + statusStyle.borderTopStyle === "none" || parseFloat(statusStyle.borderTopWidth) === 0, + "Firmware structural status container must not add a duplicate top divider", + ); + const firstInstallStyle = getComputedStyle(flashChoices[0]!); + const updateStyle = getComputedStyle(flashChoices[1]!); + assert( + firstInstallStyle.borderTopStyle !== "none" && parseFloat(firstInstallStyle.borderTopWidth) >= 1, + "First install must have exactly one panel boundary supplied by its own section", + ); + assert( + updateStyle.borderTopStyle !== "none" && parseFloat(updateStyle.borderTopWidth) >= 1, + "Update must retain exactly one panel boundary", + ); + assert(parseFloat(firstInstallStyle.marginTop) === 0, "First install must not retain redundant nested-panel top spacing"); + assert(parseFloat(updateStyle.marginTop) > 0, "Update must remain visually separated from First install"); + assert(status.querySelector("hr") === null, "Firmware divider normalization must not introduce decorative hr elements"); + const afterShell = shell.getBoundingClientRect(); const afterNav = nav.getBoundingClientRect(); const afterBuildSection = buildSection.getBoundingClientRect(); @@ -616,6 +657,80 @@ async function verifySharedRouteGeometryPolicy(): Promise { } } +async function verifyStickyHeaderPolicy(): Promise { + const routes = ["index.html", "flash.html", "help.html"] as const; + const frames: HTMLIFrameElement[] = []; + try { + for (const route of routes) frames.push(await loadRouteFrame(route, 360)); + + for (let index = 0; index < frames.length; index += 1) { + const frame = frames[index]!; + const route = routes[index]!; + const doc = requiredFrameDocument(frame); + const win = requiredFrameWindow(frame); + const header = required(doc, "#site-header"); + const nav = required(doc, ".site-nav-shell"); + const product = required(doc, ".product-mark"); + const tabs = required(doc, ".site-tabs"); + const language = required(doc, ".language-switcher"); + flushLayout(doc); + + const headerStyle = getComputedStyle(header); + assert(headerStyle.position === "sticky", `${route} must use the shared sticky header contract`); + assert(headerStyle.top === "0px", `${route} sticky header must pin to viewport top`); + assert(Number.parseInt(headerStyle.zIndex, 10) > 0, `${route} sticky header needs positive stacking order`); + assert(headerStyle.backgroundColor !== "rgba(0, 0, 0, 0)", `${route} sticky header background must be opaque/readable`); + assert(header.contains(nav) && nav.contains(product) && nav.contains(tabs) && nav.contains(language), `${route} sticky header must retain product, tabs, and language switcher`); + assert(doc.documentElement.scrollHeight > win.innerHeight, `${route} sticky-header fixture must be vertically scrollable`); + + const beforeNav = nav.getBoundingClientRect(); + const beforeShell = required(doc, ".shell").getBoundingClientRect(); + const beforeScrollWidth = doc.documentElement.scrollWidth; + win.scrollTo(0, Math.min(480, doc.documentElement.scrollHeight - win.innerHeight)); + await new Promise((resolve) => window.setTimeout(resolve, 20)); + flushLayout(doc); + + const afterHeader = header.getBoundingClientRect(); + const afterNav = nav.getBoundingClientRect(); + const afterShell = required(doc, ".shell").getBoundingClientRect(); + assert(near(afterHeader.top, 0), `${route} sticky header did not remain at viewport top while scrolling`); + assert(near(beforeNav.left, afterNav.left) && near(beforeNav.width, afterNav.width), `${route} sticky navigation shifted horizontally while scrolling`); + assert(near(beforeShell.left, afterShell.left) && near(beforeShell.width, afterShell.width), `${route} content shell shifted horizontally while scrolling`); + assert(doc.documentElement.scrollWidth === beforeScrollWidth && doc.documentElement.scrollWidth <= win.innerWidth, `${route} sticky header introduced horizontal overflow or width jitter`); + } + + const provisioningDoc = requiredFrameDocument(frames[0]!); + const headerZ = Number.parseInt(getComputedStyle(required(provisioningDoc, "#site-header")).zIndex, 10); + const presenceOverlay = required(provisioningDoc, ".presence-overlay"); + const overlayZ = Number.parseInt(getComputedStyle(presenceOverlay).zIndex, 10); + assert(overlayZ > headerZ, "Security presence overlay must retain stacking priority over the sticky header"); + + const responsiveFrame = await loadRouteFrame("help.html", 360, 540); + frames.push(responsiveFrame); + const responsiveDoc = requiredFrameDocument(responsiveFrame); + const responsiveWin = requiredFrameWindow(responsiveFrame); + flushLayout(responsiveDoc); + const responsiveHeader = required(responsiveDoc, "#site-header"); + const responsiveNav = required(responsiveDoc, ".site-nav-shell"); + const responsiveTabs = required(responsiveDoc, ".site-tabs"); + const responsiveLanguage = required(responsiveDoc, ".language-switcher"); + const responsiveProduct = required(responsiveDoc, ".product-mark"); + assert(getComputedStyle(responsiveTabs).gridRowStart === "2", "Responsive top-level tabs must retain the shared second-row layout"); + for (const element of [responsiveProduct, responsiveTabs, responsiveLanguage]) { + const rect = element.getBoundingClientRect(); + const headerRect = responsiveHeader.getBoundingClientRect(); + assert(rect.left >= headerRect.left - GEOMETRY_EPSILON_PX && rect.right <= headerRect.right + GEOMETRY_EPSILON_PX, "Responsive header controls must remain horizontally visible"); + } + assert(responsiveDoc.documentElement.scrollWidth <= responsiveWin.innerWidth, "Responsive sticky header must not create a horizontal scrollbar"); + responsiveWin.scrollTo(0, 300); + await new Promise((resolve) => window.setTimeout(resolve, 20)); + assert(near(responsiveHeader.getBoundingClientRect().top, 0), "Responsive two-row header must remain sticky while scrolling"); + assert(responsiveNav.getBoundingClientRect().width > 0, "Responsive navigation must remain rendered after scrolling"); + } finally { + for (const frame of frames) frame.remove(); + } +} + function createSnapshot( provisioned: boolean, factoryResetPresenceRequired = false, @@ -669,9 +784,9 @@ function createSnapshot( }; } -async function loadRouteFrame(path: string, heightPx: number): Promise { +async function loadRouteFrame(path: string, heightPx: number, widthPx = ROUTE_WIDTH_PX): Promise { const frame = document.createElement("iframe"); - frame.width = String(ROUTE_WIDTH_PX); + frame.width = String(widthPx); frame.height = String(heightPx); frame.style.position = "fixed"; frame.style.left = "-20000px"; From d62e4ec7c43f2e05ed848245fb3cf7da667fc72d Mon Sep 17 00:00:00 2001 From: Miso Tanaka <22117028+miso-develop@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:56:41 +0900 Subject: [PATCH 5/8] refactor(web): keep Firmware status container structural --- web/src/flasher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/flasher.ts b/web/src/flasher.ts index fc857d2..e292df6 100644 --- a/web/src/flasher.ts +++ b/web/src/flasher.ts @@ -22,7 +22,7 @@ app.innerHTML = `

Flash the CI-built M5StickS3 firmware directly from this site. Firmware images are same-origin static files; authenticator credentials are never part of a firmware package.

-
+
`; From f837486f4e7e479c120fa5a576f30a56fb4cebeb Mon Sep 17 00:00:00 2001 From: Miso Tanaka <22117028+miso-develop@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:58:42 +0900 Subject: [PATCH 6/8] test(web): verify Recovery dividers on full production route --- .../browser/post-provisioning-layout-smoke.ts | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/web/tests/browser/post-provisioning-layout-smoke.ts b/web/tests/browser/post-provisioning-layout-smoke.ts index 9a60479..480a139 100644 --- a/web/tests/browser/post-provisioning-layout-smoke.ts +++ b/web/tests/browser/post-provisioning-layout-smoke.ts @@ -319,25 +319,6 @@ async function verifyProductionProvisioningLifecycle(): Promise { assert(parseFloat(style.paddingTop) >= 32, `Provisioning section ${index + 1} requires at least 32px boundary padding`); } }); - const recoverySubsection = required(shell, '[data-security-subsection="recovery-package"]'); - const passphraseSubsection = required(shell, '[data-security-subsection="change-passphrase"]'); - for (const [name, subsection] of [ - ["Recovery Package", recoverySubsection], - ["Change Recovery Passphrase", passphraseSubsection], - ] as const) { - const heading = subsection.querySelector(":scope > h3"); - assert(heading?.textContent === name, `${name} must retain its semantic h3 heading`); - const style = getComputedStyle(subsection); - assert(style.borderTopStyle !== "none" && parseFloat(style.borderTopWidth) >= 1, `${name} must have one visible subsection divider`); - assert(parseFloat(style.marginTop) > 0 && parseFloat(style.paddingTop) > 0, `${name} divider must retain subsection spacing`); - assert(subsection.querySelector(":scope > hr") === null, `${name} must not duplicate the CSS divider with an hr`); - assert(getComputedStyle(heading).marginTop === "0px", `${name} heading must start immediately after the shared divider spacing`); - } - assert( - getComputedStyle(recoverySubsection).borderTopColor === getComputedStyle(passphraseSubsection).borderTopColor, - "Recovery subsections must share the same neutral divider treatment", - ); - const regularDivider = getComputedStyle(majorSections[0]!).borderTopColor; const dangerDivider = getComputedStyle(required(shell, "section.danger")).borderTopColor; assert(dangerDivider !== regularDivider, "Factory Reset must retain its danger-tinted divider semantics"); @@ -639,6 +620,26 @@ async function verifySharedRouteGeometryPolicy(): Promise { const productionPanels = Array.from(provisioningShell.querySelectorAll(":scope > section.panel")); assert(productionPanels.length >= 7, "Production Provisioning route must keep all major peer sections on the shared panel contract"); assert(provisioningShell.lastElementChild === productionBuildSection, "Production Web Build information section must remain the final Provisioning peer section"); + + const recoverySubsection = required(provisioningShell, '[data-security-subsection="recovery-package"]'); + const passphraseSubsection = required(provisioningShell, '[data-security-subsection="change-passphrase"]'); + for (const [name, subsection] of [ + ["Recovery Package", recoverySubsection], + ["Change Recovery Passphrase", passphraseSubsection], + ] as const) { + const heading = subsection.querySelector(":scope > h3"); + assert(sourceTextOf(heading!) === name, `${name} must retain its semantic localized h3 heading source`); + const style = getComputedStyle(subsection); + assert(style.borderTopStyle !== "none" && parseFloat(style.borderTopWidth) >= 1, `${name} must have one visible subsection divider`); + assert(parseFloat(style.marginTop) > 0 && parseFloat(style.paddingTop) > 0, `${name} divider must retain subsection spacing`); + assert(subsection.querySelector(":scope > hr") === null, `${name} must not duplicate the CSS divider with an hr`); + assert(getComputedStyle(heading!).marginTop === "0px", `${name} heading must start immediately after the shared divider spacing`); + } + assert( + getComputedStyle(recoverySubsection).borderTopColor === getComputedStyle(passphraseSubsection).borderTopColor, + "Recovery subsections must share the same neutral divider treatment", + ); + for (const [index, section] of productionPanels.entries()) { const style = getComputedStyle(section); assert(style.borderTopStyle !== "none" && parseFloat(style.borderTopWidth) >= 1, `Production Provisioning peer section ${index + 1} must retain a visible top divider`); From c8de6efed263fab53efa26b92093b00dff00e9db Mon Sep 17 00:00:00 2001 From: Miso Tanaka <22117028+miso-develop@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:59:48 +0900 Subject: [PATCH 7/8] fix(web): preserve existing header height while sticky --- web/src/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/style.css b/web/src/style.css index 6326bb0..5c27f34 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -14,7 +14,7 @@ a { color: inherit; } position: sticky; top: 0; z-index: 100; - padding: 20px 24px 8px; + padding: 20px 24px 0; background: #f5f5f5; } @@ -453,7 +453,7 @@ button:disabled, input:disabled, select:disabled { padding: 12px; } - #site-header { padding: 12px 12px 8px; } + #site-header { padding: 12px 12px 0; } #app, #flash-app, #help-app { padding: 12px; } .shell { padding: 24px; } .status div, .build-identity div { grid-template-columns: 1fr; gap: 4px; } From 91babb2c8e7f6e6da7091a35c0b9902fa07978c5 Mon Sep 17 00:00:00 2001 From: Miso Tanaka <22117028+miso-develop@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:01:00 +0900 Subject: [PATCH 8/8] test(web): validate rendered Recovery subsection localization --- web/tests/browser/post-provisioning-layout-smoke.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/tests/browser/post-provisioning-layout-smoke.ts b/web/tests/browser/post-provisioning-layout-smoke.ts index 480a139..f3b7a37 100644 --- a/web/tests/browser/post-provisioning-layout-smoke.ts +++ b/web/tests/browser/post-provisioning-layout-smoke.ts @@ -628,7 +628,12 @@ async function verifySharedRouteGeometryPolicy(): Promise { ["Change Recovery Passphrase", passphraseSubsection], ] as const) { const heading = subsection.querySelector(":scope > h3"); - assert(sourceTextOf(heading!) === name, `${name} must retain its semantic localized h3 heading source`); + const expectedHeading = name === "Recovery Package" + ? "Recovery Package" + : provisioningDoc.documentElement.lang === "ja" + ? "Recovery Passphraseを変更" + : "Change Recovery Passphrase"; + assert(heading?.textContent === expectedHeading, `${name} must retain its semantic localized h3 heading`); const style = getComputedStyle(subsection); assert(style.borderTopStyle !== "none" && parseFloat(style.borderTopWidth) >= 1, `${name} must have one visible subsection divider`); assert(parseFloat(style.marginTop) > 0 && parseFloat(style.paddingTop) > 0, `${name} divider must retain subsection spacing`);