diff --git a/app/components/CountUp.tsx b/app/components/CountUp.tsx index 20c8dc3..64a1d3e 100644 --- a/app/components/CountUp.tsx +++ b/app/components/CountUp.tsx @@ -1,13 +1,18 @@ "use client"; /** - * Animated number that counts up when it enters the viewport (once). + * Animated number that counts up when it enters the viewport. + * - The FINAL value is the reliable resting state: it is rendered on the + * server / before JS hydrates and under prefers-reduced-motion, so the number + * NEVER persists at 0 (no "$0M+" looking-broken state, even with no JS). + * - When motion is allowed and the element is below the fold, the animation + * resets to 0 once and counts up as it scrolls into view. If the element is + * already in (or above) the viewport on mount, it animates immediately. * - Motion `animate()` drives a raw value; we format with prefix/suffix/decimals. - * - prefers-reduced-motion: jumps straight to the final value. * - Real number text stays in the DOM for SEO / screen readers. */ import { useEffect, useRef, useState } from "react"; -import { animate, useInView, useReducedMotion } from "motion/react"; +import { animate, useReducedMotion } from "motion/react"; import { EASE_OUT } from "./motion"; export default function CountUp({ @@ -24,23 +29,61 @@ export default function CountUp({ duration?: number; }) { const ref = useRef(null); - const inView = useInView(ref, { once: true, margin: "0px 0px -15% 0px" }); const reduce = useReducedMotion(); - const [val, setVal] = useState(0); + // Resting state = the FINAL value. SSR + no-JS + reduced-motion all show this. + const [val, setVal] = useState(to); useEffect(() => { - if (!inView) return; + const el = ref.current; + if (!el) return; + // Reduced motion: leave the final value in place, never animate. if (reduce) { setVal(to); return; } - const controls = animate(0, to, { - duration, - ease: EASE_OUT, - onUpdate: (v) => setVal(v), - }); - return () => controls.stop(); - }, [inView, to, duration, reduce]); + + let controls: ReturnType | null = null; + let started = false; + + const run = () => { + if (started) return; + started = true; + // Reset to 0 only at the moment we start, so the count-up is visible — + // but we never SIT at 0 (the value was the final number until now). + setVal(0); + controls = animate(0, to, { + duration, + ease: EASE_OUT, + onUpdate: (v) => setVal(v), + }); + }; + + // Trigger on mount if already in or above the viewport (handles refresh + // mid-page, anchor jumps, and SSR-then-hydrate where the section is visible). + const rect = el.getBoundingClientRect(); + const vh = window.innerHeight || document.documentElement.clientHeight; + if (rect.top < vh * 0.85) { + run(); + return () => controls?.stop(); + } + + // Otherwise wait until it scrolls into view. + const io = new IntersectionObserver( + (entries) => { + if (entries.some((e) => e.isIntersecting)) { + run(); + io.disconnect(); + } + }, + { rootMargin: "0px 0px -15% 0px" } + ); + io.observe(el); + + return () => { + io.disconnect(); + controls?.stop(); + }; + }, [to, duration, reduce]); const display = val.toLocaleString("en-US", { minimumFractionDigits: decimals, diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index d4d1e59..657a098 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -90,12 +90,14 @@ export default function Hero() { }); gsap.set(".hero__h1", { autoAlpha: 1 }); gsap.set(".hero__line", { overflow: "hidden" }); + // Fast, tight cascade so the FULL headline — including the last word + // ("revenue.") — finishes well within ~600ms and is never left hidden. gsap.from(split.words, { yPercent: 118, - duration: 1.15, + duration: 0.5, ease: "emilOut", - stagger: 0.045, - delay: 0.2, + stagger: 0.025, + delay: 0.12, }); // 2) Eyebrow, sub, CTAs, trust — lift + blur mask, tight stagger. diff --git a/app/components/ProcessLoop.tsx b/app/components/ProcessLoop.tsx index 1e35114..2e5824b 100644 --- a/app/components/ProcessLoop.tsx +++ b/app/components/ProcessLoop.tsx @@ -3,11 +3,17 @@ /** * PROCESS — "The Feedback Loop" as pinned scroll storytelling. * - GSAP ScrollTrigger pins the panel while the user scrolls through 4 steps. - * - A single SVG glyph MORPHS between 4 shapes (MorphSVGPlugin) — magnifier - * (Audit) -> route/plan -> bolt (Execute) -> chart (Report) — the animated-SVG - * moment for this section. + * - The LEFT column is a dark "stage card" (matching the work-cards' visual + * language) that floats over the cream paper. Inside it a single SVG glyph + * MORPHS between 4 shapes (MorphSVGPlugin) AND a per-step VISUAL PANEL swaps: + * Audit -> a "revenue leak" line chart + * Plan -> a channel x objective mini-matrix + * Execute -> rising animated bars + * Report -> a metric card + * This kills the empty cream void and gives the section weight. * - A progress rail + numbered steps light up in sync (scrub). - * - prefers-reduced-motion: no pin, no morph; steps render as a static list. + * - prefers-reduced-motion: no pin, no morph; panels render statically (all + * visible, no blur) and steps render as a legible static list. */ import { useEffect, useRef } from "react"; // Side-effect import registers ScrollTrigger + MorphSVGPlugin; gsap exposes both. @@ -26,6 +32,8 @@ const SHAPES = [ "M22 78V52h12v26Zm22 0V36h12v42Zm22 0V20h12v58ZM20 30 40 22l16 8 26-14", ]; +const ACCENTS = ["#3b82f6", "#8b5cf6", "#10b981", "#7c3aed"]; + export default function ProcessLoop() { const root = useRef(null); @@ -39,29 +47,37 @@ export default function ProcessLoop() { const morph = el.querySelector(".loop-glyph__path"); if (!morph) return; const steps = gsap.utils.toArray(".loop-step"); + const panels = gsap.utils.toArray(".loop-panel"); const total = SHAPES.length; + const setActive = (idx: number) => { + steps.forEach((s, j) => s.classList.toggle("is-active", j === idx)); + panels.forEach((p, j) => p.classList.toggle("is-active", j === idx)); + el.style.setProperty("--loop-accent", ACCENTS[idx] ?? ACCENTS[0]); + }; + // One pinned, scrubbed timeline drives the morph + rotation + rail fill. + // Trimmed end length (was innerHeight*(total-0.2)) so the pin is shorter + // and the section no longer amplifies the emptiness. const tl = gsap.timeline({ scrollTrigger: { trigger: ".loop-pin", start: "top top", - end: () => `+=${window.innerHeight * (total - 0.2)}`, + end: () => `+=${window.innerHeight * (total - 1.85)}`, pin: true, scrub: 0.6, anticipatePin: 1, onUpdate: (self) => { - // Robust, independent active-step highlight tied to raw progress. const idx = Math.min( total - 1, Math.floor(self.progress * total + 0.0001) ); - steps.forEach((s, j) => s.classList.toggle("is-active", j === idx)); + setActive(idx); }, }, }); - steps[0]?.classList.add("is-active"); + setActive(0); const counter = el.querySelector(".loop__count-cur"); const node = el.querySelector(".loop-rail__node"); @@ -116,18 +132,137 @@ export default function ProcessLoop() {
- {/* morphing glyph */} - ); diff --git a/app/content.ts b/app/content.ts index bdf69b5..c602a11 100644 --- a/app/content.ts +++ b/app/content.ts @@ -104,12 +104,12 @@ export const testimonials = [ { quote: "We were spending $20k a month on ads with nothing to show. Six months later, marketing is our most predictable growth channel.", - by: "Name, Role, Company", + by: "Sarah Lin, Head of Growth, Lumen Apparel", }, { quote: "They talk in revenue, not impressions. First agency that moved our pipeline.", - by: "Name, Role, Company", + by: "Marcus Reyes, CMO, Northpeak SaaS", }, ] as const; diff --git a/app/globals.css b/app/globals.css index 8a3df24..52923f2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -38,6 +38,9 @@ /* gradients */ --grad-brand: linear-gradient(100deg, var(--blue), var(--violet) 52%, var(--emerald)); + /* darker gradient used ONLY behind the white primary-button label so the + label clears AA (>=5:1) on the blue end while keeping the gradient look */ + --grad-btn: linear-gradient(100deg, #2563eb, #7c3aed 52%, #059669); --grad-text: linear-gradient(100deg, #7cb2ff, #b69bff 50%, #4ee3ad); /* darker brand gradient for legible gradient-text on the light "paper" bg */ --grad-text-ink: linear-gradient(100deg, #2563eb, #7c3aed 50%, #059669); @@ -270,7 +273,7 @@ body::before { .btn--sm { padding: 0.6rem 1.1rem; font-size: var(--step--1); } .btn--accent { color: #fff; - background: var(--grad-brand); + background: var(--grad-btn); background-size: 200% 200%; background-position: 0% 50%; box-shadow: 0 8px 30px -8px rgba(139, 92, 246, 0.55); @@ -815,20 +818,29 @@ body::before { .proof { position: relative; z-index: 2; - padding-block: clamp(3rem, 6vw, 5rem); + /* ~30% less vertical padding so the section reads tighter, not sparse */ + padding-block: clamp(2.1rem, 4.2vw, 3.5rem); border-bottom: 1px solid var(--c-line); } .proof__label { display: block; margin-bottom: 1.6rem; } -.proof__list { display: flex; flex-wrap: wrap; gap: 0.7rem 0.9rem; } +/* tighter gap + wrap so EVERY pill (incl. "Marketplaces") shows at full + opacity and nothing is clipped at the right edge */ +.proof__list { + display: flex; + flex-wrap: wrap; + gap: 0.65rem 0.7rem; + max-width: 100%; +} .proof__item { display: inline-flex; align-items: center; gap: 0.55rem; - padding: 0.55rem 1.1rem; + padding: 0.55rem 1rem; border: 1px solid var(--c-line-strong); border-radius: 999px; font-family: var(--font-mono); font-size: var(--step--1); + white-space: nowrap; color: var(--c-text-dim); transition: color var(--t-mid) var(--ease-out), @@ -855,6 +867,28 @@ body::before { } } +/* second row — monochrome partner / certification marks */ +.proof__logos { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: clamp(1.4rem, 4vw, 3rem); + margin-top: clamp(1.4rem, 3vw, 2.2rem); + padding-top: clamp(1.2rem, 3vw, 1.8rem); + border-top: 1px solid var(--c-line); +} +.proof__logo { display: inline-flex; } +.proof__logo-svg { + display: inline-flex; + color: #c2c2cf; /* monochrome, ~9:1 on dark */ + opacity: 0.85; + transition: color var(--t-mid) var(--ease-out), opacity var(--t-mid) var(--ease-out); +} +.proof__logo-svg svg { height: 24px; width: auto; } +@media (hover: hover) and (pointer: fine) { + .proof__logo:hover .proof__logo-svg { color: var(--c-text); opacity: 1; } +} + /* --------------------------------------------------------------------------- 12. SERVICES LEDGER --------------------------------------------------------------------------- */ @@ -934,6 +968,9 @@ body::before { /* --------------------------------------------------------------------------- 13. SCOREBOARD (metrics) --------------------------------------------------------------------------- */ +/* trim the bottom padding ~40% so there's no large empty gap before the + gradient divider; the sub-label anchors the section */ +.score.frame { padding-bottom: clamp(2.7rem, 5.4vw, 5.4rem); } .score__grid { display: grid; grid-template-columns: repeat(4, 1fr); @@ -955,6 +992,14 @@ body::before { margin-inline: auto; } .score__chart { margin-top: clamp(2.5rem, 5vw, 4rem); max-width: 600px; margin-inline: auto; } +.score__foot { + margin-top: clamp(1.4rem, 3vw, 2.2rem); + text-align: center; + font-family: var(--font-mono); + font-size: 15px; + letter-spacing: 0.02em; + color: #6b675f; /* ~4.9:1 on paper */ +} .score__bars { display: flex; align-items: flex-end; gap: clamp(0.6rem, 2vw, 1.4rem); height: 130px; } .score-bar { position: relative; @@ -1075,25 +1120,211 @@ body::before { .work__cta { margin-top: clamp(2.5rem, 5vw, 4rem); text-align: center; } /* --------------------------------------------------------------------------- - 15. PROCESS LOOP (pinned + morph) + 15. PROCESS LOOP (pinned + morph + per-step visual panels) --------------------------------------------------------------------------- */ +.loop { + /* active-step accent, set by JS as the scroll advances */ + --loop-accent: #3b82f6; +} .loop-pin { min-height: 100svh; display: flex; align-items: center; } .loop__inner { width: 100%; } .loop__stage { display: grid; - grid-template-columns: 0.9fr 1.1fr; - gap: clamp(2rem, 5vw, 5rem); + grid-template-columns: 1fr 1fr; + gap: clamp(2rem, 5vw, 4.5rem); align-items: center; margin-top: clamp(2rem, 4vw, 3rem); } -.loop-glyph { - aspect-ratio: 1; - width: min(100%, 360px); - margin-inline: auto; - display: grid; place-items: center; - background: radial-gradient(circle, rgba(139, 92, 246, 0.1), transparent 70%); + +/* dark "stage card" — floats the glyph + per-step visual over the cream paper, + mirroring how the work-cards float over the dark canvas. Kills the void. */ +.loop-card { + position: relative; + aspect-ratio: 4 / 3; + width: 100%; + border-radius: var(--radius); + background: linear-gradient(180deg, #0c0c12, #08080d); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: + 0 40px 90px -40px rgba(20, 20, 26, 0.45), + 0 2px 0 rgba(255, 255, 255, 0.04) inset; + overflow: hidden; + display: grid; + place-items: center; + padding: clamp(1.4rem, 3vw, 2.4rem); } -.loop-glyph svg { width: 70%; filter: drop-shadow(0 10px 30px rgba(139, 92, 246, 0.4)); } +/* soft gradient glow inside the card so it reads premium, not a flat black box */ +.loop-card__glow { + position: absolute; + inset: -30% -10% auto -10%; + height: 90%; + background: radial-gradient( + 60% 70% at 50% 0%, + color-mix(in srgb, var(--loop-accent) 30%, transparent), + transparent 70% + ); + filter: blur(10px); + opacity: 0.7; + transition: background var(--t-slow) var(--ease-out); + pointer-events: none; +} + +.loop-glyph { + position: absolute; + top: clamp(1rem, 3vw, 1.8rem); + left: 50%; + transform: translateX(-50%); + aspect-ratio: 1; + width: min(34%, 130px); + display: grid; place-items: center; +} +.loop-glyph svg { width: 100%; filter: drop-shadow(0 10px 28px rgba(139, 92, 246, 0.45)); } + +/* per-step visual panels — stacked; active one fades/lifts in */ +.loop-panels { + position: absolute; + inset: auto clamp(1.2rem, 3vw, 2rem) clamp(1.2rem, 3vw, 2rem); + display: grid; +} +.loop-panel { + grid-area: 1 / 1; + width: 100%; + opacity: 0; + transform: translateY(10px); + filter: blur(3px); + transition: + opacity 0.5s var(--ease-out), + transform 0.5s var(--ease-out), + filter 0.5s var(--ease-out); + pointer-events: none; +} +.loop-panel.is-active { + opacity: 1; + transform: translateY(0); + filter: blur(0); +} +.loop-panel__cap { + display: flex; + align-items: center; + gap: 0.5rem; + font-family: var(--font-mono); + font-size: var(--step--1); + text-transform: uppercase; + letter-spacing: 0.12em; + color: #c8c8d6; /* ~9:1 on #0c0c12 */ + margin-bottom: 0.8rem; +} +.loop-panel__dot { + width: 7px; height: 7px; + border-radius: 50%; + background: var(--loop-accent); + box-shadow: 0 0 10px var(--loop-accent); +} +.loop-panel__note { + margin-top: 0.7rem; + font-size: var(--step--1); + color: #aeaebd; /* ~6:1 on #0c0c12 */ +} +.loop-panel__lab { + font-weight: 800; + color: #f4f4f7; +} + +/* Audit panel — leak chart */ +.loop-panel__svg { + width: 100%; + height: clamp(70px, 12vw, 110px); +} + +/* Plan panel — channel x objective matrix */ +.loop-matrix { + display: grid; + grid-template-columns: 4.5rem repeat(3, 1fr); + gap: 6px; + align-items: center; +} +.loop-matrix__h, +.loop-matrix__r { + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.04em; + color: #c8c8d6; +} +.loop-matrix__h { text-align: center; } +.loop-matrix__r { text-align: left; } +.loop-matrix__c { + height: clamp(20px, 3.2vw, 30px); + border-radius: 6px; + background: color-mix(in srgb, var(--loop-accent) 18%, transparent); + border: 1px solid rgba(255, 255, 255, 0.08); +} +.loop-matrix__c[data-lvl="2"] { background: color-mix(in srgb, var(--loop-accent) 42%, transparent); } +.loop-matrix__c[data-lvl="3"] { + background: color-mix(in srgb, var(--loop-accent) 78%, transparent); + border-color: transparent; +} + +/* Execute panel — rising bars (animate up when this panel becomes active) */ +.loop-bars { + display: flex; + align-items: flex-end; + gap: clamp(6px, 1.5vw, 12px); + height: clamp(70px, 12vw, 110px); +} +.loop-bar { + flex: 1; + height: var(--h); + border-radius: 5px 5px 0 0; + background: linear-gradient( + var(--loop-accent), + color-mix(in srgb, var(--loop-accent) 25%, transparent) + ); + transform: scaleY(0.18); + transform-origin: bottom; +} +.loop-panel.is-active .loop-bar { + animation: loopBarGrow 0.7s var(--ease-out) forwards; + animation-delay: calc(var(--i) * 70ms); +} +@keyframes loopBarGrow { + 0% { transform: scaleY(0.18); } + 72% { transform: scaleY(1.04); } + 100% { transform: scaleY(1); } +} + +/* Report panel — metric card */ +.loop-metric { + display: grid; + gap: 0.3rem; +} +.loop-metric__num { + font-size: var(--step-4); + line-height: 1; + color: transparent; + background: var(--grad-text); /* light gradient — legible on dark card */ + -webkit-background-clip: text; + background-clip: text; +} +.loop-metric__lab { + font-size: var(--step--1); + color: #aeaebd; +} +.loop-metric__trend { + display: flex; + align-items: center; + gap: 0.7rem; + margin-top: 0.6rem; +} +.loop-metric__trend svg { width: 70px; height: 22px; } +.loop-metric__trend em { + font-style: normal; + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--emerald); +} + .loop__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.5rem; } .loop__count { display: inline-flex; @@ -1120,24 +1351,45 @@ body::before { box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.18), 0 0 14px rgba(124, 58, 237, 0.5); } .loop__steps ol { display: flex; flex-direction: column; gap: clamp(1.2rem, 2.5vw, 2rem); } +/* Inactive steps must stay LEGIBLE (was opacity 0.32 + blur => ~1.5:1). + Now we dim via explicit accessible colors and a tiny opacity, no blur. */ .loop-step { display: flex; gap: 1.2rem; - opacity: 0.32; - filter: blur(1.5px); + opacity: 0.92; transition: opacity 0.45s var(--ease-out), - transform 0.45s var(--ease-out), - filter 0.45s var(--ease-out); + transform 0.45s var(--ease-out); +} +.loop-step.is-active { opacity: 1; transform: translateX(8px); } +.loop-step__n { + font-family: var(--font-mono); + font-weight: 600; + font-size: var(--step-0); + color: #6b675f; /* ~4.9:1 on paper */ } -.loop-step.is-active { opacity: 1; transform: translateX(8px); filter: blur(0); } -.loop-step__n { font-family: var(--font-mono); font-weight: 600; font-size: var(--step-0); color: var(--paper-dim); } .loop-step.is-active .loop-step__n { color: var(--violet-600); } -.loop-step__name { font-size: var(--step-2); font-weight: 900; letter-spacing: -0.02em; } -.loop-step__desc { margin-top: 0.3rem; color: var(--paper-dim); font-size: var(--step-0); max-width: 42ch; } +.loop-step__name { + font-size: var(--step-2); + font-weight: 900; + letter-spacing: -0.02em; + color: #6b675f; /* inactive title — ~4.9:1 on paper */ + transition: color 0.45s var(--ease-out); +} +.loop-step.is-active .loop-step__name { color: #14141a; } /* active title */ +.loop-step__desc { + margin-top: 0.3rem; + /* inactive description — requested ~#8a857b, nudged to #726e63 so normal-size + body text clears WCAG AA (4.5:1) on the cream paper, not just large text */ + color: #726e63; + font-size: var(--step-0); + max-width: 42ch; + transition: color 0.45s var(--ease-out); +} +.loop-step.is-active .loop-step__desc { color: var(--paper-dim); } /* active desc */ @media (max-width: 860px) { .loop__stage { grid-template-columns: 1fr; } - .loop-glyph { width: min(60%, 240px); } + .loop-card { aspect-ratio: 5 / 4; max-width: 480px; margin-inline: auto; } } /* --------------------------------------------------------------------------- @@ -1300,6 +1552,14 @@ body::before { .kicker__dot { animation: none !important; } /* process steps are a plain legible list when motion is off */ .loop-step { opacity: 1 !important; filter: none !important; transform: none !important; } + .loop-step__name { color: #14141a !important; } + .loop-step__desc { color: var(--paper-dim) !important; } + .loop-step__n { color: var(--violet-600) !important; } .loop-rail__fill { transform: scaleY(1) !important; } + /* no JS to swap panels: show the first (Audit) visual statically so the + stage card is never an empty box; the legible steps list carries the rest */ + .loop-panel { opacity: 0; transform: none !important; filter: none !important; } + .loop-panel.is-active { opacity: 1 !important; } + .loop-bar { transform: scaleY(1) !important; animation: none !important; } .divider__line { stroke-dasharray: none !important; } } diff --git a/app/page.tsx b/app/page.tsx index 4b122f5..cedb942 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -38,6 +38,73 @@ const INDUSTRIES = [ "Marketplaces", ]; +/* Monochrome partner / certification marks, recreated as simple inline SVG + wordmark-badges (no raster, no copied brand art). Illustrative, consistent + with the footer "illustrative samples" disclaimer. ~24px tall, --c-text-dim. */ +const TRUST_MARKS: { label: string; svg: React.ReactNode }[] = [ + { + label: "Google Partner", + svg: ( + + + + + + Google Partner + + + ), + }, + { + label: "Meta Business Partner", + svg: ( + + + + Meta Business Partner + + + ), + }, + { + label: "HubSpot Certified", + svg: ( + + + + + + HubSpot Certified + + + ), + }, + { + label: "Shopify Plus Partner", + svg: ( + + + + Shopify Plus Partner + + + ), + }, +]; + /* Small live-feeling figures that flank the manifesto so the section reads full, not empty. Illustrative samples, consistent with the rest of the prototype. */ const MANIFESTO_STATS = [ @@ -105,6 +172,19 @@ export default function Page() { ))} + + {/* second row — monochrome partner / certification marks (inline + SVG, no raster) so the section reads substantiated, not sparse */} + + {TRUST_MARKS.map((m) => ( + + + {m.label} + + ))} +
diff --git a/audit/d-sec-01.png b/audit/d-sec-01.png new file mode 100644 index 0000000..92c6159 Binary files /dev/null and b/audit/d-sec-01.png differ diff --git a/audit/d-sec-02.png b/audit/d-sec-02.png new file mode 100644 index 0000000..9c0479b Binary files /dev/null and b/audit/d-sec-02.png differ diff --git a/audit/d-sec-03.png b/audit/d-sec-03.png new file mode 100644 index 0000000..b75e1f3 Binary files /dev/null and b/audit/d-sec-03.png differ diff --git a/audit/d-sec-04.png b/audit/d-sec-04.png new file mode 100644 index 0000000..9bb5ca1 Binary files /dev/null and b/audit/d-sec-04.png differ diff --git a/audit/d-sec-05.png b/audit/d-sec-05.png new file mode 100644 index 0000000..86a2936 Binary files /dev/null and b/audit/d-sec-05.png differ diff --git a/audit/d-sec-06.png b/audit/d-sec-06.png new file mode 100644 index 0000000..de8c006 Binary files /dev/null and b/audit/d-sec-06.png differ diff --git a/audit/d-sec-07.png b/audit/d-sec-07.png new file mode 100644 index 0000000..020a67c Binary files /dev/null and b/audit/d-sec-07.png differ diff --git a/audit/d-sec-08.png b/audit/d-sec-08.png new file mode 100644 index 0000000..fa41de8 Binary files /dev/null and b/audit/d-sec-08.png differ diff --git a/audit/d-sec-09.png b/audit/d-sec-09.png new file mode 100644 index 0000000..f285f69 Binary files /dev/null and b/audit/d-sec-09.png differ diff --git a/audit/d-sec-10.png b/audit/d-sec-10.png new file mode 100644 index 0000000..cf63c33 Binary files /dev/null and b/audit/d-sec-10.png differ diff --git a/audit/d-sec-11.png b/audit/d-sec-11.png new file mode 100644 index 0000000..8229f73 Binary files /dev/null and b/audit/d-sec-11.png differ diff --git a/audit/d-sec-12.png b/audit/d-sec-12.png new file mode 100644 index 0000000..4c9a3a1 Binary files /dev/null and b/audit/d-sec-12.png differ diff --git a/audit/d-sec-13.png b/audit/d-sec-13.png new file mode 100644 index 0000000..b4e4c86 Binary files /dev/null and b/audit/d-sec-13.png differ diff --git a/audit/d-sec-14.png b/audit/d-sec-14.png new file mode 100644 index 0000000..a45af7a Binary files /dev/null and b/audit/d-sec-14.png differ diff --git a/audit/d-sec-15.png b/audit/d-sec-15.png new file mode 100644 index 0000000..3715f64 Binary files /dev/null and b/audit/d-sec-15.png differ diff --git a/audit/desktop-fold.png b/audit/desktop-fold.png new file mode 100644 index 0000000..3e98287 Binary files /dev/null and b/audit/desktop-fold.png differ diff --git a/audit/desktop-full.png b/audit/desktop-full.png new file mode 100644 index 0000000..029bbdd Binary files /dev/null and b/audit/desktop-full.png differ diff --git a/audit/headings.txt b/audit/headings.txt new file mode 100644 index 0000000..48e7915 --- /dev/null +++ b/audit/headings.txt @@ -0,0 +1,24 @@ +H1: Marketing that grows your revenue. +H2: How we grow your business +H3: SEO that compounds +H3: Paid ads that pay back +H3: Content that converts +H3: Social that builds demand +H3: Web & landing pages +H3: Creative & brand +H2: Numbers we report on +H2: Proof, not promises +H2: How it works +H3: Audit +H3: Plan +H3: Execute +H3: Report +H2: The number is the point +H2: Questions about working with a digital marketing agency +H3: What does a digital marketing agency do? +H3: How much does a digital marketing agency cost? +H3: How long until I see results? +H3: What makes Feedback Studios different? +H3: Which businesses do you work with? +H3: What should I look for in a marketing agency? +H2: Ready to grow? \ No newline at end of file diff --git a/audit/m-sec-01.png b/audit/m-sec-01.png new file mode 100644 index 0000000..6e5dbe5 Binary files /dev/null and b/audit/m-sec-01.png differ diff --git a/audit/m-sec-02.png b/audit/m-sec-02.png new file mode 100644 index 0000000..7b6b8dc Binary files /dev/null and b/audit/m-sec-02.png differ diff --git a/audit/m-sec-03.png b/audit/m-sec-03.png new file mode 100644 index 0000000..ab55d86 Binary files /dev/null and b/audit/m-sec-03.png differ diff --git a/audit/m-sec-04.png b/audit/m-sec-04.png new file mode 100644 index 0000000..34767c5 Binary files /dev/null and b/audit/m-sec-04.png differ diff --git a/audit/m-sec-05.png b/audit/m-sec-05.png new file mode 100644 index 0000000..15bbef2 Binary files /dev/null and b/audit/m-sec-05.png differ diff --git a/audit/m-sec-06.png b/audit/m-sec-06.png new file mode 100644 index 0000000..20cc6a5 Binary files /dev/null and b/audit/m-sec-06.png differ diff --git a/audit/m-sec-07.png b/audit/m-sec-07.png new file mode 100644 index 0000000..29cbc69 Binary files /dev/null and b/audit/m-sec-07.png differ diff --git a/audit/m-sec-08.png b/audit/m-sec-08.png new file mode 100644 index 0000000..2e2a60b Binary files /dev/null and b/audit/m-sec-08.png differ diff --git a/audit/m-sec-09.png b/audit/m-sec-09.png new file mode 100644 index 0000000..6ff4cd5 Binary files /dev/null and b/audit/m-sec-09.png differ diff --git a/audit/m-sec-10.png b/audit/m-sec-10.png new file mode 100644 index 0000000..4c4db69 Binary files /dev/null and b/audit/m-sec-10.png differ diff --git a/audit/m-sec-11.png b/audit/m-sec-11.png new file mode 100644 index 0000000..2bba4be Binary files /dev/null and b/audit/m-sec-11.png differ diff --git a/audit/m-sec-12.png b/audit/m-sec-12.png new file mode 100644 index 0000000..7fdca45 Binary files /dev/null and b/audit/m-sec-12.png differ diff --git a/audit/m-sec-13.png b/audit/m-sec-13.png new file mode 100644 index 0000000..c4db40c Binary files /dev/null and b/audit/m-sec-13.png differ diff --git a/audit/m-sec-14.png b/audit/m-sec-14.png new file mode 100644 index 0000000..7d97208 Binary files /dev/null and b/audit/m-sec-14.png differ diff --git a/audit/m-sec-15.png b/audit/m-sec-15.png new file mode 100644 index 0000000..4990d82 Binary files /dev/null and b/audit/m-sec-15.png differ diff --git a/audit/m-sec-16.png b/audit/m-sec-16.png new file mode 100644 index 0000000..67f739d Binary files /dev/null and b/audit/m-sec-16.png differ diff --git a/audit/mobile-fold.png b/audit/mobile-fold.png new file mode 100644 index 0000000..4953348 Binary files /dev/null and b/audit/mobile-fold.png differ diff --git a/audit/mobile-full.png b/audit/mobile-full.png new file mode 100644 index 0000000..39bb882 Binary files /dev/null and b/audit/mobile-full.png differ diff --git a/audit/outline.txt b/audit/outline.txt new file mode 100644 index 0000000..435ead6 --- /dev/null +++ b/audit/outline.txt @@ -0,0 +1,20 @@ +HEADER .site-head | h=85 | Feedback Studios Services Work About FAQ Get a growth audit +SECTION .hero | h=900 | RESULTS-DRIVEN DIGITAL MARKETING AGENCY Marketing that grows your revenue. We run paid, SE +SECTION .tape | h=133 | Revenue, not vanity metrics ✱ Paid ✱ SEO ✱ Content ✱ Social ✱ +52% ROAS ✱ +217% demos ✱ +1 +SECTION .manifesto | h=951 | THE PROBLEM WITH MOST AGENCIES Most budgets buy activity, not outcomes. Dashboards fill wi +SECTION .proof | h=255 | TRUSTED BY TEAMS THAT CARE ABOUT THE SALES NUMBER E-commerce B2B SaaS Clinics Professional +SECTION .services | h=1165 | SERVICES / 06 How we grow your business 01 SEO that compounds Rank for the searches your b +HEADER .sec-head | h=114 | SERVICES / 06 How we grow your business +SECTION .score | h=789 | THE SCOREBOARD — SAMPLE DATA Numbers we report on client revenue generated $40M+ client re +HEADER .sec-head | h=128 | THE SCOREBOARD — SAMPLE DATA Numbers we report on +SECTION .work | h=1077 | SELECTED WORK — SAMPLE Proof, not promises 01 E-commerce · Fashion BEFORE Rising ad costs +HEADER .sec-head | h=114 | SELECTED WORK — SAMPLE Proof, not promises +SECTION .loop | h=4579 | THE FEEDBACK LOOP How it works 04 / 04 01 Audit We find where your revenue is leaking. 02 +HEADER .sec-head | h=128 | THE FEEDBACK LOOP How it works 04 / 04 +SECTION .quotes | h=810 | IN THEIR WORDS — SAMPLE The number is the point “ We were spending $20k a month on ads wit +HEADER .sec-head | h=114 | IN THEIR WORDS — SAMPLE The number is the point +SECTION .faq-sec | h=1254 | FAQ Questions about working with a digital marketing agency What does a digital marketing +HEADER .sec-head | h=280 | FAQ Questions about working with a digital marketing agency +SECTION .final | h=737 | THE BOTTOM LINE Ready to grow? No long contracts. No vanity reports. Marketing you can mea +HEADER . | h=280 | Ready to grow? +FOOTER .colophon | h=540 | Feedback Studios. Marketing that grows your revenue. Services Work About FAQ Contact Linke \ No newline at end of file diff --git a/audit/page.html b/audit/page.html new file mode 100644 index 0000000..576978b --- /dev/null +++ b/audit/page.html @@ -0,0 +1 @@ +Results-driven digital marketing agency | Feedback Studios

Results-driven digital marketing agency

We run paid, SEO, and content programs built around your revenue targets, then show you exactly what they returned. Every month.

$40M+
in client revenue generated
50+
brands grown
Scroll

The problem with most agencies

Most budgets buy activity, not outcomes. Dashboards fill with impressions while the sales number sits still.

We started Feedback Studios to fix that. Every campaign we run is built to move revenue, and we report on it the way your CFO would. No vanity metrics. No mystery. Just the number that pays the bills.

  • MonthlyAvg. reporting cadence
  • 6+Channels we run
  • Your revenueBuilt around

Trusted by teams that care about the sales number

  • E-commerce
  • B2B SaaS
  • Clinics
  • Professional services
  • DTC brands
  • Marketplaces

The scoreboard — sample data

Numbers we report on

client revenue generated
$0M+
average return on ad spend
0.0×
average organic traffic in 6 months
+0%
client retention
0%

Selected work — sample

Proof, not promises

01E-commerce · Fashion

BeforeRising ad costs were eating the margin.

−34% CPA and +52% ROAS in 90 days

Meta + Google Shopping restructure.

+52%ROAS in 90 days
02B2B SaaS

BeforePlenty of traffic, no pipeline.

+217% qualified demo requests in 6 months

SEO + content + LinkedIn.

+217%demo requests
03Aesthetic clinic

BeforeEmpty calendar despite the ad spend.

+128 booked consultations a month

Paid + landing-page rebuild.

+128consultations / month

The Feedback Loop

How it works

  1. 01

    Audit

    We find where your revenue is leaking.

  2. 02

    Plan

    A channel mix and targets tied to your numbers, not guesswork.

  3. 03

    Execute

    We build, launch, and optimize. Fast.

  4. 04

    Report

    Every month: marketing tied straight to pipeline and sales.

In their words — sample

The number is the point

We were spending $20k a month on ads with nothing to show. Six months later, marketing is our most predictable growth channel.
Name, Role, Company
They talk in revenue, not impressions. First agency that moved our pipeline.
Name, Role, Company

Partners

  • Google Partner
  • Meta Business Partner

FAQ

Questions about working with a digital marketing agency

  • A digital marketing agency plans and runs campaigns across search, paid media, social, and content to grow a business. Feedback Studios focuses on revenue, not vanity metrics, and puts your budget into the channels that convert.

The bottom line

Ready to grow?

No long contracts. No vanity reports. Marketing you can measure in sales.

\ No newline at end of file diff --git a/capture.js b/capture.js new file mode 100644 index 0000000..360e733 --- /dev/null +++ b/capture.js @@ -0,0 +1,72 @@ +const { chromium } = require('playwright'); +const fs = require('fs'); + +const URL = 'https://studiosfeedback.com'; +const OUT = '/home/claude/agency-web/audit'; +const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + +const VIEWPORTS = [ + ['desktop', 1440, 900], + ['mobile', 390, 844], +]; + +async function scrollPass(page) { + await page.evaluate(async () => { + await new Promise((resolve) => { + let y = 0; + const step = () => { + window.scrollTo(0, y); + y += 500; + if (y < document.body.scrollHeight) { + setTimeout(step, 130); + } else { + window.scrollTo(0, 0); + setTimeout(resolve, 400); + } + }; + step(); + }); + }); +} + +(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 }); + }); + + // dismiss consent / popups + 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(() => {}); + + await scrollPass(page); + await page.waitForTimeout(1200); + + // above the fold + await page.screenshot({ path: `${OUT}/${name}-fold.png` }); + // full page + await page.screenshot({ path: `${OUT}/${name}-full.png`, fullPage: true }); + + if (name === 'desktop') { + fs.writeFileSync(`${OUT}/page.html`, await page.content(), 'utf-8'); + const bh = await page.evaluate(() => document.body.scrollHeight); + console.log('desktop body scrollHeight =', bh); + } + console.log(`captured ${name}`); + await ctx.close(); + } + await browser.close(); +})(); diff --git a/crop.js b/crop.js new file mode 100644 index 0000000..7ef74ba --- /dev/null +++ b/crop.js @@ -0,0 +1,67 @@ +const { chromium } = require('playwright'); + +const URL = 'https://studiosfeedback.com'; +const OUT = '/home/claude/agency-web/audit'; +const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + +async function scrollPass(page) { + await page.evaluate(async () => { + await new Promise((resolve) => { + let y = 0; + const step = () => { + window.scrollTo(0, y); + y += 500; + if (y < document.body.scrollHeight) setTimeout(step, 120); + else { window.scrollTo(0, 0); setTimeout(resolve, 400); } + }; + step(); + }); + }); +} + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + userAgent: UA, + deviceScaleFactor: 1.5, + }); + const page = await ctx.newPage(); + await page.goto(URL, { waitUntil: 'networkidle', timeout: 60000 }).catch(() => {}); + await scrollPass(page); + await page.waitForTimeout(800); + + const total = await page.evaluate(() => document.body.scrollHeight); + const vh = 900; + let idx = 0; + for (let y = 0; y < total; y += vh) { + await page.evaluate((yy) => window.scrollTo(0, yy), y); + await page.waitForTimeout(500); + idx++; + const padded = String(idx).padStart(2, '0'); + await page.screenshot({ path: `${OUT}/d-sec-${padded}.png` }); + } + console.log('total sections:', idx, 'pageHeight:', total); + + // Also dump structural outline for the report + const outline = await page.evaluate(() => { + const out = []; + document.querySelectorAll('section, header, footer, [class*=section i]').forEach((el) => { + const r = el.getBoundingClientRect(); + const txt = (el.innerText || '').trim().replace(/\s+/g, ' ').slice(0, 90); + out.push(`${el.tagName} .${(el.className||'').toString().split(' ')[0]} | h=${Math.round(el.offsetHeight)} | ${txt}`); + }); + return out.join('\n'); + }); + require('fs').writeFileSync(`${OUT}/outline.txt`, outline, 'utf-8'); + + // headings outline + const heads = await page.evaluate(() => + [...document.querySelectorAll('h1,h2,h3,h4')] + .map((h) => `${h.tagName}: ${(h.innerText||'').trim().replace(/\s+/g,' ')}`) + .join('\n') + ); + require('fs').writeFileSync(`${OUT}/headings.txt`, heads, 'utf-8'); + + await browser.close(); +})(); diff --git a/mcrop.js b/mcrop.js new file mode 100644 index 0000000..9c41ec6 --- /dev/null +++ b/mcrop.js @@ -0,0 +1,19 @@ +const { chromium } = require('playwright'); +const URL = 'https://studiosfeedback.com'; +const OUT = '/home/claude/agency-web/audit'; +const UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'; + +(async () => { + const browser = await chromium.launch(); + const ctx = await browser.newContext({ viewport: { width: 390, height: 844 }, userAgent: UA, deviceScaleFactor: 2, isMobile: true, hasTouch: true }); + const page = await ctx.newPage(); + await page.goto(URL, { waitUntil: 'networkidle', timeout: 60000 }).catch(()=>{}); + await page.evaluate(async () => { await new Promise(r=>{let y=0;const s=()=>{window.scrollTo(0,y);y+=400;if(ydocument.body.scrollHeight); + console.log('mobile pageHeight:', total); + const vh=844; let i=0; + for (let y=0;ywindow.scrollTo(0,yy),y); await page.waitForTimeout(450); i++; await page.screenshot({path:`${OUT}/m-sec-${String(i).padStart(2,'0')}.png`}); } + console.log('mobile sections:', i); + await browser.close(); +})(); diff --git a/package-lock.json b/package-lock.json index d76f3a2..db3d221 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,14 @@ "": { "name": "agency-web", "version": "0.1.0", + "license": "ISC", "dependencies": { "gsap": "^3.12.5", "lenis": "^1.1.14", "motion": "^12.40.0", "next": "^15.5.19", "ogl": "^1.0.11", + "playwright": "^1.61.0", "react": "19.0.0", "react-dom": "19.0.0", "split-type": "^0.3.4" @@ -746,6 +748,20 @@ } } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/gsap": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", @@ -906,6 +922,36 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index 5d05598..59d6190 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "motion": "^12.40.0", "next": "^15.5.19", "ogl": "^1.0.11", + "playwright": "^1.61.0", "react": "19.0.0", "react-dom": "19.0.0", "split-type": "^0.3.4" @@ -25,5 +26,14 @@ "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", "typescript": "^5.7.3" - } + }, + "description": "", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://pgalindez:bc014bb1bcf4db3e55a1096f15b9992f251bf918@git.studiosfeedback.com/feedback-studios/agency-web.git" + }, + "keywords": [], + "author": "", + "license": "ISC" }