fix(hero): apply gradient to last H1 word post-split so 'revenue.' renders (was clipped by line mask)

This commit is contained in:
Feedback Studios 2026-06-16 08:26:38 +00:00
parent 9e2fd18cb7
commit b67840270b
34 changed files with 386 additions and 1 deletions

View file

@ -88,6 +88,10 @@ export default function Hero() {
type: "lines,words", type: "lines,words",
linesClass: "hero__line", linesClass: "hero__line",
}); });
// Apply the brand gradient to the final word ("revenue.") AFTER splitting.
// A pre-nested <span class="grad"> got clipped by the line mask and never
// revealed; as a normal split word it animates + shows correctly.
split.words[split.words.length - 1]?.classList.add("grad");
gsap.set(".hero__h1", { autoAlpha: 1 }); gsap.set(".hero__h1", { autoAlpha: 1 });
gsap.set(".hero__line", { overflow: "hidden" }); gsap.set(".hero__line", { overflow: "hidden" });
// Fast, tight cascade so the FULL headline — including the last word // Fast, tight cascade so the FULL headline — including the last word
@ -255,7 +259,7 @@ export default function Hero() {
</p> </p>
<h1 id="hero-h1" className="hero__h1"> <h1 id="hero-h1" className="hero__h1">
Marketing that grows your <span className="grad">revenue.</span> Marketing that grows your revenue.
</h1> </h1>
<p className="hero__sub hero__stagger"> <p className="hero__sub hero__stagger">

BIN
audit2/desktop-faq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

BIN
audit2/desktop-final.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
audit2/desktop-fold.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
audit2/desktop-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 MiB

BIN
audit2/desktop-h1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

BIN
audit2/desktop-hero-top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

BIN
audit2/desktop-services.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

BIN
audit2/desktop-work.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
audit2/mobile-fold.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

BIN
audit2/mobile-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
audit2/mobile-hero-top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

BIN
audit2/mobile-trustedby.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

1
audit2/page.html Normal file

File diff suppressed because one or more lines are too long

165
audit2/sections.json Normal file
View file

@ -0,0 +1,165 @@
{
"scrollHeight": 11848,
"sections": [
{
"i": 0,
"tag": "HEADER",
"cls": "site-head ",
"top": 0,
"h": 85,
"txt": "Feedback Studios Services Work About FAQ Get a growth audit"
},
{
"i": 1,
"tag": "SECTION",
"cls": "hero",
"top": 0,
"h": 900,
"txt": "RESULTS-DRIVEN DIGITAL MARKETING AGENCY Marketing that grows your revenue. We run paid, SE"
},
{
"i": 2,
"tag": "SECTION",
"cls": "tape",
"top": 900,
"h": 133,
"txt": "Revenue, not vanity metrics ✱ Paid ✱ SEO ✱ Content ✱ Social ✱ +52% ROAS ✱ +217% demos ✱ +1"
},
{
"i": 3,
"tag": "SECTION",
"cls": "manifesto frame",
"top": 1033,
"h": 951,
"txt": "THE PROBLEM WITH MOST AGENCIES Most budgets buy activity, not outcomes. Dashboards fill wi"
},
{
"i": 4,
"tag": "SECTION",
"cls": "proof",
"top": 1983,
"h": 296,
"txt": "TRUSTED BY TEAMS THAT CARE ABOUT THE SALES NUMBER E-commerce B2B SaaS Clinics Professional"
},
{
"i": 5,
"tag": "SECTION",
"cls": "services frame",
"top": 2374,
"h": 1165,
"txt": "SERVICES / 06 How we grow your business 01 SEO that compounds Rank for the searches your b"
},
{
"i": 6,
"tag": "HEADER",
"cls": "sec-head",
"top": 2504,
"h": 114,
"txt": "SERVICES / 06 How we grow your business"
},
{
"i": 7,
"tag": "SECTION",
"cls": "score frame",
"top": 3539,
"h": 797,
"txt": "THE SCOREBOARD — SAMPLE DATA Numbers we report on client revenue generated $40M+ client re"
},
{
"i": 8,
"tag": "HEADER",
"cls": "sec-head sec-head--center",
"top": 3669,
"h": 128,
"txt": "THE SCOREBOARD — SAMPLE DATA Numbers we report on"
},
{
"i": 9,
"tag": "SECTION",
"cls": "work frame",
"top": 4336,
"h": 1077,
"txt": "SELECTED WORK — SAMPLE Proof, not promises 01 E-commerce · Fashion BEFORE Rising ad costs "
},
{
"i": 10,
"tag": "HEADER",
"cls": "sec-head",
"top": 4466,
"h": 114,
"txt": "SELECTED WORK — SAMPLE Proof, not promises"
},
{
"i": 11,
"tag": "SECTION",
"cls": "loop frame",
"top": 5413,
"h": 3094,
"txt": "THE FEEDBACK LOOP How it works 01 / 04 REVENUE LEAK — DETECTED $14k/mo wasted spend CHANN"
},
{
"i": 12,
"tag": "HEADER",
"cls": "sec-head loop__head",
"top": 5644,
"h": 128,
"txt": "THE FEEDBACK LOOP How it works 01 / 04"
},
{
"i": 13,
"tag": "SECTION",
"cls": "quotes frame",
"top": 8507,
"h": 810,
"txt": "IN THEIR WORDS — SAMPLE The number is the point “ We were spending $20k a month on ads wit"
},
{
"i": 14,
"tag": "HEADER",
"cls": "sec-head",
"top": 8637,
"h": 114,
"txt": "IN THEIR WORDS — SAMPLE The number is the point"
},
{
"i": 15,
"tag": "SECTION",
"cls": "faq-sec frame",
"top": 9317,
"h": 1254,
"txt": "FAQ Questions about working with a digital marketing agency What does a digital marketing "
},
{
"i": 16,
"tag": "HEADER",
"cls": "sec-head",
"top": 9447,
"h": 280,
"txt": "FAQ Questions about working with a digital marketing agency"
},
{
"i": 17,
"tag": "SECTION",
"cls": "final frame",
"top": 10571,
"h": 737,
"txt": "THE BOTTOM LINE Ready to grow? No long contracts. No vanity reports. Marketing you can mea"
},
{
"i": 18,
"tag": "HEADER",
"cls": "",
"top": 10724,
"h": 280,
"txt": "Ready to grow?"
},
{
"i": 19,
"tag": "FOOTER",
"cls": "colophon frame",
"top": 11308,
"h": 540,
"txt": "Feedback Studios. Marketing that grows your revenue. Services Work About FAQ Contact Linke"
}
]
}

1
audit2/site.css Normal file

File diff suppressed because one or more lines are too long

85
capture2.js Normal file
View file

@ -0,0 +1,85 @@
const { chromium } = require('playwright');
const fs = require('fs');
const URL = 'https://studiosfeedback.com';
const OUT = '/home/claude/agency-web/audit2';
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
const VIEWPORTS = [
['desktop', 1440, 900],
['mobile', 390, 844],
];
// slow scroll, ~250ms per step, to trigger scroll-driven animations & count-ups
async function slowScroll(page, perStep = 250, frac = 0.55) {
await page.evaluate(async ({ perStep, frac }) => {
const step = Math.max(200, Math.round(window.innerHeight * frac));
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise((r) => setTimeout(r, perStep));
}
window.scrollTo(0, document.body.scrollHeight);
await new Promise((r) => setTimeout(r, 1500));
}, { perStep, frac });
}
(async () => {
const browser = await chromium.launch();
for (const [name, w, h] of VIEWPORTS) {
const ctx = await browser.newContext({
viewport: { width: w, height: h },
userAgent: UA,
deviceScaleFactor: 2,
});
const page = await ctx.newPage();
await page.goto(URL, { waitUntil: 'networkidle', timeout: 60000 }).catch(async () => {
await page.goto(URL, { waitUntil: 'load', timeout: 60000 });
});
for (const sel of [
'text=/^(accept|aceptar|agree|ok|entendido|got it)/i',
"[id*='cookie' i] button",
"[class*='consent' i] button",
]) {
try { await page.locator(sel).first().click({ timeout: 700 }); } catch (e) {}
}
await page.keyboard.press('Escape').catch(() => {});
// two full slow passes so count-ups & per-step reveals reach final state
await slowScroll(page, 250, 0.55);
await slowScroll(page, 200, 0.45);
// fold (top)
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(1000);
await page.screenshot({ path: `${OUT}/${name}-fold.png` });
// full page in final animated state
await page.screenshot({ path: `${OUT}/${name}-full.png`, fullPage: true });
if (name === 'desktop') {
fs.writeFileSync(`${OUT}/page.html`, await page.content(), 'utf-8');
const meta = await page.evaluate(() => {
const out = [];
document.querySelectorAll('section, header, footer').forEach((el, i) => {
const r = el.getBoundingClientRect();
out.push({
i,
tag: el.tagName,
cls: (el.className || '').toString().slice(0, 70),
top: Math.round(r.top + window.scrollY),
h: Math.round(r.height),
txt: (el.innerText || '').replace(/\s+/g, ' ').slice(0, 90),
});
});
return { scrollHeight: document.body.scrollHeight, sections: out };
});
fs.writeFileSync(`${OUT}/sections.json`, JSON.stringify(meta, null, 2), 'utf-8');
console.log('desktop scrollHeight =', meta.scrollHeight, 'sections =', meta.sections.length);
}
console.log('captured', name);
await ctx.close();
}
await browser.close();
console.log('done');
})();

