{
  "config": {
    "argv": [
      "/opt/hostedtoolcache/node/20.20.2/x64/bin/node",
      "/home/runner/work/dr-card-web/dr-card-web/node_modules/.bin/playwright",
      "test",
      "tests/e2e/test-gate.spec.ts"
    ],
    "configFile": "/home/runner/work/dr-card-web/dr-card-web/playwright.config.ts",
    "rootDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
    "failOnFlakyTests": false,
    "forbidOnly": true,
    "fullyParallel": true,
    "globalSetup": null,
    "globalTeardown": null,
    "globalTimeout": 0,
    "grep": {},
    "grepInvert": null,
    "maxFailures": 0,
    "metadata": {
      "ci": {
        "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/ea5aafc55469733b7fa5b7fe3f91f89cda8f7bab",
        "commitHash": "ea5aafc55469733b7fa5b7fe3f91f89cda8f7bab",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/261",
        "prTitle": "fix(chat): apply verified scrolling fix on test baseline",
        "prBaseHash": "d821f85dd4746162355c099724e699fde320983b",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/40051343738"
      },
      "gitCommit": {
        "shortHash": "ea5aafc",
        "hash": "ea5aafc55469733b7fa5b7fe3f91f89cda8f7bab",
        "subject": "Merge d69b607e570d9273c1b8264dbad1093e4349a416 into d821f85dd4746162355c099724e699fde320983b",
        "body": "Merge d69b607e570d9273c1b8264dbad1093e4349a416 into d821f85dd4746162355c099724e699fde320983b\n",
        "author": {
          "name": "SINGING-AKIBA",
          "email": "980325684@qq.com",
          "time": 1792806420000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1792806420000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/src/components/mobile/ChatMessageList.test.tsx b/src/components/mobile/ChatMessageList.test.tsx\nindex d498ab4..004e6b5 100644\n--- a/src/components/mobile/ChatMessageList.test.tsx\n+++ b/src/components/mobile/ChatMessageList.test.tsx\n@@ -1,6 +1,7 @@\n import { describe, expect, it } from 'vitest'\n import { fireEvent, render, screen } from '@testing-library/react'\n import ChatMessageList from './ChatMessageList'\n+import PageContainer from './PageContainer'\n \n const longUrl = 'https://hexxueyantest.oss-cn-guangzhou.aliyuncs.com/storage/20261008/' + 'a'.repeat(90)\n \n@@ -25,48 +26,50 @@ describe('ChatMessageList mobile layout', () => {\n     expect(container.querySelector('[data-chat-rich-text]')?.className).toContain('[overflow-wrap:anywhere]')\n   })\n \n-  it('scrolls new messages when already near bottom but preserves history reading position', () => {\n+  it('follows new messages near the bottom and preserves position when reading history', () => {\n     const makeMessages = (count: number) => Array.from({ length: count }, (_, index) => ({\n       id: String(index + 1), role: 'user' as const, text: `消息 ${index + 1}`, status: 'sent' as const,\n     }))\n-    const { container, rerender } = render(\n-      <div data-chat-scroll style={{ height: 100, overflowY: 'auto' }}>\n-        <ChatMessageList messages={makeMessages(2)} />\n-      </div>,\n+    let scrollHeight = 200\n+    const renderChat = (messages: ReturnType<typeof makeMessages>) => (\n+      <PageContainer className=\"h-full !min-h-0 min-w-0 w-full flex flex-col pb-0\" inset={false}>\n+        <div className=\"shrink-0\">客服标题</div>\n+        <section\n+          className=\"min-w-0 min-h-0 w-full flex-1 overflow-y-auto\"\n+          data-chat-scroll\n+          ref={(node) => {\n+            if (node) {\n+              Object.defineProperty(node, 'clientHeight', { configurable: true, value: 100 })\n+              Object.defineProperty(node, 'scrollHeight', { configurable: true, get: () => scrollHeight })\n+            }\n+          }}\n+        >\n+          <ChatMessageList messages={messages} />\n+        </section>\n+        <div className=\"shrink-0\">消息输入区</div>\n+      </PageContainer>\n     )\n+    const { container, rerender } = render(renderChat(makeMessages(2)))\n+    const page = container.querySelector<HTMLElement>('[data-page-container]')!\n     const scroller = container.querySelector<HTMLElement>('[data-chat-scroll]')!\n-    Object.defineProperties(scroller, {\n-      scrollHeight: { configurable: true, value: 500 },\n-      clientHeight: { configurable: true, value: 100 },\n-    })\n+    expect(page.className).toContain('h-full')\n+    expect(page.className).toContain('!min-h-0')\n+    expect(scroller.className).toContain('min-h-0')\n+    expect(scroller.className).toContain('overflow-y-auto')\n+    expect(scroller.scrollTop).toBe(100)\n \n-    // First render starts at the newest message.\n-    rerender(\n-      <div data-chat-scroll style={{ height: 100, overflowY: 'auto' }}>\n-        <ChatMessageList messages={makeMessages(3)} />\n-      </div>,\n-    )\n-    expect(scroller.scrollTop).toBe(500)\n+    scrollHeight = 300\n+    rerender(renderChat(makeMessages(3)))\n+    expect(scroller.scrollTop).toBe(200)\n \n-    // A reader who has scrolled up should not be forced to the bottom.\n     scroller.scrollTop = 0\n     fireEvent.scroll(scroller)\n-    rerender(\n-      <div data-chat-scroll style={{ height: 100, overflowY: 'auto' }}>\n-        <ChatMessageList messages={makeMessages(4)} />\n-      </div>,\n-    )\n+    scrollHeight = 400\n+    rerender(renderChat(makeMessages(4)))\n     expect(scroller.scrollTop).toBe(0)\n \n-    // Returning near the bottom restores follow-new-message behavior.\n-    scroller.scrollTop = 350\n-    fireEvent.scroll(scroller)\n-    rerender(\n-      <div data-chat-scroll style={{ height: 100, overflowY: 'auto' }}>\n-        <ChatMessageList messages={makeMessages(5)} />\n-      </div>,\n-    )\n-    expect(scroller.scrollTop).toBe(500)\n+    scrollHeight = 500\n+    rerender(renderChat([{ id: '0', role: 'user', text: '更早的消息', status: 'sent' }, ...makeMessages(4)]))\n+    expect(scroller.scrollTop).toBe(100)\n   })\n-\n })\ndiff --git a/src/components/mobile/ChatMessageList.tsx b/src/components/mobile/ChatMessageList.tsx\nindex 2be90b7..2948f7c 100644\n--- a/src/components/mobile/ChatMessageList.tsx\n+++ b/src/components/mobile/ChatMessageList.tsx\n@@ -23,36 +23,36 @@ export default function ChatMessageList({\n   className = '',\n }: ChatMessageListProps) {\n   const listRef = useRef<HTMLUListElement>(null)\n-  const wasNearBottom = useRef(true)\n-  const hasMounted = useRef(false)\n+  const followBottomRef = useRef(true)\n+  const previousScrollHeightRef = useRef(0)\n+  const previousMessageCountRef = useRef(0)\n+  const previousFirstMessageIdRef = useRef<string | undefined>(undefined)\n \n   useEffect(() => {\n     const list = listRef.current\n     const scroller = list?.closest<HTMLElement>('[data-chat-scroll]')\n     if (!scroller) return\n \n-    const updatePosition = () => {\n-      wasNearBottom.current =\n-        scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight <= 80\n+    const onScroll = () => {\n+      followBottomRef.current = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight <= 48\n     }\n-    scroller.addEventListener('scroll', updatePosition, { passive: true })\n-    updatePosition()\n-    return () => scroller.removeEventListener('scroll', updatePosition)\n-  }, [])\n+    scroller.addEventListener('scroll', onScroll, { passive: true })\n \n-  useEffect(() => {\n-    const list = listRef.current\n-    const scroller = list?.closest<HTMLElement>('[data-chat-scroll]')\n-    if (!scroller) return\n-    if (!hasMounted.current) {\n-      scroller.scrollTop = scroller.scrollHeight\n-      hasMounted.current = true\n-      wasNearBottom.current = true\n-      return\n-    }\n-    if (wasNearBottom.current) {\n-      scroller.scrollTop = scroller.scrollHeight\n+    const previousHeight = previousScrollHeightRef.current\n+    const currentHeight = scroller.scrollHeight\n+    const firstMessageId = messages[0]?.id\n+    const historyPrepended = previousHeight > 0 && messages.length > previousMessageCountRef.current &&\n+      firstMessageId !== previousFirstMessageIdRef.current\n+    if (previousHeight === 0 || followBottomRef.current) {\n+      scroller.scrollTop = Math.max(0, currentHeight - scroller.clientHeight)\n+    } else if (historyPrepended && currentHeight > previousHeight) {\n+      scroller.scrollTop += currentHeight - previousHeight\n     }\n+    previousScrollHeightRef.current = currentHeight\n+    previousMessageCountRef.current = messages.length\n+    previousFirstMessageIdRef.current = firstMessageId\n+\n+    return () => scroller.removeEventListener('scroll', onScroll)\n   }, [messages])\n \n   return (\ndiff --git a/src/pages/ApiServiceChat.tsx b/src/pages/ApiServiceChat.tsx\nindex dfce26e..2cb06c7 100644\n--- a/src/pages/ApiServiceChat.tsx\n+++ b/src/pages/ApiServiceChat.tsx\n@@ -55,8 +55,8 @@ export default function ApiServiceChat() {\n   }, [location.hash])\n \n   return (\n-    <PageContainer className=\"flex min-h-full min-w-0 w-full flex-col pb-0\" inset={false}>\n-      <div className=\"px-4 pt-3\">\n+    <PageContainer className=\"h-full !min-h-0 min-w-0 w-full flex flex-col pb-0\" inset={false}>\n+      <div className=\"shrink-0 px-4 pt-3\">\n         <p className=\"text-center text-xs text-text-tertiary\">AI 客服 {CHAT_BOT.name} 为您服务</p>\n       </div>\n       <section className=\"min-w-0 min-h-0 w-full flex-1 overflow-y-auto px-4 pb-4 pt-4\" data-chat-scroll aria-label=\"客服聊天历史\" data-human-stage={chat.humanAwait || human.active ? 'pending' : 'idle'}>\n@@ -92,7 +92,7 @@ export default function ApiServiceChat() {\n         )}\n       </section>\n \n-      <div className=\"sticky bottom-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n+      <div className=\"shrink-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n         {chat.error && (\n           <div role=\"alert\" className=\"mb-3 rounded-container bg-surface px-3 py-2 text-xs text-danger-text\">\n             <p>{chat.error}</p>\ndiff --git a/src/pages/ServiceChat.tsx b/src/pages/ServiceChat.tsx\nindex 9538d48..0a15189 100644\n--- a/src/pages/ServiceChat.tsx\n+++ b/src/pages/ServiceChat.tsx\n@@ -176,9 +176,9 @@ function MockServiceChat() {\n       : CHAT_BOT.inputPlaceholder\n \n   return (\n-    <PageContainer className=\"flex min-h-full min-w-0 w-full flex-col pb-0\" inset={false}>\n+    <PageContainer className=\"h-full !min-h-0 min-w-0 w-full flex flex-col pb-0\" inset={false}>\n       {/* 标题栏提供独立的福利官二维码入口；底部人工按钮只负责聊天转接。 */}\n-      <div className=\"px-4 pt-3\">\n+      <div className=\"shrink-0 px-4 pt-3\">\n         <p className=\"text-center text-xs text-text-tertiary\">\n           AI 客服 {CHAT_BOT.name} 为您服务\n         </p>\n@@ -188,7 +188,7 @@ function MockServiceChat() {\n         <ChatMessageList messages={messages} onRetry={retry} />\n       </div>\n \n-      <div className=\"sticky bottom-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n+      <div className=\"shrink-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n         <div className=\"flex items-end gap-2\">\n           <button\n             type=\"button\"\ndiff --git a/src/pages/ServiceHuman.tsx b/src/pages/ServiceHuman.tsx\nindex b35c866..dd8f884 100644\n--- a/src/pages/ServiceHuman.tsx\n+++ b/src/pages/ServiceHuman.tsx\n@@ -62,8 +62,8 @@ export default function ServiceHuman() {\n   }\n \n   return (\n-    <PageContainer className=\"flex min-h-full flex-col pb-0\" inset={false}>\n-      <div className=\"px-4 pt-3\" data-queue-state={queueState}>\n+    <PageContainer className=\"h-full !min-h-0 flex flex-col pb-0\" inset={false}>\n+      <div className=\"shrink-0 px-4 pt-3\" data-queue-state={queueState}>\n         {connected ? (\n           <div className=\"flex items-center gap-2.5 rounded-container bg-surface px-3.5 py-3 shadow-sm\">\n             <span\n@@ -89,7 +89,7 @@ export default function ServiceHuman() {\n         <ChatMessageList messages={messages} />\n       </div>\n \n-      <div className=\"sticky bottom-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n+      <div className=\"shrink-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n         <div className=\"flex items-end gap-2\">\n           <div className=\"flex min-h-11 flex-1 items-center rounded-container border-2 border-transparent bg-surface px-3 focus-within:border-border-focused\">\n             <input",
      "actualWorkers": 2
    },
    "preserveOutput": "always",
    "projects": [
      {
        "outputDir": "/home/runner/work/dr-card-web/dr-card-web/test-results/artifacts",
        "repeatEach": 1,
        "retries": 1,
        "metadata": {
          "ci": {
            "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/ea5aafc55469733b7fa5b7fe3f91f89cda8f7bab",
            "commitHash": "ea5aafc55469733b7fa5b7fe3f91f89cda8f7bab",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/261",
            "prTitle": "fix(chat): apply verified scrolling fix on test baseline",
            "prBaseHash": "d821f85dd4746162355c099724e699fde320983b",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/40051343738"
          },
          "gitCommit": {
            "shortHash": "ea5aafc",
            "hash": "ea5aafc55469733b7fa5b7fe3f91f89cda8f7bab",
            "subject": "Merge d69b607e570d9273c1b8264dbad1093e4349a416 into d821f85dd4746162355c099724e699fde320983b",
            "body": "Merge d69b607e570d9273c1b8264dbad1093e4349a416 into d821f85dd4746162355c099724e699fde320983b\n",
            "author": {
              "name": "SINGING-AKIBA",
              "email": "980325684@qq.com",
              "time": 1792806420000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1792806420000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/src/components/mobile/ChatMessageList.test.tsx b/src/components/mobile/ChatMessageList.test.tsx\nindex d498ab4..004e6b5 100644\n--- a/src/components/mobile/ChatMessageList.test.tsx\n+++ b/src/components/mobile/ChatMessageList.test.tsx\n@@ -1,6 +1,7 @@\n import { describe, expect, it } from 'vitest'\n import { fireEvent, render, screen } from '@testing-library/react'\n import ChatMessageList from './ChatMessageList'\n+import PageContainer from './PageContainer'\n \n const longUrl = 'https://hexxueyantest.oss-cn-guangzhou.aliyuncs.com/storage/20261008/' + 'a'.repeat(90)\n \n@@ -25,48 +26,50 @@ describe('ChatMessageList mobile layout', () => {\n     expect(container.querySelector('[data-chat-rich-text]')?.className).toContain('[overflow-wrap:anywhere]')\n   })\n \n-  it('scrolls new messages when already near bottom but preserves history reading position', () => {\n+  it('follows new messages near the bottom and preserves position when reading history', () => {\n     const makeMessages = (count: number) => Array.from({ length: count }, (_, index) => ({\n       id: String(index + 1), role: 'user' as const, text: `消息 ${index + 1}`, status: 'sent' as const,\n     }))\n-    const { container, rerender } = render(\n-      <div data-chat-scroll style={{ height: 100, overflowY: 'auto' }}>\n-        <ChatMessageList messages={makeMessages(2)} />\n-      </div>,\n+    let scrollHeight = 200\n+    const renderChat = (messages: ReturnType<typeof makeMessages>) => (\n+      <PageContainer className=\"h-full !min-h-0 min-w-0 w-full flex flex-col pb-0\" inset={false}>\n+        <div className=\"shrink-0\">客服标题</div>\n+        <section\n+          className=\"min-w-0 min-h-0 w-full flex-1 overflow-y-auto\"\n+          data-chat-scroll\n+          ref={(node) => {\n+            if (node) {\n+              Object.defineProperty(node, 'clientHeight', { configurable: true, value: 100 })\n+              Object.defineProperty(node, 'scrollHeight', { configurable: true, get: () => scrollHeight })\n+            }\n+          }}\n+        >\n+          <ChatMessageList messages={messages} />\n+        </section>\n+        <div className=\"shrink-0\">消息输入区</div>\n+      </PageContainer>\n     )\n+    const { container, rerender } = render(renderChat(makeMessages(2)))\n+    const page = container.querySelector<HTMLElement>('[data-page-container]')!\n     const scroller = container.querySelector<HTMLElement>('[data-chat-scroll]')!\n-    Object.defineProperties(scroller, {\n-      scrollHeight: { configurable: true, value: 500 },\n-      clientHeight: { configurable: true, value: 100 },\n-    })\n+    expect(page.className).toContain('h-full')\n+    expect(page.className).toContain('!min-h-0')\n+    expect(scroller.className).toContain('min-h-0')\n+    expect(scroller.className).toContain('overflow-y-auto')\n+    expect(scroller.scrollTop).toBe(100)\n \n-    // First render starts at the newest message.\n-    rerender(\n-      <div data-chat-scroll style={{ height: 100, overflowY: 'auto' }}>\n-        <ChatMessageList messages={makeMessages(3)} />\n-      </div>,\n-    )\n-    expect(scroller.scrollTop).toBe(500)\n+    scrollHeight = 300\n+    rerender(renderChat(makeMessages(3)))\n+    expect(scroller.scrollTop).toBe(200)\n \n-    // A reader who has scrolled up should not be forced to the bottom.\n     scroller.scrollTop = 0\n     fireEvent.scroll(scroller)\n-    rerender(\n-      <div data-chat-scroll style={{ height: 100, overflowY: 'auto' }}>\n-        <ChatMessageList messages={makeMessages(4)} />\n-      </div>,\n-    )\n+    scrollHeight = 400\n+    rerender(renderChat(makeMessages(4)))\n     expect(scroller.scrollTop).toBe(0)\n \n-    // Returning near the bottom restores follow-new-message behavior.\n-    scroller.scrollTop = 350\n-    fireEvent.scroll(scroller)\n-    rerender(\n-      <div data-chat-scroll style={{ height: 100, overflowY: 'auto' }}>\n-        <ChatMessageList messages={makeMessages(5)} />\n-      </div>,\n-    )\n-    expect(scroller.scrollTop).toBe(500)\n+    scrollHeight = 500\n+    rerender(renderChat([{ id: '0', role: 'user', text: '更早的消息', status: 'sent' }, ...makeMessages(4)]))\n+    expect(scroller.scrollTop).toBe(100)\n   })\n-\n })\ndiff --git a/src/components/mobile/ChatMessageList.tsx b/src/components/mobile/ChatMessageList.tsx\nindex 2be90b7..2948f7c 100644\n--- a/src/components/mobile/ChatMessageList.tsx\n+++ b/src/components/mobile/ChatMessageList.tsx\n@@ -23,36 +23,36 @@ export default function ChatMessageList({\n   className = '',\n }: ChatMessageListProps) {\n   const listRef = useRef<HTMLUListElement>(null)\n-  const wasNearBottom = useRef(true)\n-  const hasMounted = useRef(false)\n+  const followBottomRef = useRef(true)\n+  const previousScrollHeightRef = useRef(0)\n+  const previousMessageCountRef = useRef(0)\n+  const previousFirstMessageIdRef = useRef<string | undefined>(undefined)\n \n   useEffect(() => {\n     const list = listRef.current\n     const scroller = list?.closest<HTMLElement>('[data-chat-scroll]')\n     if (!scroller) return\n \n-    const updatePosition = () => {\n-      wasNearBottom.current =\n-        scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight <= 80\n+    const onScroll = () => {\n+      followBottomRef.current = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight <= 48\n     }\n-    scroller.addEventListener('scroll', updatePosition, { passive: true })\n-    updatePosition()\n-    return () => scroller.removeEventListener('scroll', updatePosition)\n-  }, [])\n+    scroller.addEventListener('scroll', onScroll, { passive: true })\n \n-  useEffect(() => {\n-    const list = listRef.current\n-    const scroller = list?.closest<HTMLElement>('[data-chat-scroll]')\n-    if (!scroller) return\n-    if (!hasMounted.current) {\n-      scroller.scrollTop = scroller.scrollHeight\n-      hasMounted.current = true\n-      wasNearBottom.current = true\n-      return\n-    }\n-    if (wasNearBottom.current) {\n-      scroller.scrollTop = scroller.scrollHeight\n+    const previousHeight = previousScrollHeightRef.current\n+    const currentHeight = scroller.scrollHeight\n+    const firstMessageId = messages[0]?.id\n+    const historyPrepended = previousHeight > 0 && messages.length > previousMessageCountRef.current &&\n+      firstMessageId !== previousFirstMessageIdRef.current\n+    if (previousHeight === 0 || followBottomRef.current) {\n+      scroller.scrollTop = Math.max(0, currentHeight - scroller.clientHeight)\n+    } else if (historyPrepended && currentHeight > previousHeight) {\n+      scroller.scrollTop += currentHeight - previousHeight\n     }\n+    previousScrollHeightRef.current = currentHeight\n+    previousMessageCountRef.current = messages.length\n+    previousFirstMessageIdRef.current = firstMessageId\n+\n+    return () => scroller.removeEventListener('scroll', onScroll)\n   }, [messages])\n \n   return (\ndiff --git a/src/pages/ApiServiceChat.tsx b/src/pages/ApiServiceChat.tsx\nindex dfce26e..2cb06c7 100644\n--- a/src/pages/ApiServiceChat.tsx\n+++ b/src/pages/ApiServiceChat.tsx\n@@ -55,8 +55,8 @@ export default function ApiServiceChat() {\n   }, [location.hash])\n \n   return (\n-    <PageContainer className=\"flex min-h-full min-w-0 w-full flex-col pb-0\" inset={false}>\n-      <div className=\"px-4 pt-3\">\n+    <PageContainer className=\"h-full !min-h-0 min-w-0 w-full flex flex-col pb-0\" inset={false}>\n+      <div className=\"shrink-0 px-4 pt-3\">\n         <p className=\"text-center text-xs text-text-tertiary\">AI 客服 {CHAT_BOT.name} 为您服务</p>\n       </div>\n       <section className=\"min-w-0 min-h-0 w-full flex-1 overflow-y-auto px-4 pb-4 pt-4\" data-chat-scroll aria-label=\"客服聊天历史\" data-human-stage={chat.humanAwait || human.active ? 'pending' : 'idle'}>\n@@ -92,7 +92,7 @@ export default function ApiServiceChat() {\n         )}\n       </section>\n \n-      <div className=\"sticky bottom-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n+      <div className=\"shrink-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n         {chat.error && (\n           <div role=\"alert\" className=\"mb-3 rounded-container bg-surface px-3 py-2 text-xs text-danger-text\">\n             <p>{chat.error}</p>\ndiff --git a/src/pages/ServiceChat.tsx b/src/pages/ServiceChat.tsx\nindex 9538d48..0a15189 100644\n--- a/src/pages/ServiceChat.tsx\n+++ b/src/pages/ServiceChat.tsx\n@@ -176,9 +176,9 @@ function MockServiceChat() {\n       : CHAT_BOT.inputPlaceholder\n \n   return (\n-    <PageContainer className=\"flex min-h-full min-w-0 w-full flex-col pb-0\" inset={false}>\n+    <PageContainer className=\"h-full !min-h-0 min-w-0 w-full flex flex-col pb-0\" inset={false}>\n       {/* 标题栏提供独立的福利官二维码入口；底部人工按钮只负责聊天转接。 */}\n-      <div className=\"px-4 pt-3\">\n+      <div className=\"shrink-0 px-4 pt-3\">\n         <p className=\"text-center text-xs text-text-tertiary\">\n           AI 客服 {CHAT_BOT.name} 为您服务\n         </p>\n@@ -188,7 +188,7 @@ function MockServiceChat() {\n         <ChatMessageList messages={messages} onRetry={retry} />\n       </div>\n \n-      <div className=\"sticky bottom-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n+      <div className=\"shrink-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n         <div className=\"flex items-end gap-2\">\n           <button\n             type=\"button\"\ndiff --git a/src/pages/ServiceHuman.tsx b/src/pages/ServiceHuman.tsx\nindex b35c866..dd8f884 100644\n--- a/src/pages/ServiceHuman.tsx\n+++ b/src/pages/ServiceHuman.tsx\n@@ -62,8 +62,8 @@ export default function ServiceHuman() {\n   }\n \n   return (\n-    <PageContainer className=\"flex min-h-full flex-col pb-0\" inset={false}>\n-      <div className=\"px-4 pt-3\" data-queue-state={queueState}>\n+    <PageContainer className=\"h-full !min-h-0 flex flex-col pb-0\" inset={false}>\n+      <div className=\"shrink-0 px-4 pt-3\" data-queue-state={queueState}>\n         {connected ? (\n           <div className=\"flex items-center gap-2.5 rounded-container bg-surface px-3.5 py-3 shadow-sm\">\n             <span\n@@ -89,7 +89,7 @@ export default function ServiceHuman() {\n         <ChatMessageList messages={messages} />\n       </div>\n \n-      <div className=\"sticky bottom-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n+      <div className=\"shrink-0 border-t border-border-subtle bg-background px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3\">\n         <div className=\"flex items-end gap-2\">\n           <div className=\"flex min-h-11 flex-1 items-center rounded-container border-2 border-transparent bg-surface px-3 focus-within:border-border-focused\">\n             <input",
          "actualWorkers": 2
        },
        "id": "chromium-mobile",
        "name": "chromium-mobile",
        "testDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
        "testIgnore": [],
        "testMatch": [
          "**/*.@(spec|test).?(c|m)[jt]s?(x)"
        ],
        "timeout": 30000
      }
    ],
    "quiet": false,
    "reporter": [
      [
        "line",
        null
      ],
      [
        "html",
        {
          "outputFolder": "playwright-report",
          "open": "never"
        }
      ],
      [
        "json",
        {
          "outputFile": "test-results/playwright-results.json"
        }
      ]
    ],
    "reportSlowTests": {
      "max": 5,
      "threshold": 300000
    },
    "shard": null,
    "tags": [],
    "updateSnapshots": "missing",
    "updateSourceMethod": "patch",
    "version": "1.63.0",
    "workers": 2,
    "webServer": null
  },
  "suites": [
    {
      "title": "test-gate.spec.ts",
      "file": "test-gate.spec.ts",
      "column": 0,
      "line": 0,
      "specs": [
        {
          "title": "@test-gate build identity matches the production-like test policy",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 76,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T01:48:08.848Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-c3b6e0ce5d8c5d8734e5",
          "file": "test-gate.spec.ts",
          "line": 31,
          "column": 1
        },
        {
          "title": "@test-gate browser entry does not start the app router",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1284,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T01:48:08.858Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8ed37c8041402a2a6b34",
          "file": "test-gate.spec.ts",
          "line": 40,
          "column": 1
        },
        {
          "title": "@test-gate / shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1419,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T01:48:09.015Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-b030d57053896c93a550",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /profile shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1005,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T01:48:10.521Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8605535a2807e17fcccc",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /settings shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1133,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T01:48:10.620Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-4a2fdc9884c1725de4f8",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate gated browser entry issues no business API request",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1006,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T01:48:11.543Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-09f38a2b8ed735e192d8",
          "file": "test-gate.spec.ts",
          "line": 65,
          "column": 1
        },
        {
          "title": "@test-gate injected native host bypasses the notice and starts the app",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1021,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T01:48:11.777Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-10-24T01:48:07.883Z",
    "duration": 4995.688999999999,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}