diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 67d9da56..130b2aa6 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -235,7 +235,8 @@ is read through an external store so a change in one tab reaches the others. - Mixed headings stay in one face: the slice set carries Latin, so “D10 事业盘怎么读” is serif end to end (the reason Newsreader was rejected in BUG-737 does not apply). - The stack may name only loadable families (next/font/local, the serif-sc `@font-face` list, or the system-face whitelist) and must not contain `Songti SC`, `STSong`, `SimSun`, `Noto Serif CJK SC` or end in generic `serif` — pinned by `font-stack-loadable-contract.test.ts`. BUG-737 is why: an unloaded serif head sent every CJK heading to SimSun on Windows. - The `@font-face` list lives in its own file (`serif-sc.css`, ≈22 KB gzip of `unicode-range` data) imported by `src/app/site-styles.ts` next to `globals.css`, so admin never loads it. - - Deliberate sans headings (pinned by `serif-headings-contract.test.ts`): the rectification narrative's `h2`/`h3` (`.conversational-narrative .message-markdown h2, h3`, body face 600), report chart-card titles (`.personal-report-chart-card h3`), all admin UI, and the root boundary pages below. Chat body, buttons, tables, inputs and labels are body text and stay sans. + - Deliberate sans headings (pinned by `serif-headings-contract.test.ts`): the rectification narrative's `h2`/`h3` (`.conversational-narrative .message-markdown h2, h3`, body face 600), report chart-card titles (`.personal-report-chart-card h3`), the home loading screen title (`.app-loading-content strong`, 「正在载入账户 / 正在准备对话」, 2026-09-30, BUG-1129), all admin UI, and the root boundary pages below. + - The loading screen uses the body face only: it paints before the bundle runs, so a serif title would start 3–4 slice downloads (≈130–170 KB) that compete with the first-screen JS. No web font is requested for it before the page `load` event. Chat body, buttons, tables, inputs and labels are body text and stay sans. - Body/UI: `var(--font-inter, Inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`. Inter is loaded with `next/font/local` from `src/app/fonts/InterVariable-latin.woff2` (`display: "swap"`, CSS variable `--font-inter`) so Windows/Linux no longer silently fall back past a never-requested Inter, and image builds do not call fonts.googleapis.com. `StyreneB` was removed from the head of this stack for the same reason as Tiempos: it never loaded, so it was dead configuration that made the stack look intentional. - Code/data: `"JetBrains Mono", "SFMono-Regular", Consolas, monospace`, exposed as `--font-mono`. - Root boundary pages (`error.tsx`, `not-found.tsx`, `forbidden.tsx`, `global-error.tsx`) sit in the shared root layout segment and must not import `globals.css` — importing it would drag the chat stylesheet onto every admin route. They therefore cannot read `--font-mono` or any token, and inline their own values: a system stack for body text and `ui-monospace, SFMono-Regular, Menlo, monospace` for code. Keep those literals in step with the stacks above by hand. They do not load the heading serif either: their titles stay in the inline sans stack by design (TASK-serif-headings-20260928), not by omission. diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 25a3cd9b..9accc1a2 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -774,7 +774,8 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页 } .app-loading { width: 100%; height: 100vh; display: grid; place-items: center; padding: 24px; background: var(--color-canvas); } -.app-loading-content strong { margin-top: 10px; color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-lg); font-weight: 500; letter-spacing: -.3px; } +/* Sans on purpose (BUG-1129): this screen paints before the bundle runs; a serif title would fetch 3-4 font slices that compete with the critical JS. */ +.app-loading-content strong { margin-top: 10px; color: var(--color-ink); font-family: var(--font-body); font-size: var(--type-title-lg); font-weight: 500; letter-spacing: -.3px; } .app-loading-content > span { max-width: 360px; line-height: 1.55; font-size: var(--type-body-sm); } .app-loading-orbit { position: absolute; inset: 0; border: 1px solid var(--color-border); border-radius: 50%; animation: app-loading-orbit 1.4s linear infinite; border-color: var(--color-canvas-strong); } .app-loading-error { background: var(--color-canvas-soft); } diff --git a/frontend/tests/serif-headings-contract.test.ts b/frontend/tests/serif-headings-contract.test.ts index a586183c..91494cbc 100644 --- a/frontend/tests/serif-headings-contract.test.ts +++ b/frontend/tests/serif-headings-contract.test.ts @@ -46,6 +46,14 @@ test("report chart-card titles stay on the body face", () => { assert.doesNotMatch(body, /--font-display/); }); +test("the loading screen title stays on the body face (BUG-1129)", () => { + // It paints before the bundle runs; a serif title would pull 3-4 font slices + // onto the critical path next to the first-screen JS. + const body = exactRule(".app-loading-content strong"); + assert.match(body, /font-family:\s*var\(--font-body\)/); + assert.doesNotMatch(body, /--font-display/); +}); + test("assistant answer headings and report cover / section titles take the heading face", () => { assert.match(exactRule(".message-markdown h2"), /font-family:\s*var\(--font-display\)/); assert.match(cssDeclarations(".personal-report-cover h1"), /font-family:\s*var\(--font-display\)/);