From bd6e59d9677775b62432a855266aabbca9d1e6b5 Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Sat, 5 Sep 2026 15:19:46 -0500 Subject: [PATCH 1/6] perf(marketing): drop the unused Newsreader italic face Nothing on the site sets an italic serif; the italic file was 147 KB of the ~280 KB of display-font bytes mobile visitors waited on before LCP. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JLFzFg8rqENHEar5TRuC7w --- apps/marketing/src/app/layout.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/marketing/src/app/layout.tsx b/apps/marketing/src/app/layout.tsx index 6d79492322..cdd4b3db64 100644 --- a/apps/marketing/src/app/layout.tsx +++ b/apps/marketing/src/app/layout.tsx @@ -24,7 +24,6 @@ const newsreader = Newsreader({ subsets: ["latin"], weight: "variable", axes: ["opsz"], - style: ["normal", "italic"], display: "swap", }); From 4c009d5af96af5906782b95921483f89bb5a6fa5 Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Sat, 5 Sep 2026 15:21:31 -0500 Subject: [PATCH 2/6] perf(marketing): inject the Google One Tap script on idle, not on mount The 98 KB GSI script was ~75% unused on marketing pages and competed with the hero for bandwidth. Wait for requestIdleCallback (4s ceiling), or a short post-load timeout where it is unsupported. No preconnect: One Tap already skips mobile, so a hint there would open an unused connection. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JLFzFg8rqENHEar5TRuC7w --- .../marketing/src/components/GoogleOneTap.tsx | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/apps/marketing/src/components/GoogleOneTap.tsx b/apps/marketing/src/components/GoogleOneTap.tsx index 03a15c396d..107836846f 100644 --- a/apps/marketing/src/components/GoogleOneTap.tsx +++ b/apps/marketing/src/components/GoogleOneTap.tsx @@ -52,27 +52,42 @@ export function GoogleOneTap() { window.google.accounts.id.prompt(); }; - // Load or reuse the GSI script - const existing = document.querySelector( - `script[src="${GOOGLE_GSI_SCRIPT_URL}"]` - ); + // Load or reuse the GSI script. The 98 KB script is ~75% unused on a + // marketing page and competes with the hero for bandwidth, so wait until + // the browser is idle (or shortly after load where idle callbacks are + // unsupported) before injecting it. + const load = () => { + const existing = document.querySelector( + `script[src="${GOOGLE_GSI_SCRIPT_URL}"]` + ); - if (existing) { - if (window.google?.accounts?.id) { - init(); + if (existing) { + if (window.google?.accounts?.id) { + init(); + } else { + existing.addEventListener("load", init); + } } else { - existing.addEventListener("load", init); + const script = document.createElement("script"); + script.src = GOOGLE_GSI_SCRIPT_URL; + script.async = true; + script.defer = true; + script.onload = init; + document.head.appendChild(script); } + }; + + let idleId: number | undefined; + let timeoutId: number | undefined; + if ("requestIdleCallback" in window) { + idleId = window.requestIdleCallback(load, { timeout: 4000 }); } else { - const script = document.createElement("script"); - script.src = GOOGLE_GSI_SCRIPT_URL; - script.async = true; - script.defer = true; - script.onload = init; - document.head.appendChild(script); + timeoutId = window.setTimeout(load, 2500); } return () => { + if (idleId !== undefined) window.cancelIdleCallback(idleId); + if (timeoutId !== undefined) window.clearTimeout(timeoutId); if (window.google?.accounts?.id) { window.google.accounts.id.cancel(); } From b83ab4ae5aa7ff08437b7e54098a8e332cf2fd08 Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Sat, 5 Sep 2026 15:26:38 -0500 Subject: [PATCH 3/6] a11y(marketing): give mock tables a header for every column and wrap orphan list items The task-list mock's checkbox column had an empty , and the document mock rendered an
  • outside any list. Header gets visually hidden text; the list gets a
      that keeps the same disc marker and indent. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JLFzFg8rqENHEar5TRuC7w --- apps/marketing/src/components/sections/landing/landing.css | 1 + apps/marketing/src/components/sections/landing/mocks.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/marketing/src/components/sections/landing/landing.css b/apps/marketing/src/components/sections/landing/landing.css index 49c1810e95..cf5686964c 100644 --- a/apps/marketing/src/components/sections/landing/landing.css +++ b/apps/marketing/src/components/sections/landing/landing.css @@ -342,6 +342,7 @@ & .doc p .fg { color: var(--foreground); } & .doc li { font-size: 14px; color: var(--muted-foreground); margin-left: 20px; line-height: 1.6; } & .stream { display: flex; flex-direction: column; gap: 14px; height: 100%; padding: 18px 20px; } + & .doc ul { margin: 0; padding: 0; list-style: disc; } & .msg { display: flex; gap: 11px; align-items: flex-start; } & .who { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; } & .who .n { font-weight: 600; font-size: 13px; } diff --git a/apps/marketing/src/components/sections/landing/mocks.tsx b/apps/marketing/src/components/sections/landing/mocks.tsx index 5e0e1519fe..988b0ecbcb 100644 --- a/apps/marketing/src/components/sections/landing/mocks.tsx +++ b/apps/marketing/src/components/sections/landing/mocks.tsx @@ -72,7 +72,9 @@ function DocMock() {
      Q1 Planning

      A rich-text editor built on TipTap with markdown shortcuts and code blocks. Write naturally — the toolbar stays out of your way.

      AI edits your document directly. Ask the sidebar chat to rewrite a paragraph and changes appear inline.

      -
    • Real-time collaboration & version history
    • +
        +
      • Real-time collaboration & version history
      • +
      ); @@ -159,7 +161,7 @@ function TaskMock() { - From 4082291c66d4ccdd790a37e443fbcf0d384a60c1 Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Sat, 5 Sep 2026 15:28:18 -0500 Subject: [PATCH 4/6] perf(marketing): reserve the scaled hero window's box in CSS to kill mobile CLS Below 820px ScaledAppWindow lays the hero window out at 760px and scales it down, but only after hydration. Server HTML shipped the unscaled 547px-tall window, so on a throttled phone the hero shrank ~270px about 3s in and pulled the quote band into view (mobile CLS 0.20, the only shift on the page). Reserve the scaled box with aspect-ratio 760/547 so the first paint is already the final height; the effect now only adds the transform. Probed at 360/412/600/760px: one outer height throughout, CLS 0. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JLFzFg8rqENHEar5TRuC7w --- .../src/components/sections/landing/landing.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/marketing/src/components/sections/landing/landing.css b/apps/marketing/src/components/sections/landing/landing.css index cf5686964c..018033787d 100644 --- a/apps/marketing/src/components/sections/landing/landing.css +++ b/apps/marketing/src/components/sections/landing/landing.css @@ -340,9 +340,9 @@ & .doc .h3 { font-size: 22px; font-weight: 700; margin: 0 0 10px; letter-spacing: -0.01em; } & .doc p { font-size: 14px; line-height: 1.65; color: var(--muted-foreground); margin: 0 0 10px; } & .doc p .fg { color: var(--foreground); } + & .doc ul { margin: 0; padding: 0; list-style: disc; } & .doc li { font-size: 14px; color: var(--muted-foreground); margin-left: 20px; line-height: 1.6; } & .stream { display: flex; flex-direction: column; gap: 14px; height: 100%; padding: 18px 20px; } - & .doc ul { margin: 0; padding: 0; list-style: disc; } & .msg { display: flex; gap: 11px; align-items: flex-start; } & .who { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; } & .who .n { font-weight: 600; font-size: 13px; } @@ -488,6 +488,14 @@ @media (max-width: 820px) { .lp .aw-body { height: 500px; } + /* Below 820px the hero window always lays out at ScaledAppWindow's 760px + design width, which with the 500px body above makes it 547px tall, and is + then scaled to fit. Reserve exactly that scaled box (width x 547/760) in + CSS so the server HTML is already the right height; the client effect only + adds the transform. Without this the window shrank ~270px on hydration + (mobile CLS 0.20). Keep the ratio in sync with the mock's chrome height. */ + .lp .appwin-outer { aspect-ratio: 760 / 547; overflow: hidden; } + .lp .appwin-inner { width: 760px; transform-origin: top left; } } /* The hero app window keeps all three panes on every screen; below 900px the From 7c2e4f2fe78023d7087d2c82c4fc8c3f1eab8a21 Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Sat, 5 Sep 2026 15:31:10 -0500 Subject: [PATCH 5/6] a11y(marketing): decorative logo alt, hide inactive carousel slides, lift their opacity to 0.9 - Nav and footer logos sit beside visible "PageSpace" text, so alt="" stops the name being announced twice. - Inactive page-type slides are previews: aria-hidden so readers get one slide, and opacity raised from 0.5 to 0.9, the lowest value at which muted 12px text inside a dimmed slide still meets 4.5:1 (axe blends the slide's opacity into the text colour; every remaining contrast failure was one of these). Lighthouse a11y on dev: 92 -> 100. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JLFzFg8rqENHEar5TRuC7w --- apps/marketing/src/components/SiteFooter.tsx | 2 +- apps/marketing/src/components/SiteNavbar.tsx | 2 +- .../src/components/sections/PageTypeCarouselSection.tsx | 2 +- apps/marketing/src/components/sections/landing/landing.css | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/marketing/src/components/SiteFooter.tsx b/apps/marketing/src/components/SiteFooter.tsx index 3ae728c668..1d57e6687d 100644 --- a/apps/marketing/src/components/SiteFooter.tsx +++ b/apps/marketing/src/components/SiteFooter.tsx @@ -57,7 +57,7 @@ export function SiteFooter() { {PAGE_TYPES.map((t, i) => ( - + diff --git a/apps/marketing/src/components/sections/landing/landing.css b/apps/marketing/src/components/sections/landing/landing.css index 018033787d..f37a708f38 100644 --- a/apps/marketing/src/components/sections/landing/landing.css +++ b/apps/marketing/src/components/sections/landing/landing.css @@ -262,7 +262,9 @@ /* Emphasis via opacity only — scaling inactive slides made the edge-to-edge gaps look uneven (a shrunk neighbor reads as a wider gap). Uniform size keeps every gap identical. */ - & .slide { transition: opacity .3s ease; opacity: 0.5; } + /* 0.90 is the lowest opacity at which muted 12px text inside a dimmed slide + still clears 4.5:1 on the card (axe blends slide opacity into the text). */ + & .slide { transition: opacity .3s ease; opacity: 0.90; } & .slide[data-active="true"] { opacity: 1; } /* Reserve two lines so captions of different lengths don't shift the page. */ & .caption { text-align: center; margin-top: 12px; color: var(--muted-foreground); font-size: var(--t-body); line-height: 1.5; max-width: 600px; margin-left: auto; margin-right: auto; min-height: 48px; } From c1327bdbfd8b9a57e726901b732e466a695f96ec Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Sat, 5 Sep 2026 15:42:45 -0500 Subject: [PATCH 6/6] fix(marketing): use typeof for the requestIdleCallback guard `"requestIdleCallback" in window` narrows window to never in the else branch under lib.dom, failing typecheck and the Next build in CI. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JLFzFg8rqENHEar5TRuC7w --- apps/marketing/src/components/GoogleOneTap.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/marketing/src/components/GoogleOneTap.tsx b/apps/marketing/src/components/GoogleOneTap.tsx index 107836846f..7c74ce33e5 100644 --- a/apps/marketing/src/components/GoogleOneTap.tsx +++ b/apps/marketing/src/components/GoogleOneTap.tsx @@ -79,7 +79,9 @@ export function GoogleOneTap() { let idleId: number | undefined; let timeoutId: number | undefined; - if ("requestIdleCallback" in window) { + // typeof, not `in`: with lib.dom declaring requestIdleCallback the `in` + // guard narrows `window` to `never` in the else branch (TS2339 in CI). + if (typeof window.requestIdleCallback === "function") { idleId = window.requestIdleCallback(load, { timeout: 4000 }); } else { timeoutId = window.setTimeout(load, 2500);
      + Done Task Status Priority