diff --git a/app/api/customer-logos/[slug]/route.ts b/app/api/customer-logos/[slug]/route.ts new file mode 100644 index 0000000..5cf9f8c --- /dev/null +++ b/app/api/customer-logos/[slug]/route.ts @@ -0,0 +1,56 @@ +import { CUSTOMER_LOGOS } from "@/lib/customerLogos"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +export const runtime = "nodejs"; + +export async function GET( + _request: Request, + { params }: { params: Promise<{ slug: string }> }, +) { + const { slug } = await params; + const logo = CUSTOMER_LOGOS.find((entry) => entry.slug === slug); + + if (!logo) { + return new Response("Not found", { status: 404 }); + } + + const logoPath = path.join( + process.cwd(), + "design", + "logos", + "customers", + logo.fileName, + ); + + try { + const svg = await readFile(logoPath, "utf8"); + const normalizedSvg = svg.replace(/]*)>/, (_match, attrs) => { + let nextAttrs = attrs + .replace(/\swidth="[^"]*"/, "") + .replace(/\sheight="[^"]*"/, ""); + + if (logo.viewBoxOverride) { + if (/\sviewBox="[^"]*"/.test(nextAttrs)) { + nextAttrs = nextAttrs.replace( + /\sviewBox="[^"]*"/, + ` viewBox="${logo.viewBoxOverride}"`, + ); + } else { + nextAttrs = `${nextAttrs} viewBox="${logo.viewBoxOverride}"`; + } + } + + return ``; + }); + + return new Response(normalizedSvg, { + headers: { + "Content-Type": "image/svg+xml; charset=utf-8", + "Cache-Control": "public, max-age=86400, stale-while-revalidate=604800", + }, + }); + } catch { + return new Response("Not found", { status: 404 }); + } +} diff --git a/app/globals.css b/app/globals.css index 8d3ae6e..d62ff8d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -76,6 +76,32 @@ /* Dark sections — elevated in dark mode so they don't blend */ @layer components { + .customer-logo-image { + max-width: 100%; + object-fit: contain; + opacity: 0.72; + filter: none; + transition: + filter 200ms ease, + opacity 200ms ease; + } + + .customer-logo-image:hover { + opacity: 1; + filter: none; + } + + [data-theme="dark"] .customer-logo-image { + opacity: 0.9; + filter: invert(1) hue-rotate(180deg) brightness(1.08) contrast(1.05); + mix-blend-mode: screen; + } + + [data-theme="dark"] .customer-logo-image:hover { + opacity: 1; + filter: invert(1) hue-rotate(180deg) brightness(1.08) contrast(1.05); + } + .dark-section { background: radial-gradient(ellipse 80% 50% at 50% 0%, #111 0%, #060606 100%); } @@ -406,6 +432,126 @@ .poof-in { animation: poof-in 0.3s ease-out forwards; } + + .pixel-word-cycle { + display: inline-flex; + align-items: baseline; + justify-content: center; + min-height: 1em; + } + + .pixel-char { + position: relative; + display: inline-block; + min-width: 0.5em; + will-change: transform, filter, opacity; + } + + .pixel-char-main { + display: block; + transform-origin: center; + } + + .pixel-char-noise { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9em; + line-height: 1; + opacity: 0; + pointer-events: none; + color: color-mix(in srgb, var(--foreground) 90%, transparent); + clip-path: inset(var(--pixel-top, 18%) 0 var(--pixel-bottom, 18%) 0); + filter: contrast(1.6); + transform-origin: center; + } + + @keyframes pixel-char-out { + 0% { + opacity: 1; + transform: scale(1); + filter: blur(0px) contrast(1); + } + 100% { + opacity: 0.25; + transform: scale(1.03); + filter: blur(0.7px) contrast(1.9); + } + } + + @keyframes pixel-char-in { + 0% { + opacity: 0.25; + transform: scale(0.96); + filter: blur(0.7px) contrast(1.9); + } + 100% { + opacity: 1; + transform: scale(1); + filter: blur(0px) contrast(1); + } + } + + @keyframes pixel-noise-out { + 0% { + opacity: 0; + transform: translate(0, 0) scale(0.92); + } + 35% { + opacity: 0.85; + transform: translate(var(--pixel-x), 0) scale(1.08); + } + 100% { + opacity: 0; + transform: translate(var(--pixel-x-inverse), 0) scale(0.95); + } + } + + @keyframes pixel-noise-in { + 0% { + opacity: 0; + transform: translate(var(--pixel-x-inverse), 0) scale(0.95); + } + 45% { + opacity: 0.8; + transform: translate(var(--pixel-x), 0) scale(1.08); + } + 100% { + opacity: 0; + transform: translate(0, 0) scale(0.92); + } + } + + .pixel-word-cycle.pixel-swap-out .pixel-char-main { + animation: pixel-char-out 260ms steps(4, end) both; + animation-delay: var(--pixel-delay, 0ms); + } + + .pixel-word-cycle.pixel-swap-in .pixel-char-main { + animation: pixel-char-in 280ms steps(4, end) both; + animation-delay: var(--pixel-delay, 0ms); + } + + .pixel-word-cycle.pixel-swap-out .pixel-char-noise { + animation: pixel-noise-out 260ms steps(6, end) both; + animation-delay: var(--pixel-delay, 0ms); + } + + .pixel-word-cycle.pixel-swap-in .pixel-char-noise { + animation: pixel-noise-in 280ms steps(6, end) both; + animation-delay: var(--pixel-delay, 0ms); + } + + @media (prefers-reduced-motion: reduce) { + .pixel-word-cycle.pixel-swap-out .pixel-char-main, + .pixel-word-cycle.pixel-swap-in .pixel-char-main, + .pixel-word-cycle.pixel-swap-out .pixel-char-noise, + .pixel-word-cycle.pixel-swap-in .pixel-char-noise { + animation: none; + } + } } /* Fade-in animation (scroll-driven where supported) */ diff --git a/app/page.tsx b/app/page.tsx index 0407b44..46c2692 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,14 +4,13 @@ import { useState, useEffect, useRef } from "react"; import Link from "next/link"; import { siteConfig } from "@/lib/config"; import { HeroDemo } from "@/components/home/HeroDemo"; -import { ResearchCard } from "@/components/home/ResearchCard"; -import { ContentGrid } from "@/components/home/ContentGrid"; import { ArchitectureDiagram } from "@/components/home/ArchitectureDiagram"; +import { CUSTOMER_LOGOS } from "@/lib/customerLogos"; import { Check, ArrowUpRight, - User, - Users, + Package, + Code2, Building2, ArrowRight, } from "lucide-react"; @@ -48,54 +47,92 @@ function useStagger(n: number, ms = 130) { }; } -const LOGOS = ["Atlantic", "Rostrum", "300", "Warner", "Parlophone", "Fat Beats"]; - -const FLOATING_AGENTS = [ - { label: "Artist Research", x: "8%", y: "18%", delay: 200, size: "text-[12px]", opacity: 0.35 }, - { label: "Content Creator", x: "78%", y: "14%", delay: 400, size: "text-[12px]", opacity: 0.32 }, - { label: "Fan Segments", x: "7%", y: "44%", delay: 600, size: "text-[11px]", opacity: 0.28 }, - { label: "Release Report", x: "83%", y: "40%", delay: 300, size: "text-[12px]", opacity: 0.35 }, - { label: "Catalog Analyzer", x: "11%", y: "66%", delay: 800, size: "text-[11px]", opacity: 0.28 }, - { label: "Social Poster", x: "82%", y: "64%", delay: 500, size: "text-[11px]", opacity: 0.3 }, - { label: "Genre Trends", x: "19%", y: "28%", delay: 700, size: "text-[11px]", opacity: 0.25 }, - { label: "Comment Checker", x: "74%", y: "26%", delay: 900, size: "text-[11px]", opacity: 0.25 }, - { label: "Playlist Pitcher", x: "6%", y: "56%", delay: 1000, size: "text-[11px]", opacity: 0.28 }, - { label: "Fan Newsletter", x: "86%", y: "54%", delay: 450, size: "text-[11px]", opacity: 0.28 }, - { label: "Revenue Tracker", x: "16%", y: "74%", delay: 1100, size: "text-[11px]", opacity: 0.22 }, - { label: "A&R Scanner", x: "76%", y: "74%", delay: 650, size: "text-[11px]", opacity: 0.22 }, -]; - -function FloatingPills({ show }: { show: boolean }) { - return ( - - ); +const HERO_WORDS = ["Music", "Catalog", "Artist", "Fan"] as const; + +interface PixelCharFragment { + offsetX: number; + offsetY: number; + inverseOffsetX: number; + inverseOffsetY: number; + delayMs: number; + sliceTop: number; + sliceBottom: number; +} + +function buildPixelFragments(word: string): PixelCharFragment[] { + return word + .split("") + .map((_, index) => ({ + offsetX: Math.floor(Math.random() * 7) - 3, + offsetY: 0, + inverseOffsetX: Math.floor(Math.random() * 7) - 3, + inverseOffsetY: 0, + delayMs: index * 14 + Math.floor(Math.random() * 90), + sliceTop: Math.floor(Math.random() * 55), + sliceBottom: Math.floor(Math.random() * 55), + })); } export default function HomePage() { const [show, setShow] = useState(false); - useEffect(() => { const t = setTimeout(() => setShow(true), 100); return () => clearTimeout(t); }, []); + const [heroWordIndex, setHeroWordIndex] = useState(0); + const [heroDisplayWord, setHeroDisplayWord] = useState(HERO_WORDS[0]); + const [heroWordClassName, setHeroWordClassName] = useState("pixel-swap-in"); + const [heroPixelFragments, setHeroPixelFragments] = useState(() => + buildPixelFragments(HERO_WORDS[0]), + ); + const heroWordSwapTimeoutRef = useRef(null); + const heroWordShuffleIntervalRef = useRef(null); + const heroWordIndexRef = useRef(0); + useEffect(() => { + const t = setTimeout(() => setShow(true), 100); + return () => clearTimeout(t); + }, []); + useEffect(() => { + heroWordIndexRef.current = heroWordIndex; + }, [heroWordIndex]); + useEffect(() => { + const interval = window.setInterval(() => { + const currentWordIndex = heroWordIndexRef.current; + setHeroWordClassName("pixel-swap-out"); + setHeroPixelFragments(buildPixelFragments(HERO_WORDS[currentWordIndex])); + if (heroWordShuffleIntervalRef.current) { + window.clearInterval(heroWordShuffleIntervalRef.current); + } + heroWordShuffleIntervalRef.current = window.setInterval(() => { + setHeroPixelFragments(buildPixelFragments(HERO_WORDS[currentWordIndex])); + }, 65); + heroWordSwapTimeoutRef.current = window.setTimeout(() => { + if (heroWordShuffleIntervalRef.current) { + window.clearInterval(heroWordShuffleIntervalRef.current); + heroWordShuffleIntervalRef.current = null; + } + const nextIndex = (currentWordIndex + 1) % HERO_WORDS.length; + heroWordIndexRef.current = nextIndex; + setHeroWordIndex(nextIndex); + setHeroDisplayWord(HERO_WORDS[nextIndex]); + setHeroPixelFragments(buildPixelFragments(HERO_WORDS[nextIndex])); + setHeroWordClassName("pixel-swap-in"); + }, 300); + }, 2500); + + return () => { + window.clearInterval(interval); + if (heroWordShuffleIntervalRef.current) { + window.clearInterval(heroWordShuffleIntervalRef.current); + } + if (heroWordSwapTimeoutRef.current) { + window.clearTimeout(heroWordSwapTimeoutRef.current); + } + }; + }, []); - const research = useReveal(); - const content = useReveal(); + const stats = useReveal(); + const problem = useReveal(); const arch = useReveal(); + const how = useReveal(); + const layer = useReveal(); + const proof = useReveal(); const price = useReveal(); const priceC = useStagger(3); const cta = useReveal(); @@ -115,25 +152,43 @@ export default function HomePage() { WebkitMaskImage: "radial-gradient(ellipse 70% 60% at 50% 42%, black 20%, transparent 80%)", }} /> - -
-
- - - 40+ agent tools - - -

- Music +
+

+ + {heroDisplayWord.split("").map((char, index) => { + const fragment = heroPixelFragments[index]; + const fragmentStyle = { + "--pixel-delay": `${fragment?.delayMs ?? index * 25}ms`, + "--pixel-x": `${fragment?.offsetX ?? 0}px`, + "--pixel-y": `${fragment?.offsetY ?? 0}px`, + "--pixel-x-inverse": `${fragment?.inverseOffsetX ?? 0}px`, + "--pixel-y-inverse": `${fragment?.inverseOffsetY ?? 0}px`, + "--pixel-top": `${fragment?.sliceTop ?? 20}%`, + "--pixel-bottom": `${fragment?.sliceBottom ?? 20}%`, + } as React.CSSProperties; + + return ( + + {char} + + {char} + + + ); + })} + Intelligence

-

- Your record label, run by AI agents. +

+ AI research and tools for the music industry.

+
@@ -148,83 +203,90 @@ export default function HomePage() { 2. LOGOS ══════════════════════════════════════ */}
-
- Used by - {LOGOS.map(n => ( - {n} +
+ Used by teams at + {CUSTOMER_LOGOS.map((logo) => ( + + {logo.alt} + ))}
{/* ══════════════════════════════════════ - 3. RESEARCH — show don't tell + 3. STAT BAR — at-a-glance proof ══════════════════════════════════════ */} -
-
-
-
-

Research

-

- Know everything about any artist. -

-

- Streaming metrics, audience demographics, playlist placements, social traction, career history — pulled from 50+ sources and delivered in seconds. -

-

- Works from our chat, your AI assistant, or a single API call. -

-
-
- -
+
+
+
+ {[ + { v: "9", k: "labels using Recoup" }, + { v: "50+", k: "research sources" }, + { v: "40+", k: "agent tools" }, + { v: "22 / 2h", k: "videos at Fat Beats" }, + ].map((stat) => ( +
+

{stat.v}

+

{stat.k}

+
+ ))}
{/* ══════════════════════════════════════ - 5. CONTENT — show don't tell + 4. PROBLEM — state the gap first ══════════════════════════════════════ */}
-
-
-
- -
-
-

Content

-

- Create content that would take a team weeks. -

-

- Videos, images, captions, press copy — generated in batches from your catalog. Fat Beats created 22 finished videos in 2 hours with zero editing. -

-

- One prompt. Dozens of assets. Ready to post. -

-
+
+

The gap

+

+ Your team has Claude.
It still can't do the work. +

+ +
+ {[ + { k: "No music context", v: "Doesn\u2019t know your roster or catalog." }, + { k: "No safe access", v: "Can\u2019t touch Google Drive, royalty data, or distributors." }, + { k: "No music workflows", v: "Diligence, content, talent scouting \u2014 none of it built in." }, + ].map((item) => ( +
+

{item.k}

+

{item.v}

+
+ ))}
{/* ══════════════════════════════════════ - 6. ARCHITECTURE — we come to you + 5. ARCHITECTURE — here's how we fit in ══════════════════════════════════════ */}
-
- {["API", "MCP", "CLI"].map(m => ( +
+ {["Claude", "Codex", "Cursor", "Slack"].map(m => ( {m} ))}

- It works where you work. + Bring your own agent.
Recoup plugs in.

- Works with Claude, ChatGPT, Cursor, Windsurf, and any MCP-compatible agent. Or just use our chat. + One install. Available in every agent your team uses.

@@ -234,125 +296,337 @@ export default function HomePage() { {/* ══════════════════════════════════════ - 7. PRICING + 6. PULL QUOTE — real customer voice ══════════════════════════════════════ */} -
+
+
+

+ “Catalog diligence is one of the biggest pain points I have. If we can cut that into five minutes, this is really interesting.” +

+

+ Founder, Rostrum Records +

+
+
+ + + {/* ══════════════════════════════════════ + 7. HOW IT WORKS — 3 steps + ══════════════════════════════════════ */} +
+
+

How it works

+
+ {[ + { n: "01", k: "Install", v: "Add Recoup to Claude, Codex, or Cursor in one command." }, + { n: "02", k: "Wire it up", v: "Connect your roster, catalog, Google Drive, and the tools your team already pays for." }, + { n: "03", k: "Let it run", v: "Your agent researches artists, audits catalogs, builds content, and reports back." }, + ].map((step) => ( +
+

{step.n}

+

{step.k}

+

{step.v}

+
+ ))} +
+
+
+ + + {/* ══════════════════════════════════════ + 8. BENTO — six skills we ship + ══════════════════════════════════════ */} +
+
+

In the box

+

+ Six skills we ship
to your agent. +

+ +
+ {/* CONTENT — feature card (wide, dark) */} +
+
+ {[ + { bg: "from-violet-500/35 to-violet-700/20", icon: "▶" }, + { bg: "from-blue-500/35 to-blue-700/20", icon: "▶" }, + { bg: "from-amber-500/35 to-amber-700/20", icon: "■" }, + { bg: "from-rose-500/35 to-rose-700/20", icon: "▶" }, + { bg: "from-emerald-500/35 to-emerald-700/20", icon: "T" }, + { bg: "from-pink-500/35 to-pink-700/20", icon: "▶" }, + ].map((tile, i) => ( +
+ {tile.icon} +
+ ))} +
+
+

Content

+

Videos, images, captions, press.

+

One prompt, dozens of finished assets.

+
+
+ + {/* ARTIST CONTEXT */} +
+
+
+
+
+
+ Static Bloom + 12.4k monthly +
+
+
+ {["indie-pop", "bedroom-pop", "intimate"].map((tag) => ( + {tag} + ))} +
+
+
+
+

Artist context

+

A living folder per artist.

+

Bio, sound, comparables, and the rules each artist plays by — updated as your roster grows.

+
+
+ + {/* CATALOG DATA */} +
+
+
+
+ TrackSplitsRoyalty +
+ {[ + { t: "Hiccups", s: "50/30/20", r: "$1.2K" }, + { t: "Sundown", s: "70/30", r: "$840" }, + { t: "All In", s: "100", r: "$520" }, + ].map((row) => ( +
+ {row.t} + {row.s} + {row.r} +
+ ))} +
+
+
+

Catalog data

+

Splits, royalties, deal terms.

+

Every track, every right, in one place your agent can read.

+
+
+ + {/* RESEARCH */} +
+
+
+ {Array.from({ length: 50 }).map((_, i) => ( +
+ ))} +
+
+
+

Research

+

50+ sources, one call.

+

From DSP data through to fan psychology.

+
+
+ + {/* DILIGENCE */} +
+
+
+ {[ + { l: "Down", v: "$1.8M", c: "text-(--foreground)/45" }, + { l: "Base", v: "$2.4M", c: "text-(--foreground)" }, + { l: "Up", v: "$3.1M", c: "text-emerald-600 dark:text-emerald-400" }, + ].map((m) => ( +
+

{m.v}

+

{m.l}

+
+ ))} +
+
+
+

Diligence

+

Catalog diligence in minutes.

+

Weeks of analyst work → minutes.

+
+
+ + {/* GUARDRAILS */} +
+
+ {[ + { k: "Read roster", ok: true }, + { k: "Write to Drive", ok: true }, + { k: "Delete catalog", ok: false }, + { k: "Send invoices", ok: false }, + ].map((row) => ( +
+ {row.ok ? ( + + ) : ( + + + + )} + {row.k} + + {row.ok ? "auto" : "ask"} + +
+ ))} +
+
+

Guardrails

+

Respects each user's role.

+

Destructive actions need a human click.

+
+
+
+
+
+ + + {/* ══════════════════════════════════════ + 9. PROOF — we run our own labels + ══════════════════════════════════════ */} +
+
+

The proof

+

+ We run our own labels. +

+

+ Recoup Records and our artist Gatsby Grace use these same skills every day — to write release plans, audit catalogs, pitch playlists, and run the business. Every skill on this site has shipped against a real roster before it shipped here. +

+
+
+ + + {/* ══════════════════════════════════════ + 10. PRICING — three ways in + ══════════════════════════════════════ */} +

- Start free. Scale when ready. + Three ways in.

- No credit card required. Add credits anytime. + Install free skills · Pay per API call · Hire us to build it with you.

- {/* Free */} - + {/* SKILLS — free, open source, install yourself */} +
- +
-

Free

-

For artists

-

$0

+

Skills

+

Open source

+

Free

    - {["1 artist workspace", "Limited credits", "Limited models", "Community support"].map(f => ( + {["All skills, plugins, and agents", "Install in any agent", "MIT licensed", "Community support"].map(f => (
  • {f}
  • ))}
- Get started + Install free - {/* Pro */} - + {/* API — pay as you go, the conversion point */} +
- Popular + For builders
- +
-

Pro

-

For managers

-

$99/mo

+

API

+

Pay as you go

+

$20 credit pack

    - {["Full artist roster", "More credits", "All models", "Unlimited seats", "Priority support"].map(f => ( + {["Top up credits anytime", "Real-time music data", "Per-call transparency", "No subscription required", "Used by every skill"].map(f => (
  • {f}
  • ))}
- Start 30-day trial + Get an API key - {/* Partner */} - + {/* SERVICES — custom builds */} +
-

Partner

-

For labels

-

$499/mo

+

Services

+

White glove

+

Custom

    - {["Everything in Pro", "High-volume credits", "Customized agents", "Unlimited seats", "Dedicated support"].map(f => ( + {["Custom skills for your roster", "Embedded with your team", "Integrates with your stack", "3\u20136 month engagements", "Direct line to the founder"].map(f => (
  • {f}
  • ))}
- Get started + Talk to us
- - {/* Enterprise banner */} -
-
{/* ══════════════════════════════════════ - 8. CTA + 11. CTA — one command, music AI installed ══════════════════════════════════════ */}