Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .lane/reports/fix-product-hero-early-access-cta-mobile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Lane report: tinystudio-in lane 1 — product hero CTA buried below mobile fold

## Item
[unreviewed-by-opus] Promptly and Drishti mobile heroes bury the newly added Get early access CTA below the first [viewport]

## Root cause
Shared `h1` rule: `font-size: clamp(3.4rem, 7vw, 6.7rem)` + mobile `max-width: 9ch`. At 320–414px the 7vw font with a 9ch cap made the long product hero H1s ~544px tall (over half an 844px viewport), and with the action-row after the long page-lead in DOM order, the CTA bottom landed at ~1035–1191px — below the fold everywhere, including desktop 1024px.

Measured before (Playwright, 844px viewport): Promptly CTA bottom 1083px @390; Drishti 1164px @390.

## Fix (PR #147)
1. `public/styles.css` — mobile-only `.page-hero-card h1 { font-size: clamp(2.1rem, 5.6vw, 3.4rem); max-width: none; }` inside the existing `@media (max-width: 720px)` block. Homepage untouched; desktop floor (3.4rem) preserved.
2. `public/promptly/index.html`, `public/drishti/index.html` — moved the hero `.action-row` above `.page-lead` so the CTA is first after the H1 on narrow screens. Desktop two-column layout unaffected.
3. `scripts/test-public-heading-hierarchy.mjs` — extended the existing 320/390px Playwright layout probe to assert the early-access CTA is fully within the first viewport on both pages (fails closed when Playwright is missing).

## Verification
- Playwright before/after, all mobile widths (320/360/390/414) and heights (667/740/844): CTA fully in-fold after; at 320×667 it now sits at 536–584px top/bottom (was ~1035–1083+).
- Hero H1 height at 320px: 544px -> 202px.
- `test-public-heading-hierarchy.mjs`: 93 checks, 0 failures (includes new CTA assertions, rendered at 320 and 390).
- All 10 public-site tests pass; all other CI scripts pass; `node --check` on every script passes.
- `check-retention-automation.mjs` fails in CI for environmental reasons (stale-checkout comparison against a different checkout path), pre-existing and unrelated.
- Visual confirmation via 390×844 screenshots: heading, all three buttons, lead, and chips visible in first viewport.

## Files changed
- public/styles.css
- public/promptly/index.html
- public/drishti/index.html
- scripts/test-public-heading-hierarchy.mjs

## PR
https://github.com/nish3451/tinystudio-in/pull/147
10 changes: 5 additions & 5 deletions public/drishti/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,17 @@
<p class="eyebrow">Drishti</p>
<div class="app-status"><span class="dot" aria-hidden="true"></span> Mindful pause before distraction</div>
<h1>Drishti helps bring awareness to screen time before distraction takes over.</h1>
<div class="action-row">
<a class="button" href="mailto:support@tinystudio.in?subject=Drishti%20early%20access">Get early access</a>
<a class="ghost-button" href="/drishti/support/">Drishti support</a>
<a class="ghost-button" href="/drishti/privacy/">Drishti privacy</a>
</div>
<p class="page-lead">
Drishti is a mindful screen-time app built around intentional
pauses. When you try to open a distracting app, it creates a
moment to breathe, check your intention, and choose with more
awareness instead of running on autopilot.
</p>
<div class="action-row">
<a class="button" href="mailto:support@tinystudio.in?subject=Drishti%20early%20access">Get early access</a>
<a class="ghost-button" href="/drishti/support/">Drishti support</a>
<a class="ghost-button" href="/drishti/privacy/">Drishti privacy</a>
</div>
<div class="meta-row">
<span class="chip accent">iPhone app</span>
<span class="chip">Mindful screen time</span>
Expand Down
10 changes: 5 additions & 5 deletions public/promptly/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,17 @@
<p class="eyebrow">Promptly</p>
<div class="app-status"><span class="dot" aria-hidden="true"></span> Booking + no-show prevention</div>
<h1>Promptly keeps solo professionals booked, prepared, and harder to ghost.</h1>
<div class="action-row">
<a class="button" href="mailto:support@tinystudio.in?subject=Promptly%20early%20access">Get early access</a>
<a class="ghost-button" href="/promptly/support/">Promptly support</a>
<a class="ghost-button" href="/promptly/privacy/">Promptly privacy</a>
</div>
<p class="page-lead">
Promptly is the mobile control room for solo professionals.
It brings bookings, reminders, payment proof, and day-of clarity
into one focused workflow, while clients simply open links to
book, confirm, reschedule, or cancel.
</p>
<div class="action-row">
<a class="button" href="mailto:support@tinystudio.in?subject=Promptly%20early%20access">Get early access</a>
<a class="ghost-button" href="/promptly/support/">Promptly support</a>
<a class="ghost-button" href="/promptly/privacy/">Promptly privacy</a>
</div>
<div class="meta-row">
<span class="chip accent">iPhone app</span>
<span class="chip">Reminder windows: 24h, 2h, 30min</span>
Expand Down
5 changes: 5 additions & 0 deletions public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,11 @@ h4 {
max-width: 9ch;
}

