Skip to content

Rework the mobile above-the-fold hero - #1113

Open
blove wants to merge 16 commits into
mainfrom
blove/sharp-shaw-c45f8c
Open

blove wants to merge 16 commits into
mainfrom
blove/sharp-shaw-c45f8c

Conversation

@blove

@blove blove commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Reworks the threadplane.ai homepage hero for phone viewports. Spec: docs/superpowers/specs/2026-09-18-mobile-above-fold-design.md.

Why

Four defects, each observed directly in a browser at 375x812 against production:

  1. The H1 rendered four lines / 207px — a quarter of the fold — breaking "open-source" across its hyphen.
  2. The phone hero poster was sliced across its top edge, opening on a tool-result table cut through horizontally.
  3. No autoplay below 768px: phones got a static poster and a "Play walkthrough" button.
  4. "Angular" appeared nowhere in the H1 or the <title>, only in a small low-contrast eyebrow.

PostHog shows mobile median session duration at 2-3s against 6s+ on desktop, but that sample is ~41 sessions/30d and is directional only. This is justified by the defects above, not by a bounce emergency — homepage bounce has been improving steeply and is already below the sector median.

What changed

Before After
H1 4 lines / 207px 3 lines / 116.63px
[data-hero-demo] top 592px 501.42px
Phone autoplay none iframe mounts, state: ready
Angular in <title> no yes
Mobile poster sliced frames whole, 34,124 B
  • Copy (positioning.ts only): H1 is now The open-source thread-plane for Angular agents.; HERO_EYEBROW drops the now-duplicated Angular ·; PRIMARY_TAGLINE gains Angular, which is what puts it in <title> and the site-wide OG defaults.
  • Type ramp: --text-h1 is clamp(48px, 6vw, 72px), so at 375px the clamp floor is what renders. landing.css overrides .hero-heading to 36px and flows .hero-heading-line inline below 767px. Both halves are required.
  • Autoplay: MIN_AUTOPLAY_WIDTH 768 → 0. A live iframe lays out at the stage's real width, so this improves legibility by construction, not just motion. prefers-reduced-motion still gates it at every width.
  • Poster: re-recorded at 390x906 (65:151) with a new top-edge guard in the recorder.
  • Backup table: stacks below 480px in the demo app (583px → 268px), which is what made a clean capture window exist.

Notable findings

  • The poster has shipped sliced since 8900e1886, which introduced a list_backups table taller than the whole capture frame. Not timing drift — captures are byte-identical.
  • The phone stage aspect-ratio is derived, not chosen: the chat timeline is bottom-pinned, so the capture height decides where the frame's top edge lands and it must fall in a gap between message blocks. The recorder's header documents the measurement so it can be re-derived.
  • nx test website had been red for three commits while the Playwright suite stayed green, because the stale guards assert CSS the e2e never reads.
  • nx build examples-chat-angular compiles spec files without dom.iterable, so a NodeList spread broke the build while nx test stayed green.

New guards, all mutation-tested

  • Fold budgets at 375x812: .hero-heading ≤ 130px, [data-hero-demo] top ≤ 530px. Absolute, not relative — a relative guard passes vacuously once the thing it guards drifts, which is how both defects shipped. Verified to fail (259px / 643.94px) with the fix reverted.
  • Top-edge assertion in the poster recorder, verified to reproduce the real shipped defect.
  • toEqual pin on HERO_H1_LINES, which is also the line-breaking for two fixed-height card renderers.
  • Phone-width autoplay and reduced-motion-at-phone-width unit tests; the latter was previously uncovered.

Test plan

  • nx test website, nx lint website, nx build website — all exit 0
  • nx test examples-chat-angular, nx build examples-chat-angular — exit 0
  • nx e2e website --grep "homepage hero" — 5/5
  • WEBSITE_E2E_MODE=production nx e2e website — 175 passed, incl. the public-copy contract gate
  • Verified in a real browser at 375x812 against a production build
  • Manual, required after merge: re-upload docs/brand/github-social-preview.png at Settings → General → Social preview. GitHub's social preview has no API.

Known open

  • The top-edge guard runs at re-record time, not in CI, so hero-replay.json drift can still stale the committed poster silently.
  • Nothing asserts the card eyebrow surfaces agree — which is how hero.svg went stale. marketing/assets deliberately keeps ANGULAR (its cards carry a blog title, not the hero H1) and now records why.
  • LCP before/after (328ms → 319ms, poster still the LCP element) is unthrottled local, not cellular. A throttled Lighthouse mobile run against prod is the real close-out on the autoplay risk.

🤖 Generated with Claude Code

blove and others added 16 commits September 19, 2026 08:15
Four defects observed at 375x812: a four-line 207px H1, a mobile poster
sliced across its top edge, no autoplay below 768px, and Angular absent
from both the H1 and the title.

Records two corrections found while diagnosing: the mobile poster is a
purpose-built 390x650 capture rather than a scaled desktop one, and its
slicing is drift from the beat its recorder documents rather than the
beat itself. Also resolves the messaging.md hero drift deliberately,
keeping the category claim and adding the stack to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Amends the spec with canvas-measured card line widths: HERO_H1_LINES
stays three spans, re-split to absorb "Angular", because the array is
also the line-breaking for two fixed-height card images. A two-span
split measured 964px against a ~536px column. The phone fix is a
font-size override plus flowing the spans inline, since the shared
--text-h1 clamp floor of 48px is what renders at 375px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
website.spec.ts hard-coded the H1 and guarded "Angular" on the eyebrow —
the assertion this work inverts — and site-metadata.ts hand-duplicates
the tagline in the site-wide OG alt. Neither was enumerated. Also folds
the card eyebrow duplication into Task 6, gtm.md and README into Task 7,
and records that nx build website needs GROWTH_FORM_POLICY=growth_v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The phone poster is a 390x650 capture displayed in a ~335px stage — about
0.86 scale, so its type is degraded. A live iframe lays out at the stage's
real width, so its type renders at its designed size. Dropping the 768px
autoplay floor to 0 improves legibility by construction, not just motion.

