diff --git a/web/platform/app-brand.test.tsx b/web/platform/app-brand.test.tsx
new file mode 100644
index 000000000..2747bb3b4
--- /dev/null
+++ b/web/platform/app-brand.test.tsx
@@ -0,0 +1,39 @@
+import type { WebLinkProps } from "./navigation.js";
+import { WebNavigationProvider } from "./navigation.js";
+import { AppBrand } from "./app-brand.js";
+import { cleanup, render, screen } from "./test/rtl.js";
+import { afterEach, expect, it } from "vitest";
+
+afterEach(cleanup);
+
+it("names the home link without reading the decorative monogram", () => {
+ render(Stovepipe);
+ expect(screen.getByRole("link", { name: "Stovepipe" })).toHaveAttribute("href", "/queue");
+ expect(screen.getByText("SP")).toHaveAttribute("aria-hidden", "true");
+});
+
+it("preserves host navigation and an explicit home label", () => {
+ function HostLink(props: WebLinkProps) {
+ return ;
+ }
+ render(
+ SubmitQueue
+ );
+ const link = screen.getByRole("link", { name: "SubmitQueue queues" });
+ expect(link).toHaveAttribute("href", "/sq");
+ expect(link).toHaveAttribute("data-host-link", "true");
+});
+
+it("omits an empty monogram while preserving custom branding", () => {
+ render(My queue host);
+ const link = screen.getByRole("link", { name: "My queue host" });
+ expect(link.querySelector("[aria-hidden]")).toBeNull();
+ expect(link.querySelector("strong")).toHaveTextContent("My queue host");
+});
+
+it("keeps a custom symbol decorative without adding a monogram badge", () => {
+ render(}>Stovepipe);
+ const link = screen.getByRole("link", { name: "Stovepipe" });
+ expect(link.querySelector("svg")?.parentElement).toHaveAttribute("aria-hidden", "true");
+ expect(link).toHaveTextContent("Stovepipe");
+});
diff --git a/web/platform/app-brand.tsx b/web/platform/app-brand.tsx
new file mode 100644
index 000000000..5949f6f10
--- /dev/null
+++ b/web/platform/app-brand.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { styled } from "baseui";
+import { StyledLink } from "baseui/link";
+import { useWebNavigation } from "./navigation.js";
+
+const BrandLink = styled(StyledLink, ({ $theme }) => ({
+ display: "inline-flex",
+ alignItems: "center",
+ gap: $theme.sizing.scale300,
+ color: $theme.colors.contentPrimary,
+ fontSize: "18px",
+ fontWeight: 600,
+ textDecoration: "none",
+ ":visited": { color: $theme.colors.contentPrimary },
+ ":hover": { color: $theme.colors.contentPrimary, textDecoration: "none" },
+}));
+
+const BrandMark = styled("span", ({ $theme }) => ({
+ backgroundColor: $theme.colors.contentPrimary,
+ color: $theme.colors.backgroundPrimary,
+ borderRadius: "6px",
+ padding: "5px 7px",
+ fontSize: "12px",
+ lineHeight: 1,
+}));
+
+const BrandSymbol = styled("span", {
+ display: "inline-flex",
+ alignItems: "center",
+ flexShrink: 0,
+ lineHeight: 0,
+});
+
+export interface AppBrandProps {
+ href: string;
+ /** Accessible name of the home link. */
+ homeLabel?: string;
+ children: ReactNode;
+ /** Decorative mark; text is rendered as a monogram badge. */
+ mark?: ReactNode;
+}
+
+export function AppBrand({ href, homeLabel, children, mark = "" }: AppBrandProps) {
+ const { Link } = useWebNavigation();
+ return
+ {mark ? typeof mark === "string"
+ ? {mark}
+ : {mark}
+ : null}
+ {children}
+ ;
+}
diff --git a/web/platform/app-header.test.tsx b/web/platform/app-header.test.tsx
new file mode 100644
index 000000000..4e1fcae8e
--- /dev/null
+++ b/web/platform/app-header.test.tsx
@@ -0,0 +1,21 @@
+import { afterEach, expect, it } from "vitest";
+import { cleanup, render, screen, within } from "./test/rtl.js";
+import { AppBrand } from "./app-brand.js";
+import { AppHeader } from "./app-header.js";
+
+afterEach(cleanup);
+
+it("keeps branding and controls in a compact header that can grow when wrapped", () => {
+ render(
+ SubmitQueue
+
+ );
+ const header = screen.getByRole("banner");
+ expect(within(header).getByRole("link", { name: "SubmitQueue" })).toHaveAttribute("href", "/");
+ expect(within(header).getByRole("button", { name: "Enable auto-refresh" })).toBeVisible();
+ const style = getComputedStyle(header);
+ expect(style.minHeight).toBe("64px");
+ expect(style.height).not.toBe("64px");
+ expect(style.boxSizing).toBe("border-box");
+ expect(style.flexWrap).toBe("wrap");
+});
diff --git a/web/platform/app-header.tsx b/web/platform/app-header.tsx
new file mode 100644
index 000000000..8703f47f2
--- /dev/null
+++ b/web/platform/app-header.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { styled } from "baseui";
+
+export const AppHeader = styled("header", ({ $theme }) => ({
+ minHeight: "64px",
+ boxSizing: "border-box",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ flexWrap: "wrap",
+ gap: $theme.sizing.scale600,
+ paddingTop: $theme.sizing.scale300,
+ paddingBottom: $theme.sizing.scale300,
+ borderBottom: `1px solid ${$theme.colors.borderOpaque}`,
+}));
diff --git a/web/platform/auto-refresh.tsx b/web/platform/auto-refresh.tsx
index 829276e93..5bc68ed1a 100644
--- a/web/platform/auto-refresh.tsx
+++ b/web/platform/auto-refresh.tsx
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { styled, type Theme } from "baseui";
import { Block } from "baseui/block";
-import { Button, KIND, SHAPE, SIZE } from "baseui/button";
+import { Button, KIND, MIN_HIT_AREA, SHAPE, SIZE } from "baseui/button";
import { StatefulTooltip } from "baseui/tooltip";
const RefreshIcon = styled<"svg", { $spinning: boolean }>("svg", ({ $spinning }) => ({
@@ -114,21 +114,17 @@ export function AutoRefresh({
return (
-