From fb7c5cda9837b403dc57a6626859ec94444007d6 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Tue, 19 May 2026 00:58:28 -0400 Subject: [PATCH 01/34] feat(home): customer logo proof strip with theme-aware rendering - Add CUSTOMER_LOGOS metadata with per-logo viewBox crops in lib/customerLogos.ts - Serve customer SVGs through /api/customer-logos/[slug] with normalized root attrs - Switch the "Used by" strip to fixed-width logo slots so all logos sit on one row - Add .customer-logo-image CSS treatment: colorful by default in light mode, invert+hue-rotate in dark mode (preserves brand color while flipping black/white) - Enable outputFileTracingIncludes so the logo API route ships the SVGs in production Co-authored-by: Cursor --- app/api/customer-logos/[slug]/route.ts | 56 ++++++++++ app/globals.css | 146 +++++++++++++++++++++++++ app/page.tsx | 119 +++++++++++++++++++- lib/customerLogos.ts | 70 ++++++++++++ next.config.ts | 3 + 5 files changed, 388 insertions(+), 6 deletions(-) create mode 100644 app/api/customer-logos/[slug]/route.ts create mode 100644 lib/customerLogos.ts 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..f5a089d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,6 +7,7 @@ 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, @@ -48,7 +49,31 @@ function useStagger(n: number, ms = 130) { }; } -const LOGOS = ["Atlantic", "Rostrum", "300", "Warner", "Parlophone", "Fat Beats"]; +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), + })); +} const FLOATING_AGENTS = [ { label: "Artist Research", x: "8%", y: "18%", delay: 200, size: "text-[12px]", opacity: 0.35 }, @@ -91,7 +116,54 @@ function FloatingPills({ show }: { show: boolean }) { export default function HomePage() { const [show, setShow] = useState(false); + 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(); @@ -127,7 +199,31 @@ export default function HomePage() {

- 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

@@ -148,10 +244,21 @@ export default function HomePage() { 2. LOGOS ══════════════════════════════════════ */}
-
- Used by - {LOGOS.map(n => ( - {n} +
+ Used by + {CUSTOMER_LOGOS.map((logo) => ( + + {logo.alt} + ))}
diff --git a/lib/customerLogos.ts b/lib/customerLogos.ts new file mode 100644 index 0000000..e64a1ca --- /dev/null +++ b/lib/customerLogos.ts @@ -0,0 +1,70 @@ +export interface CustomerLogo { + slug: string; + fileName: string; + alt: string; + viewBoxOverride?: string; + className?: string; +} + +export const CUSTOMER_LOGOS: readonly CustomerLogo[] = [ + { + slug: "atlantic-records", + fileName: "atlantic-records.svg", + alt: "Atlantic Records logo", + viewBoxOverride: "240 240 330 330", + }, + { + slug: "rostrum-records", + fileName: "rostrum-records.svg", + alt: "Rostrum Records logo", + viewBoxOverride: "157 266 496 279", + }, + { + slug: "300-ent", + fileName: "300-ent.svg", + alt: "300 Entertainment logo", + viewBoxOverride: "249 304 312 202", + }, + { + slug: "warner-records", + fileName: "warner-records.svg", + alt: "Warner Records logo", + viewBoxOverride: "137 346 536 133", + }, + { + slug: "fatbeats-records", + fileName: "fatbeats-records.svg", + alt: "Fat Beats logo", + viewBoxOverride: "185 281 440 248", + }, + { + slug: "one-rpm", + fileName: "one-rpm.svg", + alt: "ONErpm logo", + viewBoxOverride: "240 280 330 250", + className: "max-h-4.5 sm:max-h-5.5", + }, + { + slug: "seeker-music", + fileName: "seeker-music.svg", + alt: "Seeker Music logo", + viewBoxOverride: "95 310 620 180", + className: "max-h-4.5 sm:max-h-5.5", + }, + { + slug: "duetti", + fileName: "duetti.svg", + alt: "Duetti logo", + viewBoxOverride: "106 360 597 90", + className: "max-h-4.5 sm:max-h-5.5", + }, + { + slug: "big-ass-kids", + fileName: "big-ass-kids.svg", + alt: "Big Ass Kids logo", + viewBoxOverride: "118 352 573 105", + className: "max-h-4.5 sm:max-h-5.5", + }, +] as const; + +export type CustomerLogoSlug = (typeof CUSTOMER_LOGOS)[number]["slug"]; diff --git a/next.config.ts b/next.config.ts index 675ac92..d9d88bd 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + outputFileTracingIncludes: { + "/api/customer-logos/[slug]": ["./design/logos/customers/**/*.svg"], + }, images: { remotePatterns: [ { From f6b1a61d74919770e38e3eb891dddbdf22500017 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Tue, 19 May 2026 01:06:12 -0400 Subject: [PATCH 02/34] feat(home): reposition site around music-layer-for-agents narrative Reframes the homepage from "AI chat that runs your label" to "music context, skills, and tools for the agents your team already uses." Hero - Badge: "40+ agent tools" -> "Works in Claude, Codex, Cursor" - Subcopy: "Your record label, run by AI agents." -> "Music agents for the AI your team already uses." New sections - Problem ("Your team has Claude. It still can't do the work."): three plain text blocks naming the real adoption gaps (no music context, no safe access, no music workflows). - Recoup Layer ("Recoup is the music layer for your agents."): 6 shadow-border cards covering artist context, catalog data, research APIs, content workflows, catalog diligence, approvals/guardrails. - Catalog Diligence ("Turn a deal room into an 80% diligence pass."): mirrors the strongest customer-pain story (label diligence) with a dashboard preview showing downside/base/upside cases and parsed-file callouts. Tightened copy - Research italic line now: "Available from Claude, Codex, Cursor, or any MCP-compatible agent." - Content headline: "Create the content you'd hire a team for." plus context-aware body. - Architecture pills: Claude / Codex / Cursor / Slack / MCP / API; headline: "Works inside the agent tools your team already uses."; body emphasises one-time install + MCP availability. - Pricing intro: "Start with one artist. Scale to a roster." and "Bring your own agent." - Enterprise banner reframed as "We set up your music AI layer with you." (setup services positioning). - Final CTA: "Install Recoup in your agent of choice." with secondary "Read the docs". No new components or icons; reuses the existing reveal hook, pixel display type, and shadow-border card pattern. Co-authored-by: Cursor --- app/page.tsx | 172 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 154 insertions(+), 18 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index f5a089d..2bd0031 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -165,6 +165,9 @@ export default function HomePage() { }; }, []); + const problem = useReveal(); + const layer = useReveal(); + const catalog = useReveal(); const research = useReveal(); const content = useReveal(); const arch = useReveal(); @@ -195,7 +198,7 @@ export default function HomePage() {
- 40+ agent tools + Works in Claude, Codex, Cursor

@@ -228,7 +231,7 @@ export default function HomePage() {

- Your record label, run by AI agents. + Music agents for the AI your team already uses.

@@ -265,9 +268,78 @@ export default function HomePage() { {/* ══════════════════════════════════════ - 3. RESEARCH — show don't tell + 3. PROBLEM — the gap agent tools leave + ══════════════════════════════════════ */} +
+
+
+

The gap

+

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

+

+ General agents don't know your catalog, your roster, or your workflows. They can't safely reach into the tools your team already pays for. They guess. Your team copies and pastes. Nothing gets shipped. +

+
+ +
+ {[ + { k: "No music context", v: "Agents don't know your roster, catalog, or release strategy. Every prompt starts from scratch." }, + { k: "No safe access", v: "Drives, distributors, royalty systems, social — agents can't reach them without IT review." }, + { k: "No music workflows", v: "Diligence, content batches, fan reports, A&R — the work that matters isn't built into general tools." }, + ].map((item) => ( +
+

{item.k}

+

{item.v}

+
+ ))} +
+
+
+ + + {/* ══════════════════════════════════════ + 4. RECOUP LAYER — what we add on top ══════════════════════════════════════ */}
+
+
+

The layer

+

+ Recoup is the music layer
for your agents. +

+

+ Install once. Recoup adds music-specific context, skills, and tools to whatever agent your team uses — and respects the permissions your team already has. +

+
+ +
+ {[ + { k: "Artist context", v: "A living folder per artist. Bio, audience, sound, comparables, sacred rules. Updated as your roster grows." }, + { k: "Catalog data", v: "Splits, royalties, deal terms, anomalies. Available to your agent the moment it needs them." }, + { k: "Research APIs", v: "Streaming metrics, audience demographics, playlist placements, social traction — 50+ sources, one call." }, + { k: "Content workflows", v: "Videos, images, captions, press — generated in batches from your catalog and brand." }, + { k: "Catalog diligence", v: "Drop in a deal room. Get an 80% pass before a human ever opens the file." }, + { k: "Approvals & guardrails", v: "Agents act inside the permissions each user already has. Destructive actions require a human click." }, + ].map((item) => ( +
+

{item.k}

+

{item.v}

+
+ ))} +
+
+
+ + + {/* ══════════════════════════════════════ + 5. RESEARCH — show don't tell + ══════════════════════════════════════ */} +
@@ -279,7 +351,7 @@ export default function HomePage() { 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. + Available from Claude, Codex, Cursor, or any MCP-compatible agent.

@@ -290,6 +362,67 @@ export default function HomePage() {
+ {/* ══════════════════════════════════════ + 6. CATALOG DILIGENCE — Benjy's pain point + ══════════════════════════════════════ */} +
+
+
+
+

Catalog diligence

+

+ Turn a deal room into an 80% diligence pass. +

+

+ Drop in royalty statements, summaries, and financials. Recoup parses every file, flags missing requirements, catches publisher and master conflicts, and models downside, base, and upside cases. +

+

+ Two-to-four week analyst work, ready for human review in minutes. +

+
+
+
+
+

Q4 publishing catalog

+ Ready +
+ +
+ {[ + { l: "Downside", v: "$1.8M", c: "text-(--foreground)/50" }, + { l: "Base", v: "$2.4M", c: "text-(--foreground)" }, + { l: "Upside", v: "$3.1M", c: "text-emerald-500" }, + ].map((m) => ( +
+

{m.l}

+

{m.v}

+
+ ))} +
+ +
+ {[ + { k: "147 songs parsed", ok: true }, + { k: "3 publisher share conflicts flagged", ok: false }, + { k: "12 missing recording ledgers", ok: false }, + { k: "Top track concentration: 14%", ok: true }, + ].map((row) => ( +
+ + {row.k} +
+ ))} +
+
+
+
+
+
+ + {/* ══════════════════════════════════════ 5. CONTENT — show don't tell ══════════════════════════════════════ */} @@ -302,10 +435,10 @@ export default function HomePage() {

Content

- Create content that would take a team weeks. + Create the content you'd hire a team for.

- Videos, images, captions, press copy — generated in batches from your catalog. Fat Beats created 22 finished videos in 2 hours with zero editing. + Videos, images, captions, press copy — generated in batches from the catalog and brand Recoup already knows. Fat Beats created 22 finished videos in 2 hours with zero editing.

One prompt. Dozens of assets. Ready to post. @@ -322,16 +455,16 @@ export default function HomePage() {

-
- {["API", "MCP", "CLI"].map(m => ( +
+ {["Claude", "Codex", "Cursor", "Slack", "MCP", "API"].map(m => ( {m} ))}

- It works where you work. + Works inside the agent tools
your team already uses.

- Works with Claude, ChatGPT, Cursor, Windsurf, and any MCP-compatible agent. Or just use our chat. + Install once. Recoup's skills, context, and tools become available in any MCP-compatible agent — without changing how your team works.

@@ -346,10 +479,10 @@ export default function HomePage() {

- Start free. Scale when ready. + Start with one artist. Scale to a roster.

- No credit card required. Add credits anytime. + No credit card required. Add credits anytime. Bring your own agent.

@@ -422,9 +555,9 @@ export default function HomePage() {

Enterprise

-

Built around your catalog and workflows.

+

We set up your music AI layer with you.

- Custom solutions tailored to your roster. Dedicated infrastructure, co-created campaigns, strategic support, and integrations designed for how your team works. + For labels and distributors. We install Recoup into your agent stack, wire up roster and catalog context, define team permissions, and ship the music-specific skills your team actually uses.

{["Custom agents", "Co-created IP", "Dedicated team", "White-glove onboarding"].map(tag => ( @@ -450,16 +583,19 @@ export default function HomePage() {