diff --git a/src/auto-reply/reply/export-html/template.css b/src/auto-reply/reply/export-html/template.css
index b99c93e0b4d..9ab42801478 100644
--- a/src/auto-reply/reply/export-html/template.css
+++ b/src/auto-reply/reply/export-html/template.css
@@ -1063,7 +1063,7 @@ body {
display: block;
}
- #hamburger {
+ #hamburger.sidebar-menu-trigger {
display: inline-flex;
}
diff --git a/src/auto-reply/reply/export-html/template.security.test.ts b/src/auto-reply/reply/export-html/template.security.test.ts
index 1c7836a03c5..870938326e1 100644
--- a/src/auto-reply/reply/export-html/template.security.test.ts
+++ b/src/auto-reply/reply/export-html/template.security.test.ts
@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
-import vm from "node:vm";
import { fileURLToPath } from "node:url";
+import vm from "node:vm";
import { describe, expect, it } from "vitest";
type SessionEntry = {
@@ -141,6 +141,32 @@ function now() {
return new Date("2026-02-24T00:00:00.000Z").toISOString();
}
+function selectorSpecificity(selector: string): [number, number, number] {
+ const ids = selector.match(/#[\w-]+/g)?.length ?? 0;
+ const classes = selector.match(/\.[\w-]+/g)?.length ?? 0;
+ const withoutIdsOrClasses = selector.replace(/#[\w-]+|\.[\w-]+/g, " ");
+ const elements = withoutIdsOrClasses
+ .split(/[\s>+~]+/)
+ .filter((part) => /^[a-z][\w-]*$/i.test(part)).length;
+ return [ids, classes, elements];
+}
+
+function compareSpecificity(left: [number, number, number], right: [number, number, number]) {
+ for (let index = 0; index < left.length; index += 1) {
+ if (left[index] !== right[index]) {
+ return left[index] - right[index];
+ }
+ }
+ return 0;
+}
+
+function firstSelectorForDisplay(css: string, display: string, startAt: number): string | null {
+ const displayRule = new RegExp(`([^{}]+)\\{[^{}]*\\bdisplay\\s*:\\s*${display}\\s*;`, "g");
+ displayRule.lastIndex = startAt;
+ const match = displayRule.exec(css);
+ return match?.[1]?.split(",").at(-1)?.trim() ?? null;
+}
+
describe("export html sidebar trigger affordance", () => {
it("keeps the hamburger sidebar trigger accessible and visibly interactive", () => {
expect(templateHtml).toContain('id="hamburger" class="sidebar-menu-trigger"');
@@ -154,6 +180,21 @@ describe("export html sidebar trigger affordance", () => {
expect(templateCss).toContain("background: var(--container-bg);");
expect(templateCss).toContain("#hamburger.sidebar-menu-trigger:focus-visible {");
});
+
+ it("lets the mobile hamburger display rule win the CSS cascade", () => {
+ const baseSelector = "#hamburger.sidebar-menu-trigger";
+ const mobileMediaIndex = templateCss.indexOf("@media (max-width: 900px)");
+ const mobileSelector = firstSelectorForDisplay(templateCss, "inline-flex", mobileMediaIndex);
+
+ expect(mobileMediaIndex).toBeGreaterThan(templateCss.indexOf(`${baseSelector} {`));
+ expect(mobileSelector).toBe(baseSelector);
+ if (!mobileSelector) {
+ throw new Error("Missing mobile hamburger display rule");
+ }
+ expect(
+ compareSpecificity(selectorSpecificity(mobileSelector), selectorSpecificity(baseSelector)),
+ ).toBeGreaterThanOrEqual(0);
+ });
});
describe("export html security hardening", () => {