.page-hero-card h1 {
font-size: clamp(2.1rem, 5.6vw, 3.4rem);
max-width: none;
}

.product-row,
.portfolio-row {
grid-template-columns: 1fr;
Expand Down
27 changes: 26 additions & 1 deletion scripts/test-public-heading-hierarchy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,20 @@ const measurePage = async (chromium, origin, path, width) => {
const response = await page.goto(`${origin}${path}`, { waitUntil: "domcontentloaded" })
const metrics = await page.evaluate(() => {
const heading = document.querySelector("h1")
const cta = document.querySelector('.action-row a.button[href^="mailto:"]')
const ctaRect = cta ? cta.getBoundingClientRect() : null
return {
heading: heading ? heading.innerText.replace(/\s+/g, " ").trim() : "",
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
headingScrollWidth: heading ? heading.scrollWidth : 0,
headingClientWidth: heading ? heading.clientWidth : 0,
overflowWrap: heading ? getComputedStyle(heading).overflowWrap : "",
overflowX: getComputedStyle(document.documentElement).overflowX
overflowX: getComputedStyle(document.documentElement).overflowX,
ctaVisible: cta ? ctaRect.top < innerHeight && ctaRect.bottom >= 0 : false,
ctaTop: ctaRect ? ctaRect.top : -1,
ctaBottom: ctaRect ? ctaRect.bottom : -1,
ctaText: cta ? cta.innerText.replace(/\s+/g, " ").trim() : ""
}
})
return { status: response?.status() ?? 0, ...metrics }
Expand Down Expand Up @@ -270,6 +276,14 @@ if (!chromiumLauncher) {
promptly320.overflowX !== "hidden" && promptly390.overflowX !== "hidden",
"document overflow-x is not hidden"
)
ok(
promptly320.ctaVisible && promptly390.ctaVisible,
"Promptly early-access CTA is fully within the first mobile viewport"
)
ok(
promptly320.ctaText === "Get early access" && promptly390.ctaText === "Get early access",
`Promptly early-access CTA copy is unchanged (got "${promptly320.ctaText}" at 320)`
)

for (const path of ["/", "/drishti/"]) {
const sibling = await measurePage(browser, origin, path, 320)
Expand All @@ -282,6 +296,17 @@ if (!chromiumLauncher) {
sibling.headingScrollWidth <= sibling.headingClientWidth,
`${path} heading stays inside its box at 320 (${sibling.headingScrollWidth} <= ${sibling.headingClientWidth})`
)
if (path === "/drishti/") {
const drishti390 = await measurePage(browser, origin, "/drishti/", 390)
ok(
drishti390.ctaVisible,
"Drishti early-access CTA is fully within the first mobile viewport"
)
ok(
drishti390.ctaText === "Get early access",
`Drishti early-access CTA copy is unchanged (got "${drishti390.ctaText}" at 390)`
)
}
}
} finally {
await browser.close()
Expand Down