diff --git a/canvas/src/components/mobile/MobileChat.tsx b/canvas/src/components/mobile/MobileChat.tsx
index 10e46c222..1437f9f0d 100644
--- a/canvas/src/components/mobile/MobileChat.tsx
+++ b/canvas/src/components/mobile/MobileChat.tsx
@@ -756,7 +756,12 @@ export function MobileChat({
border: "none",
outline: "none",
background: "transparent",
- fontSize: 14.5,
+ // iOS Safari/PWA zooms the viewport when a focused textarea
+ // has a computed font-size below 16px. 14.5 triggers that
+ // focus-zoom; the page looks broken until the user pinches
+ // back (#224, same class as desktop #1434 / sibling #225).
+ // 16px is the minimum that keeps focus from zooming.
+ fontSize: 16,
lineHeight: 1.4,
color: p.text,
padding: "6px 0",
diff --git a/canvas/src/components/mobile/MobileSpawn.tsx b/canvas/src/components/mobile/MobileSpawn.tsx
index 34f6a9295..cdcbaf479 100644
--- a/canvas/src/components/mobile/MobileSpawn.tsx
+++ b/canvas/src/components/mobile/MobileSpawn.tsx
@@ -318,7 +318,12 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
border: `0.5px solid ${p.border}`,
borderRadius: 12,
fontFamily: MOBILE_FONT_MONO,
- fontSize: 13.5,
+ // iOS Safari/PWA zooms the viewport when a focused input has
+ // a computed font-size below 16px; the layout jumps and the
+ // page looks broken until the user pinches back (#224 / #225,
+ // same class as desktop #1434). 16px is the minimum that
+ // suppresses that focus-zoom.
+ fontSize: 16,
color: p.text,
outline: "none",
boxSizing: "border-box",
diff --git a/canvas/src/components/mobile/__tests__/MobileChat.test.tsx b/canvas/src/components/mobile/__tests__/MobileChat.test.tsx
index 0c8e24595..0aa69351d 100644
--- a/canvas/src/components/mobile/__tests__/MobileChat.test.tsx
+++ b/canvas/src/components/mobile/__tests__/MobileChat.test.tsx
@@ -263,6 +263,20 @@ describe("MobileChat — composer", () => {
const sendBtn = container.querySelector('[aria-label="Send"]') as HTMLButtonElement;
expect(sendBtn.disabled).toBe(true);
});
+
+ // Regression #224: the composer textarea must render with font-size
+ // ≥ 16px. iOS Safari and PWAs auto-zoom the viewport when a focused
+ // input has a computed font-size below 16px — the layout jumps and
+ // the page looks broken until the user pinches back. Same class as
+ // desktop #1434 / sibling MobileSpawn #225.
+ it("composer textarea renders at font-size 16px or greater (iOS focus-zoom regression #224)", () => {
+ const { container } = renderChat(mockAgentId);
+ const textarea = container.querySelector("textarea") as HTMLTextAreaElement;
+ expect(textarea).toBeTruthy();
+ const fs = Number.parseFloat(textarea.style.fontSize);
+ expect(Number.isFinite(fs)).toBe(true);
+ expect(fs).toBeGreaterThanOrEqual(16);
+ });
});
// ─── Tabs ─────────────────────────────────────────────────────────────────────
diff --git a/canvas/src/components/mobile/__tests__/MobileSpawn.test.tsx b/canvas/src/components/mobile/__tests__/MobileSpawn.test.tsx
index fb34825ef..03b0cffe3 100644
--- a/canvas/src/components/mobile/__tests__/MobileSpawn.test.tsx
+++ b/canvas/src/components/mobile/__tests__/MobileSpawn.test.tsx
@@ -93,6 +93,24 @@ describe("MobileSpawn — render", () => {
expect(input).toBeTruthy();
});
+ // Regression #224 / #225: the agent-name input must render with a
+ // font-size ≥ 16px. iOS Safari and PWAs auto-zoom the viewport when a
+ // focused input has a computed font-size below 16px — the layout
+ // jumps and the page looks broken until the user pinches back.
+ it("renders the name input at font-size 16px or greater (iOS focus-zoom regression)", () => {
+ apiGetSpy.mockResolvedValue(mockTemplates);
+ render();
+ const input = document.querySelector(
+ 'input[aria-label="Agent name"]',
+ ) as HTMLInputElement | null;
+ expect(input).toBeTruthy();
+ // Parse the inline style font-size — jsdom doesn't run a layout
+ // engine, so getComputedStyle reports the inline value verbatim.
+ const fs = Number.parseFloat(input!.style.fontSize);
+ expect(Number.isFinite(fs)).toBe(true);
+ expect(fs).toBeGreaterThanOrEqual(16);
+ });
+
it("renders all 4 tier buttons", () => {
apiGetSpy.mockResolvedValue(mockTemplates);
render();