85
crops.js Normal file
View file

@ -0,0 +1,85 @@
const { chromium } = require('playwright');
const URL = 'https://studiosfeedback.com';
const OUT = '/home/claude/agency-web/audit2';
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
async function slowScroll(page, perStep = 250, frac = 0.55) {
await page.evaluate(async ({ perStep, frac }) => {
const step = Math.max(200, Math.round(window.innerHeight * frac));
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise((r) => setTimeout(r, perStep));
}
window.scrollTo(0, document.body.scrollHeight);
await new Promise((r) => setTimeout(r, 1500));
}, { perStep, frac });
}
// crop targets by CSS selector -> filename
const TARGETS = [
{ sel: 'section.proof', name: 'trustedby' },
{ sel: 'section.score', name: 'scoreboard' },
{ sel: 'section.loop', name: 'howitworks' },
{ sel: 'section.quotes', name: 'testimonials' },
];
(async () => {
const browser = await chromium.launch();
// DESKTOP crops
let ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, userAgent: UA, deviceScaleFactor: 2 });
let page = await ctx.newPage();
await page.goto(URL, { waitUntil: 'networkidle', timeout: 60000 }).catch(async () => {
await page.goto(URL, { waitUntil: 'load', timeout: 60000 });
});
await page.keyboard.press('Escape').catch(() => {});
await slowScroll(page, 250, 0.55);
await slowScroll(page, 200, 0.45);
await page.waitForTimeout(800);
for (const t of TARGETS) {
const el = page.locator(t.sel).first();
try {
await el.scrollIntoViewIfNeeded();
await page.waitForTimeout(900);
await el.screenshot({ path: `${OUT}/desktop-${t.name}.png` });
console.log('crop desktop', t.name);
} catch (e) {
console.log('FAIL desktop', t.name, e.message);
}
}
// For the long "How it works" loop, also grab the 4 per-step panels individually if present
const steps = await page.locator('section.loop [class*="step" i], section.loop article, section.loop .loop__panel').count().catch(() => 0);
console.log('loop step-like elements:', steps);
await ctx.close();
// MOBILE crops
ctx = await browser.newContext({ viewport: { width: 390, height: 844 }, userAgent: UA, deviceScaleFactor: 2 });
page = await ctx.newPage();
await page.goto(URL, { waitUntil: 'networkidle', timeout: 60000 }).catch(async () => {
await page.goto(URL, { waitUntil: 'load', timeout: 60000 });
});
await page.keyboard.press('Escape').catch(() => {});
await slowScroll(page, 250, 0.55);
await slowScroll(page, 200, 0.45);
await page.waitForTimeout(800);
for (const t of TARGETS) {
const el = page.locator(t.sel).first();
try {
await el.scrollIntoViewIfNeeded();
await page.waitForTimeout(900);
await el.screenshot({ path: `${OUT}/mobile-${t.name}.png` });
console.log('crop mobile', t.name);
} catch (e) {
console.log('FAIL mobile', t.name, e.message);
}
}
await ctx.close();
await browser.close();
console.log('done');
})();

