diff --git a/.lane/reports/fix-product-hero-early-access-cta-mobile.md b/.lane/reports/fix-product-hero-early-access-cta-mobile.md
new file mode 100644
index 00000000..97606e79
--- /dev/null
+++ b/.lane/reports/fix-product-hero-early-access-cta-mobile.md
@@ -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
diff --git a/public/drishti/index.html b/public/drishti/index.html
index 9ef4bf15..00a3c008 100644
--- a/public/drishti/index.html
+++ b/public/drishti/index.html
@@ -104,17 +104,17 @@
Drishti
Mindful pause before distraction
Drishti helps bring awareness to screen time before distraction takes over.
+
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.
-
iPhone app
Mindful screen time
diff --git a/public/promptly/index.html b/public/promptly/index.html
index c151f290..88220e9a 100644
--- a/public/promptly/index.html
+++ b/public/promptly/index.html
@@ -104,17 +104,17 @@
Promptly
Booking + no-show prevention
Promptly keeps solo professionals booked, prepared, and harder to ghost.
+
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.
-
iPhone app
Reminder windows: 24h, 2h, 30min
diff --git a/public/styles.css b/public/styles.css
index 7c825fc5..c74298f1 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -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;
diff --git a/scripts/test-public-heading-hierarchy.mjs b/scripts/test-public-heading-hierarchy.mjs
index 01c8b0e4..58fe70e0 100644
--- a/scripts/test-public-heading-hierarchy.mjs
+++ b/scripts/test-public-heading-hierarchy.mjs
@@ -215,6 +215,8 @@ 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,
@@ -222,7 +224,11 @@ const measurePage = async (chromium, origin, path, width) => {
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 }
@@ -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)
@@ -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()