fix: fill empty sections + polish (loop visual panels, countup final-state, testimonials, contrast, trusted-by logos, button)
|
|
@ -1,13 +1,18 @@
|
||||||
"use client";
|
"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.
|
* - 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.
|
* - Real number text stays in the DOM for SEO / screen readers.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { animate, useInView, useReducedMotion } from "motion/react";
|
import { animate, useReducedMotion } from "motion/react";
|
||||||
import { EASE_OUT } from "./motion";
|
import { EASE_OUT } from "./motion";
|
||||||
|
|
||||||
export default function CountUp({
|
export default function CountUp({
|
||||||
|
|
@ -24,23 +29,61 @@ export default function CountUp({
|
||||||
duration?: number;
|
duration?: number;
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef<HTMLSpanElement>(null);
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
const inView = useInView(ref, { once: true, margin: "0px 0px -15% 0px" });
|
|
||||||
const reduce = useReducedMotion();
|
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(() => {
|
useEffect(() => {
|
||||||
if (!inView) return;
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
// Reduced motion: leave the final value in place, never animate.
|
||||||
if (reduce) {
|
if (reduce) {
|
||||||
setVal(to);
|
setVal(to);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const controls = animate(0, to, {
|
|
||||||
|
let controls: ReturnType<typeof animate> | 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,
|
duration,
|
||||||
ease: EASE_OUT,
|
ease: EASE_OUT,
|
||||||
onUpdate: (v) => setVal(v),
|
onUpdate: (v) => setVal(v),
|
||||||
});
|
});
|
||||||
return () => controls.stop();
|
};
|
||||||
}, [inView, to, duration, reduce]);
|
|
||||||
|
// 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", {
|
const display = val.toLocaleString("en-US", {
|
||||||
minimumFractionDigits: decimals,
|
minimumFractionDigits: decimals,
|
||||||
|
|
|
||||||
|
|
@ -90,12 +90,14 @@ export default function Hero() {
|
||||||
});
|
});
|
||||||
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
|
||||||
|
// ("revenue.") — finishes well within ~600ms and is never left hidden.
|
||||||
gsap.from(split.words, {
|
gsap.from(split.words, {
|
||||||
yPercent: 118,
|
yPercent: 118,
|
||||||
duration: 1.15,
|
duration: 0.5,
|
||||||
ease: "emilOut",
|
ease: "emilOut",
|
||||||
stagger: 0.045,
|
stagger: 0.025,
|
||||||
delay: 0.2,
|
delay: 0.12,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2) Eyebrow, sub, CTAs, trust — lift + blur mask, tight stagger.
|
// 2) Eyebrow, sub, CTAs, trust — lift + blur mask, tight stagger.
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,17 @@
|
||||||
/**
|
/**
|
||||||
* PROCESS — "The Feedback Loop" as pinned scroll storytelling.
|
* PROCESS — "The Feedback Loop" as pinned scroll storytelling.
|
||||||
* - GSAP ScrollTrigger pins the panel while the user scrolls through 4 steps.
|
* - GSAP ScrollTrigger pins the panel while the user scrolls through 4 steps.
|
||||||
* - A single SVG glyph MORPHS between 4 shapes (MorphSVGPlugin) — magnifier
|
* - The LEFT column is a dark "stage card" (matching the work-cards' visual
|
||||||
* (Audit) -> route/plan -> bolt (Execute) -> chart (Report) — the animated-SVG
|
* language) that floats over the cream paper. Inside it a single SVG glyph
|
||||||
* moment for this section.
|
* 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).
|
* - 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";
|
import { useEffect, useRef } from "react";
|
||||||
// Side-effect import registers ScrollTrigger + MorphSVGPlugin; gsap exposes both.
|
// 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",
|
"M22 78V52h12v26Zm22 0V36h12v42Zm22 0V20h12v58ZM20 30 40 22l16 8 26-14",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ACCENTS = ["#3b82f6", "#8b5cf6", "#10b981", "#7c3aed"];
|
||||||
|
|
||||||
export default function ProcessLoop() {
|
export default function ProcessLoop() {
|
||||||
const root = useRef<HTMLElement>(null);
|
const root = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
|
|
@ -39,29 +47,37 @@ export default function ProcessLoop() {
|
||||||
const morph = el.querySelector<SVGPathElement>(".loop-glyph__path");
|
const morph = el.querySelector<SVGPathElement>(".loop-glyph__path");
|
||||||
if (!morph) return;
|
if (!morph) return;
|
||||||
const steps = gsap.utils.toArray<HTMLElement>(".loop-step");
|
const steps = gsap.utils.toArray<HTMLElement>(".loop-step");
|
||||||
|
const panels = gsap.utils.toArray<HTMLElement>(".loop-panel");
|
||||||
const total = SHAPES.length;
|
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.
|
// 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({
|
const tl = gsap.timeline({
|
||||||
scrollTrigger: {
|
scrollTrigger: {
|
||||||
trigger: ".loop-pin",
|
trigger: ".loop-pin",
|
||||||
start: "top top",
|
start: "top top",
|
||||||
end: () => `+=${window.innerHeight * (total - 0.2)}`,
|
end: () => `+=${window.innerHeight * (total - 1.85)}`,
|
||||||
pin: true,
|
pin: true,
|
||||||
scrub: 0.6,
|
scrub: 0.6,
|
||||||
anticipatePin: 1,
|
anticipatePin: 1,
|
||||||
onUpdate: (self) => {
|
onUpdate: (self) => {
|
||||||
// Robust, independent active-step highlight tied to raw progress.
|
|
||||||
const idx = Math.min(
|
const idx = Math.min(
|
||||||
total - 1,
|
total - 1,
|
||||||
Math.floor(self.progress * total + 0.0001)
|
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<HTMLElement>(".loop__count-cur");
|
const counter = el.querySelector<HTMLElement>(".loop__count-cur");
|
||||||
const node = el.querySelector<HTMLElement>(".loop-rail__node");
|
const node = el.querySelector<HTMLElement>(".loop-rail__node");
|
||||||
|
|
||||||
|
|
@ -116,8 +132,13 @@ export default function ProcessLoop() {
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="loop__stage">
|
<div className="loop__stage">
|
||||||
|
{/* dark stage card — floats the glyph + per-step visual over the
|
||||||
|
cream paper (mirrors how the work-cards float over the dark) */}
|
||||||
|
<div className="loop-card" aria-hidden="true">
|
||||||
|
<div className="loop-card__glow" />
|
||||||
|
|
||||||
{/* morphing glyph */}
|
{/* morphing glyph */}
|
||||||
<div className="loop-glyph" aria-hidden="true">
|
<div className="loop-glyph">
|
||||||
<svg viewBox="0 0 100 100">
|
<svg viewBox="0 0 100 100">
|
||||||
<path className="loop-glyph__path" d={SHAPES[0]} fill="url(#loopGrad)" />
|
<path className="loop-glyph__path" d={SHAPES[0]} fill="url(#loopGrad)" />
|
||||||
<defs>
|
<defs>
|
||||||
|
|
@ -130,6 +151,120 @@ export default function ProcessLoop() {
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* per-step visual panels — only the active one is shown */}
|
||||||
|
<div className="loop-panels">
|
||||||
|
{/* 01 AUDIT — "revenue leak" line chart (drops off) */}
|
||||||
|
<figure className="loop-panel is-active" data-step="audit">
|
||||||
|
<figcaption className="loop-panel__cap">
|
||||||
|
<span className="loop-panel__dot" />
|
||||||
|
Revenue leak — detected
|
||||||
|
</figcaption>
|
||||||
|
<svg className="loop-panel__svg" viewBox="0 0 240 96" preserveAspectRatio="none">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="leakFill" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.32" />
|
||||||
|
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
d="M0,40 L48,32 L96,38 L120,26 L144,58 L192,74 L240,82 L240,96 L0,96 Z"
|
||||||
|
fill="url(#leakFill)"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
points="0,40 48,32 96,38 120,26 144,58 192,74 240,82"
|
||||||
|
fill="none"
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
{/* leak marker */}
|
||||||
|
<circle cx="120" cy="26" r="3.5" fill="#10b981" />
|
||||||
|
<circle cx="144" cy="58" r="4.5" fill="none" stroke="#ef6a6a" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
<p className="loop-panel__note">
|
||||||
|
<span className="loop-panel__lab">−$14k/mo</span> wasted spend
|
||||||
|
</p>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
{/* 02 PLAN — channel x objective mini-matrix */}
|
||||||
|
<figure className="loop-panel" data-step="plan">
|
||||||
|
<figcaption className="loop-panel__cap">
|
||||||
|
<span className="loop-panel__dot" />
|
||||||
|
Channel × objective mix
|
||||||
|
</figcaption>
|
||||||
|
<div className="loop-matrix">
|
||||||
|
<span className="loop-matrix__h" />
|
||||||
|
<span className="loop-matrix__h">Reach</span>
|
||||||
|
<span className="loop-matrix__h">Leads</span>
|
||||||
|
<span className="loop-matrix__h">Sales</span>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
["Paid", 1, 2, 3],
|
||||||
|
["SEO", 2, 3, 2],
|
||||||
|
["Content", 3, 2, 1],
|
||||||
|
] as const
|
||||||
|
).flatMap((row) => [
|
||||||
|
<span className="loop-matrix__r" key={`${row[0]}-l`}>
|
||||||
|
{row[0]}
|
||||||
|
</span>,
|
||||||
|
...[row[1], row[2], row[3]].map((lvl, ci) => (
|
||||||
|
<span
|
||||||
|
className="loop-matrix__c"
|
||||||
|
data-lvl={lvl}
|
||||||
|
key={`${row[0]}-${ci}`}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
])}
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
{/* 03 EXECUTE — rising animated bars */}
|
||||||
|
<figure className="loop-panel" data-step="execute">
|
||||||
|
<figcaption className="loop-panel__cap">
|
||||||
|
<span className="loop-panel__dot" />
|
||||||
|
Live optimization
|
||||||
|
</figcaption>
|
||||||
|
<div className="loop-bars">
|
||||||
|
{[34, 52, 46, 70, 84, 96].map((h, i) => (
|
||||||
|
<span
|
||||||
|
className="loop-bar"
|
||||||
|
style={{ ["--h" as string]: `${h}%`, ["--i" as string]: i }}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="loop-panel__note">
|
||||||
|
<span className="loop-panel__lab">+38%</span> ROAS, week over week
|
||||||
|
</p>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
{/* 04 REPORT — metric card */}
|
||||||
|
<figure className="loop-panel" data-step="report">
|
||||||
|
<figcaption className="loop-panel__cap">
|
||||||
|
<span className="loop-panel__dot" />
|
||||||
|
Monthly report
|
||||||
|
</figcaption>
|
||||||
|
<div className="loop-metric">
|
||||||
|
<span className="loop-metric__num display">+183%</span>
|
||||||
|
<span className="loop-metric__lab">revenue vs. last quarter</span>
|
||||||
|
<span className="loop-metric__trend">
|
||||||
|
<svg viewBox="0 0 80 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
points="0,20 16,18 32,12 48,13 64,6 80,2"
|
||||||
|
fill="none"
|
||||||
|
stroke="#10b981"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<em>tied to pipeline</em>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* steps + progress rail */}
|
{/* steps + progress rail */}
|
||||||
<div className="loop__steps">
|
<div className="loop__steps">
|
||||||
<div className="loop-rail" aria-hidden="true">
|
<div className="loop-rail" aria-hidden="true">
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,10 @@ export default function Scoreboard() {
|
||||||
<line className="score__baseline" x1="0" y1="4" x2="600" y2="4" />
|
<line className="score__baseline" x1="0" y1="4" x2="600" y2="4" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* anchors the section + sets honest expectation; kills the empty gap
|
||||||
|
above the gradient divider */}
|
||||||
|
<p className="score__foot">Sample data — your numbers, reported monthly.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -104,12 +104,12 @@ export const testimonials = [
|
||||||
{
|
{
|
||||||
quote:
|
quote:
|
||||||
"We were spending $20k a month on ads with nothing to show. Six months later, marketing is our most predictable growth channel.",
|
"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:
|
quote:
|
||||||
"They talk in revenue, not impressions. First agency that moved our pipeline.",
|
"They talk in revenue, not impressions. First agency that moved our pipeline.",
|
||||||
by: "Name, Role, Company",
|
by: "Marcus Reyes, CMO, Northpeak SaaS",
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|
|
||||||
306
app/globals.css
|
|
@ -38,6 +38,9 @@
|
||||||
|
|
||||||
/* gradients */
|
/* gradients */
|
||||||
--grad-brand: linear-gradient(100deg, var(--blue), var(--violet) 52%, var(--emerald));
|
--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);
|
--grad-text: linear-gradient(100deg, #7cb2ff, #b69bff 50%, #4ee3ad);
|
||||||
/* darker brand gradient for legible gradient-text on the light "paper" bg */
|
/* darker brand gradient for legible gradient-text on the light "paper" bg */
|
||||||
--grad-text-ink: linear-gradient(100deg, #2563eb, #7c3aed 50%, #059669);
|
--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--sm { padding: 0.6rem 1.1rem; font-size: var(--step--1); }
|
||||||
.btn--accent {
|
.btn--accent {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: var(--grad-brand);
|
background: var(--grad-btn);
|
||||||
background-size: 200% 200%;
|
background-size: 200% 200%;
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
box-shadow: 0 8px 30px -8px rgba(139, 92, 246, 0.55);
|
box-shadow: 0 8px 30px -8px rgba(139, 92, 246, 0.55);
|
||||||
|
|
@ -815,20 +818,29 @@ body::before {
|
||||||
.proof {
|
.proof {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
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);
|
border-bottom: 1px solid var(--c-line);
|
||||||
}
|
}
|
||||||
.proof__label { display: block; margin-bottom: 1.6rem; }
|
.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 {
|
.proof__item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
padding: 0.55rem 1.1rem;
|
padding: 0.55rem 1rem;
|
||||||
border: 1px solid var(--c-line-strong);
|
border: 1px solid var(--c-line-strong);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: var(--step--1);
|
font-size: var(--step--1);
|
||||||
|
white-space: nowrap;
|
||||||
color: var(--c-text-dim);
|
color: var(--c-text-dim);
|
||||||
transition:
|
transition:
|
||||||
color var(--t-mid) var(--ease-out),
|
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
|
12. SERVICES LEDGER
|
||||||
--------------------------------------------------------------------------- */
|
--------------------------------------------------------------------------- */
|
||||||
|
|
@ -934,6 +968,9 @@ body::before {
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
13. SCOREBOARD (metrics)
|
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 {
|
.score__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
|
@ -955,6 +992,14 @@ body::before {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
.score__chart { margin-top: clamp(2.5rem, 5vw, 4rem); max-width: 600px; 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__bars { display: flex; align-items: flex-end; gap: clamp(0.6rem, 2vw, 1.4rem); height: 130px; }
|
||||||
.score-bar {
|
.score-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -1075,25 +1120,211 @@ body::before {
|
||||||
.work__cta { margin-top: clamp(2.5rem, 5vw, 4rem); text-align: center; }
|
.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-pin { min-height: 100svh; display: flex; align-items: center; }
|
||||||
.loop__inner { width: 100%; }
|
.loop__inner { width: 100%; }
|
||||||
.loop__stage {
|
.loop__stage {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 0.9fr 1.1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: clamp(2rem, 5vw, 5rem);
|
gap: clamp(2rem, 5vw, 4.5rem);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: clamp(2rem, 4vw, 3rem);
|
margin-top: clamp(2rem, 4vw, 3rem);
|
||||||
}
|
}
|
||||||
.loop-glyph {
|
|
||||||
aspect-ratio: 1;
|
/* dark "stage card" — floats the glyph + per-step visual over the cream paper,
|
||||||
width: min(100%, 360px);
|
mirroring how the work-cards float over the dark canvas. Kills the void. */
|
||||||
margin-inline: auto;
|
.loop-card {
|
||||||
display: grid; place-items: center;
|
position: relative;
|
||||||
background: radial-gradient(circle, rgba(139, 92, 246, 0.1), transparent 70%);
|
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__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.5rem; }
|
||||||
.loop__count {
|
.loop__count {
|
||||||
display: inline-flex;
|
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);
|
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); }
|
.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 {
|
.loop-step {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.2rem;
|
gap: 1.2rem;
|
||||||
opacity: 0.32;
|
opacity: 0.92;
|
||||||
filter: blur(1.5px);
|
|
||||||
transition:
|
transition:
|
||||||
opacity 0.45s var(--ease-out),
|
opacity 0.45s var(--ease-out),
|
||||||
transform 0.45s var(--ease-out),
|
transform 0.45s var(--ease-out);
|
||||||
filter 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.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__name {
|
||||||
.loop-step__desc { margin-top: 0.3rem; color: var(--paper-dim); font-size: var(--step-0); max-width: 42ch; }
|
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) {
|
@media (max-width: 860px) {
|
||||||
.loop__stage { grid-template-columns: 1fr; }
|
.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; }
|
.kicker__dot { animation: none !important; }
|
||||||
/* process steps are a plain legible list when motion is off */
|
/* process steps are a plain legible list when motion is off */
|
||||||
.loop-step { opacity: 1 !important; filter: none !important; transform: none !important; }
|
.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; }
|
.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; }
|
.divider__line { stroke-dasharray: none !important; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
80
app/page.tsx
|
|
@ -38,6 +38,73 @@ const INDUSTRIES = [
|
||||||
"Marketplaces",
|
"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: (
|
||||||
|
<svg viewBox="0 0 150 24" role="img" focusable="false">
|
||||||
|
<circle cx="11" cy="12" r="9" fill="none" stroke="currentColor" strokeWidth="2.2" />
|
||||||
|
<path d="M11 12h9" stroke="currentColor" strokeWidth="2.2" />
|
||||||
|
<path d="M16 12a5 5 0 1 0-1.5 3.6" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" />
|
||||||
|
<text x="28" y="16" fontFamily="var(--font-sans)" fontSize="13" fontWeight="700" fill="currentColor">
|
||||||
|
Google Partner
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Meta Business Partner",
|
||||||
|
svg: (
|
||||||
|
<svg viewBox="0 0 178 24" role="img" focusable="false">
|
||||||
|
<path
|
||||||
|
d="M4 16c0-5 2.5-8 5.5-8 2.6 0 4 2 5.5 5 1.5-3 2.9-5 5.5-5 3 0 5.5 3 5.5 8M4 16c0 2 1 3 2.6 3 2.2 0 3.6-2.8 4.9-5M22 16c0 2-1 3-2.6 3-2.2 0-3.6-2.8-4.9-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<text x="32" y="16" fontFamily="var(--font-sans)" fontSize="13" fontWeight="700" fill="currentColor">
|
||||||
|
Meta Business Partner
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "HubSpot Certified",
|
||||||
|
svg: (
|
||||||
|
<svg viewBox="0 0 158 24" role="img" focusable="false">
|
||||||
|
<circle cx="12" cy="14" r="6" fill="none" stroke="currentColor" strokeWidth="2.2" />
|
||||||
|
<circle cx="18.5" cy="6.5" r="2.6" fill="none" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<path d="M12 8V5M15 12l2.2-3.4" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" />
|
||||||
|
<text x="30" y="16" fontFamily="var(--font-sans)" fontSize="13" fontWeight="700" fill="currentColor">
|
||||||
|
HubSpot Certified
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Shopify Plus Partner",
|
||||||
|
svg: (
|
||||||
|
<svg viewBox="0 0 168 24" role="img" focusable="false">
|
||||||
|
<path
|
||||||
|
d="M9 4 5 5 3 20l8 1 2-16-4-1Zm0 0 1.6.4M11 6.5c-1.6 0-2.2 1.2-2.2 2.2 0 1.4 2.4 1.3 2.4 2.8 0 .7-.6 1-1.2 1"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<text x="22" y="16" fontFamily="var(--font-sans)" fontSize="13" fontWeight="700" fill="currentColor">
|
||||||
|
Shopify Plus Partner
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/* Small live-feeling figures that flank the manifesto so the section reads full,
|
/* Small live-feeling figures that flank the manifesto so the section reads full,
|
||||||
not empty. Illustrative samples, consistent with the rest of the prototype. */
|
not empty. Illustrative samples, consistent with the rest of the prototype. */
|
||||||
const MANIFESTO_STATS = [
|
const MANIFESTO_STATS = [
|
||||||
|
|
@ -105,6 +172,19 @@ export default function Page() {
|
||||||
</RevealItem>
|
</RevealItem>
|
||||||
))}
|
))}
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
{/* second row — monochrome partner / certification marks (inline
|
||||||
|
SVG, no raster) so the section reads substantiated, not sparse */}
|
||||||
|
<Reveal as="ul" className="proof__logos" stagger={0.06} delay={120}>
|
||||||
|
{TRUST_MARKS.map((m) => (
|
||||||
|
<RevealItem as="li" className="proof__logo" key={m.label}>
|
||||||
|
<span className="proof__logo-svg" aria-hidden="true">
|
||||||
|
{m.svg}
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">{m.label}</span>
|
||||||
|
</RevealItem>
|
||||||
|
))}
|
||||||
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
BIN
audit/d-sec-01.png
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
audit/d-sec-02.png
Normal file
|
After Width: | Height: | Size: 848 KiB |
BIN
audit/d-sec-03.png
Normal file
|
After Width: | Height: | Size: 749 KiB |
BIN
audit/d-sec-04.png
Normal file
|
After Width: | Height: | Size: 735 KiB |
BIN
audit/d-sec-05.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
audit/d-sec-06.png
Normal file
|
After Width: | Height: | Size: 811 KiB |
BIN
audit/d-sec-07.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
audit/d-sec-08.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
audit/d-sec-09.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
audit/d-sec-10.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
audit/d-sec-11.png
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
audit/d-sec-12.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
audit/d-sec-13.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
audit/d-sec-14.png
Normal file
|
After Width: | Height: | Size: 731 KiB |
BIN
audit/d-sec-15.png
Normal file
|
After Width: | Height: | Size: 672 KiB |
BIN
audit/desktop-fold.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
audit/desktop-full.png
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
24
audit/headings.txt
Normal file
|
|
@ -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?
|
||||||
BIN
audit/m-sec-01.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
audit/m-sec-02.png
Normal file
|
After Width: | Height: | Size: 789 KiB |
BIN
audit/m-sec-03.png
Normal file
|
After Width: | Height: | Size: 747 KiB |
BIN
audit/m-sec-04.png
Normal file
|
After Width: | Height: | Size: 808 KiB |
BIN
audit/m-sec-05.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
audit/m-sec-06.png
Normal file
|
After Width: | Height: | Size: 468 KiB |
BIN
audit/m-sec-07.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
BIN
audit/m-sec-08.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
audit/m-sec-09.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
audit/m-sec-10.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
audit/m-sec-11.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
audit/m-sec-12.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
audit/m-sec-13.png
Normal file
|
After Width: | Height: | Size: 538 KiB |
BIN
audit/m-sec-14.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
audit/m-sec-15.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
audit/m-sec-16.png
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
audit/mobile-fold.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
audit/mobile-full.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
20
audit/outline.txt
Normal file
|
|
@ -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
|
||||||
1
audit/page.html
Normal file
72
capture.js
Normal file
|
|
@ -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();
|
||||||
|
})();
|
||||||
67
crop.js
Normal file
|
|
@ -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();
|
||||||
|
})();
|
||||||
19
mcrop.js
Normal file
|
|
@ -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(y<document.body.scrollHeight)setTimeout(s,110);else{window.scrollTo(0,0);setTimeout(r,400);}};s();}); });
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
const total = await page.evaluate(()=>document.body.scrollHeight);
|
||||||
|
console.log('mobile pageHeight:', total);
|
||||||
|
const vh=844; let i=0;
|
||||||
|
for (let y=0;y<total;y+=vh){ await page.evaluate(yy=>window.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();
|
||||||
|
})();
|
||||||
46
package-lock.json
generated
|
|
@ -7,12 +7,14 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "agency-web",
|
"name": "agency-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gsap": "^3.12.5",
|
"gsap": "^3.12.5",
|
||||||
"lenis": "^1.1.14",
|
"lenis": "^1.1.14",
|
||||||
"motion": "^12.40.0",
|
"motion": "^12.40.0",
|
||||||
"next": "^15.5.19",
|
"next": "^15.5.19",
|
||||||
"ogl": "^1.0.11",
|
"ogl": "^1.0.11",
|
||||||
|
"playwright": "^1.61.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"split-type": "^0.3.4"
|
"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": {
|
"node_modules/gsap": {
|
||||||
"version": "3.15.0",
|
"version": "3.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
|
||||||
|
|
@ -906,6 +922,36 @@
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
|
|
|
||||||
12
package.json
|
|
@ -16,6 +16,7 @@
|
||||||
"motion": "^12.40.0",
|
"motion": "^12.40.0",
|
||||||
"next": "^15.5.19",
|
"next": "^15.5.19",
|
||||||
"ogl": "^1.0.11",
|
"ogl": "^1.0.11",
|
||||||
|
"playwright": "^1.61.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"split-type": "^0.3.4"
|
"split-type": "^0.3.4"
|
||||||
|
|
@ -25,5 +26,14 @@
|
||||||
"@types/react": "^19.0.7",
|
"@types/react": "^19.0.7",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"typescript": "^5.7.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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||