The `@media (max-width: 767px)` breakpoint is now a TWO-way coupling — the
3:5 stage ratio and the <source media> — both tied to the poster's geometry.
MIN_AUTOPLAY_WIDTH is no longer part of it; both comments say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "reclaims 90px" figure was measured against the pre-Angular copy.
Against the copy that now ships, block spans make all three lines wrap,
so the real pre-fix state is 6 lines / 311px and the reclaim is ~194px
(~24% of the fold). Both baselines are real; the spec now names which
is which rather than implying one.

Also drops scrollIntoViewIfNeeded from the Task 4 phone test: the demo
now sits at y=501 with 51% visible on load, so asserting the mount from
a standing start is what proves the fold win.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The phone poster has opened on a backup table cut through horizontally
since the approval tools became executable (#1011): that run streams a
669px result table, taller than the whole 390x650 frame, and the timeline
is pinned to the bottom, so the frame's top edge landed inside it.

Guard the top edge in the recorder. The clipping edge is the chat's own
scroll container, not y=0, so a sliced block can still have a positive y;
the guard fails when any chat-message straddles that edge and prints the
measured geometry.

The height budget genuinely moved and could not stay 3:5. 526 is the
middle of the gap that puts the edge above the delete_backups chip, so
the poster keeps the approval beat and opens on nothing sliced. The
capture height, POSTER_MOBILE_W/H and .hero-demo-stage's aspect-ratio
move together to 195:263 so object-fit: cover stays a no-op.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
At 390px the four-column grid wrapped the S3 path to four lines per row
via word-break: break-all, making the table 583px tall. Below 480px each
backup is now a two-line block — id + size, then truncated path + date —
which is 268px and keeps all four values. CSS only, so one template, no
resize listener, and desktop is untouched.

This makes the live phone demo readable. It does NOT reframe the hero
poster: the timeline is bottom-pinned, so shrinking the table moves only
its top edge and its bottom stays put.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tsconfig.app.json` includes spec files (`src/**/*.ts`) and its lib set is
`["es2022", "dom"]` with no `dom.iterable`, so spreading a
`NodeListOf<Element>` fails with TS2488 and breaks
`nx build examples-chat-angular`. `nx test` does not typecheck, so the
break was invisible there and landed in 93307be6d.

Verified: without the change `nx build examples-chat-angular` exits 1 with
TS2488; with it, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the phone capture from 390x526 to 390x906 (65:151, shipped 585x1359).

The timeline is bottom-pinned, so the capture height decides where the
frame's top edge lands and it must fall in a gap between messages. Measured
from the DOM at h=866, with the edge fixed at y=62: user prompt -54..10,
list_backups table 34..388, delete_backups chip 412..455, answer 479..705.
Raising h by d shifts every box down by d, so h = 928 - e for an edge
position e, giving windows (449, 473), (516, 540) and (894, 918). 906 is the
middle of the third: the prompt's bottom lands at 50, wholly above the edge,
and the table's top at 74, clear below it.

526 framed the story but shrank the live stage: this ratio also sizes the
autoplaying iframe below 768px, so the earlier budget cost the phone demo 19%
of its viewport. 906 frames the whole streamed backup table AND gives the
live demo more room than it has ever had.

Also updates the two guards that enforce the coupling. They still asserted
3/5 and 975 -- the pre-Task-5 geometry -- so `nx test website` had been red
since 017568f8e moved the source without them.

The poster ships at webp quality 40, not 55: the taller capture pushed q55 to
39,016 bytes, over the 36,000-byte budget. Single-encode from one capture,
q55 39,050 / q50 37,880 / q45 36,394 / q40 34,094 / q35 32,264. q40 clears
the budget with ~1.9KB of headroom and is indistinguishable from q55 at 1:1
in the table's monospace text, so the budget itself is unchanged.

Verified at 375x812: [data-hero-demo] top 501.4 (budget <= 530), stage
333x774, visibleFraction 0.342 against the 0.25 autoplay threshold, state
"ready" with the iframe mounted. Top-edge guard confirmed non-vacuous at 906
-- with the responsive table disabled it trips with the table spanning
-241..429 across the edge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three corrections learned by running it: nx build website needs
GROWTH_FORM_POLICY=growth_v1; examples-chat-angular must be BUILT, not
just tested, because its tsconfig compiles specs without dom.iterable;
and card:github defaults to fetching production, so regenerating before
deploy captures the copy being replaced.

Also scopes hero.svg into Task 7 — it claims to mirror the social cards
and nothing tests that claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The phone stage ratio moved three times on this branch and five comments
still asserted a superseded value as current — including the outer
comment sitting directly on the .hero-demo-stage rule, which claimed
390x650/3:5 fifteen lines above aspect-ratio: 65/151. That is the exact
failure mode this branch exists to fix, so it is worth the sweep.

Also drops two over-claims. The recorder header presented the taller
live stage as an unambiguous win; it is not — at 375px only ~34% of the
stage is ever on screen, and the honest justification for 906 is poster
framing, with the live stage along for the ride because object-fit:
cover is only a no-op while the ratios match. And the spec said the
top-edge guard "converts a silent drift into a failing capture"; it
converts it into a failing RE-RECORD, which is weaker, because nothing
checks the committed poster in CI.

Records why marketing/assets deliberately keeps ANGULAR in its eyebrow
while the website's cards dropped it: those cards carry a blog title,
not the hero H1, so the word adds information rather than repeating it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 19, 2026 3:19pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant