feat: 'Iridescent Apparatus' homepage redesign (WebGL, kinetic, brand system)

This commit is contained in:
Feedback Studios 2026-06-16 05:34:34 +00:00
parent d4dbef11ed
commit daeb3f6cea
17 changed files with 3420 additions and 312 deletions

View file

@ -1,20 +1,34 @@
"use client"; "use client";
/**
* Cursor a character cursor with a difference-blend "lens".
*
* A small dot tracks tightly; a larger ring trails with easing and inverts the
* content beneath it (mix-blend-mode: difference). Over interactive elements it
* grows; over elements carrying data-cursor it swaps in a contextual label
* (e.g. "ver"). Disabled on touch / coarse pointers. Native cursor remains as a
* fallback so the page is always usable.
*/
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { gsap } from "gsap"; import { gsap } from "gsap";
export default function Cursor() { export default function Cursor() {
const dot = useRef<HTMLDivElement>(null); const dot = useRef<HTMLDivElement>(null);
const ring = useRef<HTMLDivElement>(null); const ring = useRef<HTMLDivElement>(null);
const label = useRef<HTMLSpanElement>(null);
useEffect(() => { useEffect(() => {
if (window.matchMedia("(pointer: coarse)").matches) return; if (window.matchMedia("(pointer: coarse)").matches) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
if (!dot.current || !ring.current) return; if (!dot.current || !ring.current) return;
const xD = gsap.quickTo(dot.current, "x", { duration: 0.15, ease: "power3" }); document.body.classList.add("has-custom-cursor");
const yD = gsap.quickTo(dot.current, "y", { duration: 0.15, ease: "power3" });
const xR = gsap.quickTo(ring.current, "x", { duration: 0.5, ease: "power3" }); const xD = gsap.quickTo(dot.current, "x", { duration: 0.12, ease: "power3" });
const yR = gsap.quickTo(ring.current, "y", { duration: 0.5, ease: "power3" }); const yD = gsap.quickTo(dot.current, "y", { duration: 0.12, ease: "power3" });
const xR = gsap.quickTo(ring.current, "x", { duration: 0.55, ease: "power3" });
const yR = gsap.quickTo(ring.current, "y", { duration: 0.55, ease: "power3" });
const move = (e: MouseEvent) => { const move = (e: MouseEvent) => {
xD(e.clientX); xD(e.clientX);
@ -24,16 +38,30 @@ export default function Cursor() {
}; };
const over = (e: Event) => { const over = (e: Event) => {
if ((e.target as HTMLElement).closest("a,button,.hoverable")) { const t = (e.target as HTMLElement).closest<HTMLElement>(
ring.current?.classList.add("cursor-ring--big"); "a,button,.hoverable,[data-cursor]"
);
if (!t) return;
ring.current?.classList.add("cursor-ring--big");
const text = t.getAttribute("data-cursor");
if (text && label.current) {
label.current.textContent = text;
ring.current?.classList.add("cursor-ring--label");
} }
}; };
const out = () => ring.current?.classList.remove("cursor-ring--big"); const out = (e: Event) => {
const t = (e.target as HTMLElement).closest<HTMLElement>(
"a,button,.hoverable,[data-cursor]"
);
if (!t) return;
ring.current?.classList.remove("cursor-ring--big", "cursor-ring--label");
};
window.addEventListener("mousemove", move); window.addEventListener("mousemove", move, { passive: true });
document.addEventListener("mouseover", over); document.addEventListener("mouseover", over);
document.addEventListener("mouseout", out); document.addEventListener("mouseout", out);
return () => { return () => {
document.body.classList.remove("has-custom-cursor");
window.removeEventListener("mousemove", move); window.removeEventListener("mousemove", move);
document.removeEventListener("mouseover", over); document.removeEventListener("mouseover", over);
document.removeEventListener("mouseout", out); document.removeEventListener("mouseout", out);
@ -42,8 +70,10 @@ export default function Cursor() {
return ( return (
<> <>
<div ref={ring} className="cursor-ring" aria-hidden /> <div ref={ring} className="cursor-ring" aria-hidden="true">
<div ref={dot} className="cursor-dot" aria-hidden /> <span ref={label} className="cursor-ring__label" />
</div>
<div ref={dot} className="cursor-dot" aria-hidden="true" />
</> </>
); );
} }

View file

@ -2,107 +2,125 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { gsap } from "gsap"; import { gsap } from "gsap";
import KineticText from "./KineticText";
const line1 = "La mayoría de agencias alquilan sus herramientas.".split(" "); import PillMark from "./PillMark";
const line2 = "Nosotros construimos la nuestra.".split(" "); import Magnetic from "./Magnetic";
function Words({ words, grad }: { words: string[]; grad?: boolean }) {
return (
<span className={"line" + (grad ? " grad" : "")}>
{words.map((w, i) => (
<span className="word" key={i}>
<span className="word-in">{w}</span>{" "}
</span>
))}
</span>
);
}
export default function Hero() { export default function Hero() {
const root = useRef<HTMLElement>(null); const root = useRef<HTMLElement>(null);
const mark = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
const el = root.current; const el = root.current;
if (!el) return; if (!el) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
const tl = gsap.timeline({ defaults: { ease: "power4.out" } }); const tl = gsap.timeline({ defaults: { ease: "power4.out" } });
tl.from(".eyebrow", { y: 20, opacity: 0, duration: 0.8 }) tl.from(".hero__eyebrow", { y: 18, opacity: 0, duration: 0.9, delay: 0.15 })
.from(".hero__sub", { y: 22, opacity: 0, duration: 0.9 }, "-=0.2")
.from( .from(
".word-in", ".hero__actions > *",
{ yPercent: 115, duration: 1.05, stagger: 0.045 }, { y: 20, opacity: 0, stagger: 0.1, duration: 0.7 },
"-=0.4" "-=0.5"
) )
.from(".hero-sub", { y: 24, opacity: 0, duration: 0.9 }, "-=0.7") .from(".hero__meta", { opacity: 0, duration: 0.8 }, "-=0.4");
.from(".hero-actions > *", { y: 20, opacity: 0, stagger: 0.12, duration: 0.7 }, "-=0.6")
.from(".bar", { scaleX: 0, transformOrigin: "left", stagger: 0.08, duration: 0.7, ease: "power3.inOut" }, "-=1.1");
// floating pill-mark // parallax on the floating mark as you scroll the hero out
gsap.to(".bar", { gsap.to(".hero__mark", {
y: "+=8", yPercent: -22,
duration: 2.4, ease: "none",
ease: "sine.inOut", scrollTrigger: {
repeat: -1, trigger: el,
yoyo: true, start: "top top",
stagger: { each: 0.15, from: "random" }, end: "bottom top",
scrub: true,
},
});
gsap.to(".hero__display", {
yPercent: 12,
opacity: 0.25,
ease: "none",
scrollTrigger: {
trigger: el,
start: "top top",
end: "bottom top",
scrub: true,
},
}); });
}, el); }, el);
const onMove = (e: MouseEvent) => { return () => ctx.revert();
const rx = (e.clientX / window.innerWidth - 0.5) * 2;
const ry = (e.clientY / window.innerHeight - 0.5) * 2;
gsap.to(mark.current, { x: rx * 22, y: ry * 22, duration: 0.8, ease: "power3" });
gsap.to(".blob", { x: rx * 30, y: ry * 30, duration: 1.2, ease: "power3" });
};
window.addEventListener("mousemove", onMove);
return () => {
window.removeEventListener("mousemove", onMove);
ctx.revert();
};
}, []); }, []);
return ( return (
<section className="hero" ref={root}> <section className="hero" ref={root}>
<div className="mesh" aria-hidden> <div className="hero__inner wrap">
<span className="blob b1" /> <p className="hero__eyebrow">
<span className="blob b2" /> <span className="dot" aria-hidden="true" />
<span className="blob b3" /> Agencia de marketing AI-native
<span className="blob b4" />
</div>
<div className="hero-inner wrap">
<p className="eyebrow">Agencia de marketing AI-native</p>
<div ref={mark} className="pillmark" aria-hidden>
<span className="bar bar-grad" />
<div className="bar-row">
<span className="bar bar-ink" />
<span className="bar bar-blue" />
</div>
<span className="bar bar-green" />
</div>
<h1 className="hero-h1">
<Words words={line1} />
<Words words={line2} grad />
</h1>
<p className="hero-sub">
Estrategia humana + nuestra propia plataforma de IA. Web, SEO, ads y
contenido más rápido, más medible y a mejor coste que una agencia
tradicional.
</p> </p>
<div className="hero-actions"> <h1 className="hero__display">
<a className="btn primary hoverable" href="#contacto">Habla con nosotros</a> <KineticText as="span" className="hero__line" text="No alquilamos" immediate delay={0.25} />
<a className="btn ghost hoverable" href="#servicios">Ver qué hacemos</a> <span className="hero__line hero__line--mixed">
<KineticText as="span" text="las" immediate delay={0.4} />
<em className="hero__serif"> herramientas.</em>
</span>
<KineticText
as="span"
className="hero__line hero__line--grad"
text="Construimos la máquina."
immediate
delay={0.5}
highlight={[0, 2]}
/>
</h1>
<div className="hero__mark" aria-hidden="true">
<PillMark animate breathe />
</div> </div>
<p className="hero__sub">
Estrategia humana sobre <strong>nuestra propia plataforma de IA</strong>.
Web, SEO, ads y contenido más rápido, más medible y a mejor coste que
una agencia tradicional.
</p>
<div className="hero__actions">
<Magnetic strength={0.5}>
<a className="btn btn--primary hoverable" href="#contacto">
<span>Habla con nosotros</span>
</a>
</Magnetic>
<Magnetic strength={0.3}>
<a className="btn btn--ghost hoverable" href="#servicios">
<span>Ver qué hacemos</span>
</a>
</Magnetic>
</div>
<dl className="hero__meta">
<div>
<dt>Entrega</dt>
<dd>en días, no semanas</dd>
</div>
<div>
<dt>Reporting</dt>
<dd>cada acción medida</dd>
</div>
<div>
<dt>Infraestructura</dt>
<dd>propia, no alquilada</dd>
</div>
</dl>
</div> </div>
<div className="scroll-hint" aria-hidden>scroll</div> <a className="hero__scroll hoverable" href="#ventaja" aria-label="Bajar a la siguiente sección">
<span>scroll</span>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path d="M12 4v16m0 0l-6-6m6 6l6-6" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</a>
</section> </section>
); );
} }

View file

@ -0,0 +1,235 @@
"use client";
/**
* Iridescence generative WebGL backdrop.
*
* A living interpretation of the brand cloudscape: domain-warped fractal noise
* mapped onto an iridescent pastel palette (lavender -> blue -> rose -> mint),
* with a faint rainbow refraction band and animated film grain. Drifts on its
* own and leans toward the cursor. Falls back to a static CSS gradient when
* WebGL is unavailable or the user prefers reduced motion.
*
* Renders behind everything (fixed, -z) and is purely decorative (aria-hidden).
*/
import { useEffect, useRef } from "react";
import { Renderer, Program, Mesh, Triangle, Vec2 } from "ogl";
const VERT = /* glsl */ `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const FRAG = /* glsl */ `
precision highp float;
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uMouse; // 0..1, smoothed
uniform float uIntensity; // global motion amount (0 for reduced motion)
// Brand palette anchors
const vec3 VIOLET = vec3(0.545, 0.361, 0.965); // #8b5cf6
const vec3 BLUE = vec3(0.231, 0.510, 0.965); // #3b82f6
const vec3 ROSE = vec3(0.957, 0.475, 0.851); // soft pink
const vec3 MINT = vec3(0.063, 0.725, 0.506); // #10b981 softened
const vec3 HAZE = vec3(0.945, 0.949, 0.992); // near-white lavender
// -- hash / value noise --------------------------------------------------
vec2 hash22(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453123);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = dot(hash22(i + vec2(0.0, 0.0)) - 0.5, f - vec2(0.0, 0.0));
float b = dot(hash22(i + vec2(1.0, 0.0)) - 0.5, f - vec2(1.0, 0.0));
float c = dot(hash22(i + vec2(0.0, 1.0)) - 0.5, f - vec2(0.0, 1.0));
float d = dot(hash22(i + vec2(1.0, 1.0)) - 0.5, f - vec2(1.0, 1.0));
return 0.5 + mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0;
float amp = 0.55;
mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);
for (int i = 0; i < 5; i++) {
v += amp * noise(p);
p = rot * p * 2.0;
amp *= 0.5;
}
return v;
}
// film grain
float grain(vec2 uv, float t) {
return fract(sin(dot(uv * (t + 1.0), vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
vec2 uv = gl_FragCoord.xy / uResolution.xy;
float aspect = uResolution.x / uResolution.y;
vec2 p = uv;
p.x *= aspect;
float t = uTime * 0.045 * (0.25 + uIntensity);
// mouse parallax (gentle)
vec2 m = (uMouse - 0.5);
p += m * 0.12 * uIntensity;
// domain warping for that liquid cloud feel
vec2 q = vec2(fbm(p + vec2(0.0, t)), fbm(p + vec2(5.2, -t)));
vec2 r = vec2(
fbm(p + 1.7 * q + vec2(8.3, 2.8) + 0.15 * t),
fbm(p + 1.7 * q + vec2(1.2, 6.5) - 0.12 * t)
);
float f = fbm(p + 2.0 * r);
// build the iridescent gradient from the field
vec3 col = HAZE;
col = mix(col, VIOLET, smoothstep(0.15, 0.85, f));
col = mix(col, BLUE, smoothstep(0.30, 0.95, r.x));
col = mix(col, ROSE, smoothstep(0.55, 1.05, q.y) * 0.65);
col = mix(col, MINT, smoothstep(0.62, 1.0, r.y) * 0.30);
// central radiant bloom (echoes the cloudscape light source up high)
vec2 center = vec2(0.5 * aspect, 0.86);
float d = distance(p, center);
float bloom = smoothstep(0.9, 0.0, d);
col = mix(col, HAZE, bloom * 0.55);
// faint rainbow refraction band, lower-left like the asset
float band = smoothstep(0.5, 0.0, abs((uv.x - uv.y) * 1.3 - 0.15 + 0.05 * sin(t * 2.0)));
vec3 spectrum = 0.5 + 0.5 * cos(6.2831 * (vec3(0.0, 0.33, 0.67) + (uv.x + uv.y) * 0.7));
col += spectrum * band * 0.06 * (0.4 + uIntensity);
// lift overall to keep it airy / pastel and protect text legibility
col = mix(col, vec3(1.0), 0.18);
// soft vignette to anchor content readability
float vig = smoothstep(1.25, 0.25, distance(uv, vec2(0.5)));
col *= 0.82 + 0.18 * vig;
// animated grain to kill banding + add texture
float g = grain(uv, floor(uTime * 12.0) * 0.5);
col += (g - 0.5) * 0.035;
gl_FragColor = vec4(col, 1.0);
}
`;
export default function Iridescence() {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = ref.current;
if (!canvas) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let renderer: Renderer;
try {
renderer = new Renderer({
canvas,
dpr: Math.min(window.devicePixelRatio, 1.75),
alpha: false,
antialias: false,
});
} catch {
// WebGL unavailable — CSS fallback (set on the host) remains visible.
canvas.style.display = "none";
return;
}
const gl = renderer.gl;
gl.clearColor(0.95, 0.95, 0.99, 1);
const uniforms = {
uTime: { value: 0 },
uResolution: { value: new Vec2(1, 1) },
uMouse: { value: new Vec2(0.5, 0.5) },
uIntensity: { value: reduce ? 0 : 1 },
};
const program = new Program(gl, {
vertex: VERT,
fragment: FRAG,
uniforms,
});
const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });
const resize = () => {
const w = window.innerWidth;
const h = window.innerHeight;
renderer.setSize(w, h);
uniforms.uResolution.value.set(gl.drawingBufferWidth, gl.drawingBufferHeight);
};
resize();
window.addEventListener("resize", resize);
// smoothed pointer
const target = { x: 0.5, y: 0.5 };
const onMove = (e: PointerEvent) => {
target.x = e.clientX / window.innerWidth;
target.y = 1 - e.clientY / window.innerHeight;
};
window.addEventListener("pointermove", onMove, { passive: true });
let raf = 0;
let running = true;
const start = performance.now();
const loop = (now: number) => {
if (!running) return;
raf = requestAnimationFrame(loop);
uniforms.uTime.value = (now - start) / 1000;
// ease pointer
const m = uniforms.uMouse.value;
m.x += (target.x - m.x) * 0.04;
m.y += (target.y - m.y) * 0.04;
renderer.render({ scene: mesh });
};
if (reduce) {
// render a single frame, then idle
uniforms.uTime.value = 18;
renderer.render({ scene: mesh });
} else {
raf = requestAnimationFrame(loop);
}
// pause when tab hidden (perf + battery)
const onVis = () => {
if (document.hidden) {
running = false;
cancelAnimationFrame(raf);
} else if (!reduce) {
running = true;
raf = requestAnimationFrame(loop);
}
};
document.addEventListener("visibilitychange", onVis);
return () => {
running = false;
cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
window.removeEventListener("pointermove", onMove);
document.removeEventListener("visibilitychange", onVis);
const ext = gl.getExtension("WEBGL_lose_context");
ext?.loseContext();
};
}, []);
return (
<div className="iridescence" aria-hidden="true">
<canvas ref={ref} className="iridescence__canvas" />
</div>
);
}

View file

@ -0,0 +1,83 @@
"use client";
/**
* KineticText word/line masked reveal driven by ScrollTrigger.
*
* Splits the given text into words wrapped in overflow-hidden masks (manual
* spans no premium SplitText plugin) and slides each word up from below as it
* enters the viewport, with a stagger. Renders the text as real, selectable DOM
* so it stays accessible and SEO-safe; animation only transforms (perf-friendly).
*
* With reduced-motion, the text simply appears (no transform).
*/
import { createElement, useEffect, useRef, type ElementType } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
type Props = {
text: string;
as?: ElementType;
className?: string;
/** Delay the whole reveal (s). */
delay?: number;
/** Start animation on mount instead of on scroll (for the hero). */
immediate?: boolean;
/** Mark a word index range as gradient-highlighted. */
highlight?: [number, number];
};
export default function KineticText({
text,
as: Tag = "span",
className = "",
delay = 0,
immediate = false,
highlight,
}: Props) {
const ref = useRef<HTMLElement>(null);
const words = text.split(" ");
useEffect(() => {
const el = ref.current;
if (!el) return;
const inner = gsap.utils.toArray<HTMLElement>(".ktext__in", el);
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
gsap.set(inner, { yPercent: 0, opacity: 1 });
return;
}
const ctx = gsap.context(() => {
gsap.set(inner, { yPercent: 118 });
const anim: gsap.TweenVars = {
yPercent: 0,
duration: 1.0,
ease: "power4.out",
stagger: 0.045,
delay,
};
if (!immediate) {
anim.scrollTrigger = { trigger: el, start: "top 88%" };
}
gsap.to(inner, anim);
}, el);
return () => ctx.revert();
}, [delay, immediate]);
const children = words.map((w, i) => {
const hot =
highlight && i >= highlight[0] && i <= highlight[1] ? " ktext__word--grad" : "";
return (
<span className={`ktext__word${hot}`} key={i}>
<span className="ktext__in">{w}</span>
{i < words.length - 1 ? " " : ""}
</span>
);
});
return createElement(Tag, { ref, className: `ktext ${className}` }, children);
}

View file

@ -0,0 +1,60 @@
"use client";
/**
* Magnetic wraps a single interactive child and pulls it toward the cursor
* within a radius, springing back on leave. Disabled for coarse pointers and
* reduced-motion. Purely visual; does not alter semantics of the child.
*/
import { useEffect, useRef, type ReactNode } from "react";
import { gsap } from "gsap";
export default function Magnetic({
children,
strength = 0.4,
className = "",
}: {
children: ReactNode;
strength?: number;
className?: string;
}) {
const ref = useRef<HTMLSpanElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (window.matchMedia("(pointer: coarse)").matches) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const target = el.firstElementChild as HTMLElement | null;
if (!target) return;
const xTo = gsap.quickTo(target, "x", { duration: 0.5, ease: "elastic.out(1, 0.4)" });
const yTo = gsap.quickTo(target, "y", { duration: 0.5, ease: "elastic.out(1, 0.4)" });
const move = (e: MouseEvent) => {
const r = el.getBoundingClientRect();
const mx = e.clientX - (r.left + r.width / 2);
const my = e.clientY - (r.top + r.height / 2);
xTo(mx * strength);
yTo(my * strength);
};
const leave = () => {
xTo(0);
yTo(0);
};
el.addEventListener("mousemove", move);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventListener("mousemove", move);
el.removeEventListener("mouseleave", leave);
};
}, [strength]);
return (
<span ref={ref} className={`magnetic ${className}`}>
{children}
</span>
);
}

View file

@ -0,0 +1,40 @@
"use client";
/**
* Marquee seamless looping band of disciplines, separated by the brand pill
* glyph. Pure CSS animation (transform only); pauses under reduced-motion via
* the stylesheet. Decorative.
*/
const items = [
"Web & Desarrollo",
"SEO & GEO",
"Paid Ads",
"Contenido",
"Diseño & Marca",
"Automatización",
];
function Row() {
return (
<div className="marquee__row" aria-hidden="true">
{items.map((it) => (
<span className="marquee__item" key={it}>
{it}
<span className="marquee__sep" />
</span>
))}
</div>
);
}
export default function Marquee() {
return (
<div className="marquee" aria-hidden="true">
<div className="marquee__track">
<Row />
<Row />
</div>
</div>
);
}

View file

@ -0,0 +1,98 @@
"use client";
/**
* Packages four tiers, each named for an outcome rather than a task list.
* "Motor" is the featured tier (lifted, gradient spine). Cards reveal on scroll
* and the featured one carries a continuously-shifting gradient border.
*/
import Reveal from "./Reveal";
type Pkg = {
n: string;
who: string;
price: string;
f: string[];
featured?: boolean;
};
const packages: Pkg[] = [
{
n: "Sitio AI-native",
who: "Necesitas web, ya",
price: "Proyecto",
f: ["Web en código, editable", "SEO base + tracking", "Entregada en tiempo récord"],
},
{
n: "Base",
who: "Estás arrancando",
price: "Mensual",
f: ["Fundamentos SEO", "Contenido inicial", "Analítica conectada"],
},
{
n: "Motor",
who: "Quieres crecer en serio",
price: "Mensual",
f: ["Ads + SEO + contenido", "Dashboard de resultados", "Optimización mensual", "Plataforma de IA incluida"],
featured: true,
},
{
n: "Partner",
who: "Quieres escalar",
price: "Retainer",
f: ["Full-stack de crecimiento", "Automatización a medida", "Prioridad y roadmap", "Acceso directo al equipo"],
},
];
export default function Packages() {
return (
<section id="paquetes" className="packages">
<div className="wrap">
<Reveal>
<p className="kicker">04 Paquetes</p>
<h2 className="packages__title">
Cada uno con un resultado. <span className="serif-em">No listas de tareas.</span>
</h2>
</Reveal>
<Reveal className="packages__grid" stagger={0.1}>
{packages.map((p) => (
<article
className={`pkg hoverable${p.featured ? " pkg--featured" : ""}`}
key={p.n}
data-cursor={p.featured ? "el más elegido" : "ver"}
>
{p.featured && <span className="pkg__tag">Más elegido</span>}
<header className="pkg__head">
<span className="pkg__price">{p.price}</span>
<h3 className="pkg__name">{p.n}</h3>
<p className="pkg__who">{p.who}</p>
</header>
<ul className="pkg__features">
{p.f.map((x) => (
<li key={x}>
<svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true">
<path
d="M4 10.5l3.5 3.5L16 6"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span>{x}</span>
</li>
))}
</ul>
<a className="pkg__cta hoverable" href="#contacto">
Empezar con {p.n}
<span aria-hidden="true"></span>
</a>
</article>
))}
</Reveal>
</div>
</section>
);
}

View file

@ -0,0 +1,97 @@
"use client";
/**
* PillMark the brand imagotype as a living system.
*
* Reconstructs the 4-bar capsule grid (violetblue gradient bar / ink bar +
* blue square / emerald bar) as inline SVG so each pill can be animated
* independently: it assembles on mount, then breathes. Used at hero scale and
* as a compact mark in dividers / footer.
*
* Decorative by default (aria-hidden). Pass `title` to expose it as an image.
*/
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
type Props = {
className?: string;
/** Animate assembly on mount (hero). */
animate?: boolean;
/** Continuous breathing after assembly. */
breathe?: boolean;
title?: string;
};
export default function PillMark({
className = "",
animate = false,
breathe = false,
title,
}: Props) {
const ref = useRef<SVGSVGElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const ctx = gsap.context(() => {
const bars = gsap.utils.toArray<SVGElement>(".pillmark__bar");
if (animate) {
gsap.set(bars, { transformOrigin: "center center" });
gsap.from(bars, {
scaleX: 0,
scaleY: 0.2,
opacity: 0,
duration: 1.1,
ease: "elastic.out(1, 0.75)",
stagger: { each: 0.09, from: "start" },
delay: 0.2,
});
}
if (breathe) {
bars.forEach((bar, i) => {
gsap.to(bar, {
y: i % 2 === 0 ? "+=6" : "-=6",
duration: 2.6 + i * 0.25,
ease: "sine.inOut",
repeat: -1,
yoyo: true,
delay: 1.2 + i * 0.1,
});
});
}
}, el);
return () => ctx.revert();
}, [animate, breathe]);
return (
<svg
ref={ref}
className={`pillmark ${className}`}
viewBox="0 0 1192 1287"
role={title ? "img" : "presentation"}
aria-hidden={title ? undefined : "true"}
aria-label={title}
>
{title ? <title>{title}</title> : null}
<defs>
<linearGradient id="pm-grad" x1="0" y1="0" x2="1192" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0" stopColor="#8b5cf6" />
<stop offset="1" stopColor="#3b82f6" />
</linearGradient>
</defs>
{/* top: full-width violet→blue gradient capsule */}
<rect className="pillmark__bar" x="0" y="0" width="1192" height="348.64" rx="174.32" fill="url(#pm-grad)" />
{/* middle row: ink wide capsule + blue square capsule */}
<rect className="pillmark__bar" x="0.02" y="480.64" width="725.02" height="348.64" rx="174.32" fill="#111827" />
<rect className="pillmark__bar" x="843" y="480.28" width="349.04" height="348.98" rx="174.32" fill="#3b82f6" />
{/* bottom: full-width emerald capsule */}
<rect className="pillmark__bar" x="0.04" y="938.21" width="1191.97" height="348.64" rx="174.32" fill="#10b981" />
</svg>
);
}

View file

@ -1,16 +1,21 @@
"use client"; "use client";
import { useEffect, useRef, ReactNode } from "react"; import { useEffect, useRef, type ReactNode } from "react";
import { gsap } from "gsap"; import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
/**
* Reveal generic on-scroll entrance with personality (slide + soft clip),
* not a plain fade-up. With `stagger`, animates direct children in sequence.
* Respects reduced-motion (content shown immediately).
*/
export default function Reveal({ export default function Reveal({
children, children,
className = "", className = "",
stagger = 0, stagger = 0,
y = 40, y = 44,
}: { }: {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
@ -22,13 +27,16 @@ export default function Reveal({
useEffect(() => { useEffect(() => {
const el = ref.current; const el = ref.current;
if (!el) return; if (!el) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const targets = stagger > 0 ? Array.from(el.children) : [el]; const targets = stagger > 0 ? Array.from(el.children) : [el];
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
gsap.from(targets, { gsap.from(targets, {
y, y,
opacity: 0, opacity: 0,
duration: 1, filter: "blur(8px)",
duration: 1.05,
ease: "power3.out", ease: "power3.out",
stagger, stagger,
scrollTrigger: { trigger: el, start: "top 85%" }, scrollTrigger: { trigger: el, start: "top 85%" },

View file

@ -0,0 +1,163 @@
"use client";
/**
* ServicesJourney pinned horizontal-scroll track of services.
*
* On desktop the section pins and the panels scroll sideways as you scroll
* down (GSAP ScrollTrigger pin + scrub), with a progress bar. On mobile / when
* the pin would be awkward, and under reduced-motion, it degrades to a normal
* vertical stack (no pin, panels just stack and reveal). Each panel is a real
* <article> so the content is accessible regardless of layout.
*/
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const services = [
{
n: "01",
t: "Web & Desarrollo",
d: "Sitios construidos en código, veloces y 100% editables. Sin plantillas, sin atajos: arquitectura que escala contigo.",
tags: ["Next.js", "Headless", "Core Web Vitals"],
},
{
n: "02",
t: "SEO & GEO",
d: "Posicionamiento técnico y de contenido — para Google y para los motores de IA. Reporting real, no humo.",
tags: ["Técnico", "Contenido", "Answer Engines"],
},
{
n: "03",
t: "Paid Ads",
d: "Google, Meta, TikTok y LinkedIn. Creatividad asistida por IA y pujas optimizadas hacia el resultado, no el clic.",
tags: ["Performance", "Creatividad IA", "Atribución"],
},
{
n: "04",
t: "Contenido",
d: "Piezas pensadas para rankear y para convertir. Producción a escala sin perder la voz de tu marca.",
tags: ["Editorial", "Vídeo", "Social"],
},
{
n: "05",
t: "Diseño & Marca",
d: "Identidad y dirección de arte con criterio. Sistemas de diseño que viven en producto, no en un PDF.",
tags: ["Identidad", "Design System", "Motion"],
},
{
n: "06",
t: "Automatización",
d: "Flujos que devuelven horas: captación de leads, reporting automático y operativa conectada de punta a punta.",
tags: ["Leads", "Reporting", "Ops"],
},
];
export default function ServicesJourney() {
const section = useRef<HTMLElement>(null);
const track = useRef<HTMLDivElement>(null);
const bar = useRef<HTMLSpanElement>(null);
useEffect(() => {
const sec = section.current;
const trk = track.current;
if (!sec || !trk) return;
const mm = gsap.matchMedia();
// Desktop with motion allowed: pin + horizontal scroll.
mm.add(
"(min-width: 900px) and (prefers-reduced-motion: no-preference)",
() => {
const distance = () => trk.scrollWidth - window.innerWidth;
const tween = gsap.to(trk, {
x: () => -distance(),
ease: "none",
scrollTrigger: {
trigger: sec,
start: "top top",
end: () => "+=" + distance(),
scrub: 0.6,
pin: true,
invalidateOnRefresh: true,
onUpdate: (self) => {
if (bar.current) {
bar.current.style.transform = `scaleX(${self.progress})`;
}
},
},
});
// Per-panel inner reveal as it slides into center.
gsap.utils.toArray<HTMLElement>(".sjourney__panel").forEach((panel) => {
gsap.from(panel.querySelectorAll(".sjourney__reveal"), {
y: 40,
opacity: 0,
duration: 0.6,
stagger: 0.06,
ease: "power3.out",
scrollTrigger: {
trigger: panel,
containerAnimation: tween,
start: "left 80%",
},
});
});
return () => {
if (bar.current) bar.current.style.transform = "scaleX(0)";
};
}
);
// Mobile / reduced-motion: simple stacked reveals.
mm.add("(max-width: 899px)", () => {
gsap.utils.toArray<HTMLElement>(".sjourney__panel").forEach((panel) => {
gsap.from(panel, {
y: 36,
opacity: 0,
duration: 0.7,
ease: "power3.out",
scrollTrigger: { trigger: panel, start: "top 88%" },
});
});
});
return () => mm.revert();
}, []);
return (
<section id="servicios" className="sjourney" ref={section} aria-label="Servicios">
<div className="sjourney__head wrap">
<p className="kicker">02 Qué hacemos</p>
<h2 className="sjourney__title">
Todo tu crecimiento, <span className="serif-em">un mismo sistema.</span>
</h2>
<div className="sjourney__progress" aria-hidden="true">
<span ref={bar} className="sjourney__progress-bar" />
</div>
</div>
<div className="sjourney__viewport">
<div className="sjourney__track" ref={track}>
{services.map((s) => (
<article className="sjourney__panel hoverable" key={s.t} data-cursor="explorar">
<span className="sjourney__n sjourney__reveal">{s.n}</span>
<h3 className="sjourney__panel-title sjourney__reveal">{s.t}</h3>
<p className="sjourney__panel-desc sjourney__reveal">{s.d}</p>
<ul className="sjourney__tags sjourney__reveal">
{s.tags.map((tag) => (
<li key={tag}>{tag}</li>
))}
</ul>
<span className="sjourney__pill" aria-hidden="true" />
</article>
))}
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,82 @@
"use client";
/**
* SiteHeader fixed top nav. Compacts (adds a glass backdrop) once you scroll
* past the hero, and includes a mobile menu toggle. The wordmark is real text
* ("feedback studios") with the brand mark inline.
*/
import { useEffect, useRef, useState } from "react";
import PillMark from "./PillMark";
const links = [
{ href: "#ventaja", label: "Ventaja" },
{ href: "#servicios", label: "Servicios" },
{ href: "#sectores", label: "Sectores" },
{ href: "#paquetes", label: "Paquetes" },
];
export default function SiteHeader() {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const headerRef = useRef<HTMLElement>(null);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 80);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
// Close the mobile menu on Escape.
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open]);
return (
<header
ref={headerRef}
className={`site-header${scrolled ? " is-scrolled" : ""}${open ? " is-open" : ""}`}
>
<div className="site-header__inner">
<a className="brand hoverable" href="#top" aria-label="Feedback Studios, inicio">
<PillMark className="brand__mark" title="Feedback Studios" />
<span className="brand__word">
feedback<span className="brand__word-2">studios</span>
</span>
</a>
<nav className="site-nav" aria-label="Principal">
<ul>
{links.map((l) => (
<li key={l.href}>
<a className="hoverable" href={l.href} onClick={() => setOpen(false)}>
{l.label}
</a>
</li>
))}
</ul>
<a className="btn btn--sm btn--primary hoverable" href="#contacto" onClick={() => setOpen(false)}>
<span>Hablemos</span>
</a>
</nav>
<button
type="button"
className="site-header__toggle hoverable"
aria-expanded={open}
aria-label={open ? "Cerrar menú" : "Abrir menú"}
onClick={() => setOpen((v) => !v)}
>
<span />
<span />
</button>
</div>
</header>
);
}

View file

@ -9,6 +9,12 @@ gsap.registerPlugin(ScrollTrigger);
export default function SmoothScroll() { export default function SmoothScroll() {
useEffect(() => { useEffect(() => {
// Respect reduced-motion: skip momentum scrolling entirely.
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
ScrollTrigger.refresh();
return;
}
const lenis = new Lenis({ const lenis = new Lenis({
duration: 1.15, duration: 1.15,
smoothWheel: true, smoothWheel: true,
@ -21,7 +27,22 @@ export default function SmoothScroll() {
gsap.ticker.add(ticker); gsap.ticker.add(ticker);
gsap.ticker.lagSmoothing(0); gsap.ticker.lagSmoothing(0);
// Anchor links should route through Lenis for the smooth glide.
const onClick = (e: MouseEvent) => {
const a = (e.target as HTMLElement).closest('a[href^="#"]');
if (!a) return;
const id = a.getAttribute("href");
if (!id || id === "#") return;
const target = document.querySelector(id);
if (target) {
e.preventDefault();
lenis.scrollTo(target as HTMLElement, { offset: -20 });
}
};
document.addEventListener("click", onClick);
return () => { return () => {
document.removeEventListener("click", onClick);
gsap.ticker.remove(ticker); gsap.ticker.remove(ticker);
lenis.destroy(); lenis.destroy();
}; };

File diff suppressed because it is too large Load diff

View file

@ -24,13 +24,25 @@ export default function RootLayout({
return ( return (
<html lang="es"> <html lang="es">
<head> <head>
{/* Satoshi (brand) + Instrument Serif (editorial accent) */}
<link rel="preconnect" href="https://api.fontshare.com" crossOrigin="" />
<link <link
rel="stylesheet" rel="stylesheet"
href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" href="https://api.fontshare.com/v2/css?f[]=satoshi@300,400,500,700,900&display=swap"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap"
/> />
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" /> <meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
<meta name="theme-color" content="#0a0a12" />
</head> </head>
<body> <body>
<a href="#main" className="skip-link">
Saltar al contenido
</a>
<SmoothScroll /> <SmoothScroll />
<Cursor /> <Cursor />
{children} {children}

View file

@ -1,87 +1,118 @@
import Iridescence from "./components/Iridescence";
import SiteHeader from "./components/SiteHeader";
import Hero from "./components/Hero"; import Hero from "./components/Hero";
import Marquee from "./components/Marquee";
import Reveal from "./components/Reveal"; import Reveal from "./components/Reveal";
import KineticText from "./components/KineticText";
import ServicesJourney from "./components/ServicesJourney";
import Packages from "./components/Packages";
import PillMark from "./components/PillMark";
const pillars = [ const advantages = [
{ k: "Más rápido", d: "Entregamos en días lo que una agencia tradicional tarda semanas." }, {
{ k: "Más medible", d: "Cada acción con su dashboard y su resultado, no humo." }, k: "Más rápido",
{ k: "Mejor coste", d: "Nuestra plataforma hace el trabajo pesado. Pagas resultado, no horas." }, metric: "días",
sub: "no semanas",
d: "Nuestra plataforma hace el trabajo pesado. Entregamos en días lo que una agencia tradicional tarda en arrancar.",
},
{
k: "Más medible",
metric: "100%",
sub: "trazable",
d: "Cada acción tiene su dashboard y su resultado. Sabes qué funciona y qué no — sin humo ni informes de relleno.",
},
{
k: "Mejor coste",
metric: "0",
sub: "horas vacías",
d: "No pagas por horas: pagas por resultado. La IA absorbe lo repetitivo, el equipo se concentra en la estrategia.",
},
]; ];
const services = [ const sectors = [
{ n: "01", t: "Web & Desarrollo", d: "Sitios en código, rápidos y editables. Sin plantillas." }, { t: "Clínicas estéticas", d: "Captación de pacientes, reputación y agenda llena." },
{ n: "02", t: "SEO", d: "Posicionamiento técnico y de contenido, con reporting real." }, { t: "Servicios profesionales", d: "Autoridad, leads cualificados y cierre." },
{ n: "03", t: "Paid Ads", d: "Google, Meta, TikTok, LinkedIn — creatividad + IA." }, { t: "E-commerce", d: "ROAS sostenible y catálogo que convierte." },
{ n: "04", t: "Contenido", d: "Piezas optimizadas para Google y para IA (GEO)." }, { t: "SaaS & Tech", d: "Activación, retención y growth medible." },
{ n: "05", t: "Diseño & Marca", d: "Identidad y dirección de arte con criterio." },
{ n: "06", t: "Automatización", d: "Flujos que ahorran horas: leads, reporting, operativa." },
];
const packages = [
{ n: "Sitio AI-native", who: "Necesitas web ya", f: ["Web editable en código", "SEO base + tracking", "Listo en tiempo récord"] },
{ n: "Base", who: "Estás arrancando", f: ["Fundamentos SEO", "Contenido inicial", "Analítica"] },
{ n: "Motor", who: "Quieres crecer", f: ["Ads + SEO + contenido", "Dashboard de resultados", "Optimización mensual"], featured: true },
{ n: "Partner", who: "Quieres escalar", f: ["Full-stack de crecimiento", "Automatización a medida", "Prioridad y plataforma"] },
]; ];
export default function Home() { export default function Home() {
return ( return (
<> <>
<header className="hd"> <Iridescence />
<span className="logo">feedback<span className="logo-dot">studios</span></span> <SiteHeader />
<nav className="hd-nav">
<a href="#servicios" className="hoverable">Servicios</a>
<a href="#paquetes" className="hoverable">Paquetes</a>
<a href="#contacto" className="hd-cta hoverable">Habla con nosotros</a>
</nav>
</header>
<main> <main id="main">
<span id="top" />
<Hero /> <Hero />
{/* MARQUEE */} <Marquee />
<div className="marquee" aria-hidden>
<div className="marquee-track">
{Array.from({ length: 2 }).map((_, i) => (
<span key={i}>
Web · SEO · Paid Ads · Contenido · Diseño · Automatización · IA ·&nbsp;
</span>
))}
</div>
</div>
{/* VENTAJA INJUSTA */} {/* VENTAJA INJUSTA — dark chapter */}
<section className="band ink"> <section id="ventaja" className="band band--ink advantage">
<div className="wrap"> <div className="wrap">
<Reveal> <Reveal>
<p className="kicker">01 Ventaja injusta</p> <p className="kicker kicker--light">01 Ventaja injusta</p>
<h2 className="band-h2"> <h2 className="band__title">
No alquilamos herramientas. <span className="grad">Construimos la máquina.</span> <KineticText as="span" text="No alquilamos herramientas." />{" "}
<KineticText
as="span"
className="band__title-grad"
text="Construimos la máquina."
highlight={[0, 2]}
/>
</h2> </h2>
</Reveal> </Reveal>
<Reveal className="grid3" stagger={0.12}>
{pillars.map((p) => ( <div className="advantage__grid">
<div className="card" key={p.k}> {advantages.map((a, i) => (
<h3>{p.k}</h3> <Reveal className="advantage__item" key={a.k} y={56}>
<p>{p.d}</p> <span className="advantage__index">0{i + 1}</span>
</div> <p className="advantage__metric">
{a.metric}
<span className="advantage__metric-sub">{a.sub}</span>
</p>
<h3 className="advantage__k">{a.k}</h3>
<p className="advantage__d">{a.d}</p>
</Reveal>
))} ))}
</Reveal> </div>
</div> </div>
</section> </section>
{/* SERVICIOS */} {/* SERVICIOS — horizontal journey */}
<section id="servicios" className="band ink"> <ServicesJourney />
<div className="wrap">
<Reveal> {/* SECTORES — deep dive */}
<p className="kicker">02 Qué hacemos</p> <section id="sectores" className="band sectors">
<h2 className="band-h2">Todo tu crecimiento, un mismo sistema.</h2> <div className="wrap sectors__wrap">
</Reveal> <div className="sectors__intro">
<Reveal className="svc-list" stagger={0.1}> <Reveal>
{services.map((s) => ( <p className="kicker">03 Profundidad por sector</p>
<div className="svc hoverable" key={s.t}> <h2 className="sectors__title">
<span className="svc-n">{s.n}</span> No hacemos marketing genérico.{" "}
<span className="svc-t">{s.t}</span> <span className="serif-em">Hablamos tu negocio.</span>
<span className="svc-d">{s.d}</span> </h2>
<p className="sectors__lead">
Construimos soluciones a medida de cada sector: el mismo motor de
IA, afinado con el contexto, el vocabulario y los KPIs que de verdad
importan en tu mercado.
</p>
<a className="link-arrow hoverable" href="#contacto">
¿Tu sector no está? Cuéntanoslo
<span aria-hidden="true"></span>
</a>
</Reveal>
</div>
<Reveal className="sectors__list" stagger={0.1}>
{sectors.map((s) => (
<div className="sector hoverable" key={s.t} data-cursor="ver">
<PillMark className="sector__mark" />
<div>
<h3 className="sector__t">{s.t}</h3>
<p className="sector__d">{s.d}</p>
</div>
</div> </div>
))} ))}
</Reveal> </Reveal>
@ -89,42 +120,62 @@ export default function Home() {
</section> </section>
{/* PAQUETES */} {/* PAQUETES */}
<section id="paquetes" className="band ink"> <Packages />
<div className="wrap">
<Reveal>
<p className="kicker">03 Paquetes</p>
<h2 className="band-h2">Cada uno con un resultado. No listas de tareas.</h2>
</Reveal>
<Reveal className="grid4" stagger={0.1}>
{packages.map((p) => (
<div className={"pkg hoverable" + (p.featured ? " featured" : "")} key={p.n}>
{p.featured && <span className="tag">Más popular</span>}
<h3>{p.n}</h3>
<p className="who">{p.who}</p>
<ul>{p.f.map((x) => <li key={x}>{x}</li>)}</ul>
</div>
))}
</Reveal>
</div>
</section>
{/* CTA */} {/* CTA — light iridescent close */}
<section id="contacto" className="cta"> <section id="contacto" className="cta">
<div className="mesh mesh-cta" aria-hidden> <Reveal className="wrap cta__wrap">
<span className="blob b1" /><span className="blob b3" /> <PillMark className="cta__mark" animate breathe />
</div> <h2 className="cta__title">
<Reveal className="wrap"> <KineticText as="span" text="¿Hablamos de tu" />{" "}
<h2 className="cta-h2">¿Hablamos de tu <span className="grad">crecimiento</span>?</h2> <KineticText
<p className="hero-sub">Cuéntanos dónde estás y te enseñamos cómo llegar al siguiente nivel.</p> as="span"
<a className="btn primary big hoverable" href="mailto:feedback.studios.design@gmail.com">Habla con nosotros</a> className="cta__title-grad"
text="crecimiento?"
highlight={[0, 0]}
/>
</h2>
<p className="cta__lead">
Cuéntanos dónde estás y te enseñamos, con datos, cómo llegar al
siguiente nivel. Respuesta en menos de 24h.
</p>
<a
className="btn btn--primary btn--lg hoverable"
href="mailto:feedback.studios.design@gmail.com"
data-cursor="escríbenos"
>
<span>Habla con nosotros</span>
</a>
<p className="cta__mail">
o escríbenos a{" "}
<a className="hoverable" href="mailto:feedback.studios.design@gmail.com">
feedback.studios.design@gmail.com
</a>
</p>
</Reveal> </Reveal>
</section> </section>
</main> </main>
<footer className="ft"> <footer className="site-footer">
<div className="wrap"> <div className="wrap site-footer__inner">
<span className="logo">feedback<span className="logo-dot">studios</span></span> <div className="site-footer__brand">
<span className="muted">© 2026 · Agencia de marketing AI-native · infraestructura propia</span> <span className="brand__word brand__word--lg">
feedback<span className="brand__word-2">studios</span>
</span>
<p className="site-footer__tag">
La agencia de marketing AI-native. Estrategia humana,
infraestructura propia.
</p>
</div>
<nav className="site-footer__nav" aria-label="Pie">
<a className="hoverable" href="#servicios">Servicios</a>
<a className="hoverable" href="#paquetes">Paquetes</a>
<a className="hoverable" href="#sectores">Sectores</a>
<a className="hoverable" href="mailto:feedback.studios.design@gmail.com">Contacto</a>
</nav>
<p className="site-footer__legal">
© 2026 Feedback Studios · Marketing AI-native con infraestructura propia
</p>
</div> </div>
</footer> </footer>
</> </>

999
package-lock.json generated Normal file
View file

@ -0,0 +1,999 @@
{
"name": "agency-web",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "agency-web",
"version": "0.1.0",
"dependencies": {
"gsap": "^3.12.5",
"lenis": "^1.1.14",
"next": "15.1.6",
"ogl": "^1.0.11",
"react": "19.0.0",
"react-dom": "19.0.0",
"split-type": "^0.3.4"
},
"devDependencies": {
"@types/node": "^22.10.7",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"typescript": "^5.7.3"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@next/env": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz",
"integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz",
"integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz",
"integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz",
"integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz",
"integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz",
"integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz",
"integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz",
"integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz",
"integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@types/node": {
"version": "22.19.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz",
"integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/react": {
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001799",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"optional": true,
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT",
"optional": true
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"optional": true,
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/gsap": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
},
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT",
"optional": true
},
"node_modules/lenis": {
"version": "1.3.23",
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz",
"integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==",
"license": "MIT",
"workspaces": [
"packages/*",
"playground",
"playground/*"
],
"funding": {
"type": "github",
"url": "https://github.com/sponsors/darkroomengineering"
},
"peerDependencies": {
"@nuxt/kit": ">=3.0.0",
"react": ">=17.0.0",
"vue": ">=3.0.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"react": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz",
"integrity": "sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==",
"deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.",
"license": "MIT",
"dependencies": {
"@next/env": "15.1.6",
"@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.1.6",
"@next/swc-darwin-x64": "15.1.6",
"@next/swc-linux-arm64-gnu": "15.1.6",
"@next/swc-linux-arm64-musl": "15.1.6",
"@next/swc-linux-x64-gnu": "15.1.6",
"@next/swc-linux-x64-musl": "15.1.6",
"@next/swc-win32-arm64-msvc": "15.1.6",
"@next/swc-win32-x64-msvc": "15.1.6",
"sharp": "^0.33.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/ogl": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz",
"integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==",
"license": "Unlicense"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.25.0"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
"semver": "^7.6.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.5",
"@img/sharp-darwin-x64": "0.33.5",
"@img/sharp-libvips-darwin-arm64": "1.0.4",
"@img/sharp-libvips-darwin-x64": "1.0.4",
"@img/sharp-libvips-linux-arm": "1.0.5",
"@img/sharp-libvips-linux-arm64": "1.0.4",
"@img/sharp-libvips-linux-s390x": "1.0.4",
"@img/sharp-libvips-linux-x64": "1.0.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
"@img/sharp-linux-arm": "0.33.5",
"@img/sharp-linux-arm64": "0.33.5",
"@img/sharp-linux-s390x": "0.33.5",
"@img/sharp-linux-x64": "0.33.5",
"@img/sharp-linuxmusl-arm64": "0.33.5",
"@img/sharp-linuxmusl-x64": "0.33.5",
"@img/sharp-wasm32": "0.33.5",
"@img/sharp-win32-ia32": "0.33.5",
"@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"license": "MIT",
"optional": true,
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/split-type": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/split-type/-/split-type-0.3.4.tgz",
"integrity": "sha512-otEk9vnD8qwfLsk3Lx0gz+qRkNIJCx0mlyL47ImP/DjMuV39d75Lpfwjn9fHteDRz0aoOblSzQjSNT9+Sswxcg==",
"license": "ISC"
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
},
"engines": {
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View file

@ -14,8 +14,10 @@
"gsap": "^3.12.5", "gsap": "^3.12.5",
"lenis": "^1.1.14", "lenis": "^1.1.14",
"next": "15.1.6", "next": "15.1.6",
"ogl": "^1.0.11",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0" "react-dom": "19.0.0",
"split-type": "^0.3.4"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.7.3", "typescript": "^5.7.3",