44
looptiles.js Normal file
View file

@ -0,0 +1,44 @@
const { chromium } = require('playwright');
const OUT = '/home/claude/agency-web/audit2';
const URL = 'https://studiosfeedback.com';
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
async function slowScroll(page, perStep, frac) {
await page.evaluate(async ({ perStep, frac }) => {
const step = Math.max(200, Math.round(window.innerHeight * frac));
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise((r) => setTimeout(r, perStep));
}
window.scrollTo(0, document.body.scrollHeight);
await new Promise((r) => setTimeout(r, 1500));
}, { perStep, frac });
}
(async () => {
const b = await chromium.launch();
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 }, userAgent: UA, deviceScaleFactor: 2 });
const page = await ctx.newPage();
await page.goto(URL, { waitUntil: 'networkidle', timeout: 60000 }).catch(() => {});
await page.keyboard.press('Escape').catch(() => {});
await slowScroll(page, 250, 0.55);
const box = await page.evaluate(() => {
const loop = document.querySelector('section.loop');
const r = loop.getBoundingClientRect();
return { top: Math.round(r.top + window.scrollY), h: Math.round(r.height) };
});
console.log('loop box', JSON.stringify(box));
const vh = 900;
const tiles = Math.ceil(box.h / vh);
for (let i = 0; i < tiles; i++) {
const y = box.top + i * vh;
await page.evaluate((yy) => window.scrollTo(0, yy), y);
await page.waitForTimeout(900);
await page.screenshot({ path: `${OUT}/desktop-howitworks-tile${i + 1}.png` });
console.log('tile', i + 1, 'at y=', y);
}
await b.close();
console.log('done');
})();