Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fb7c5cd
feat(home): customer logo proof strip with theme-aware rendering
sidneyswift May 19, 2026
f6b1a61
feat(home): reposition site around music-layer-for-agents narrative
sidneyswift May 19, 2026
270f686
refactor(home): cut copy hard — let headlines do the work
sidneyswift May 19, 2026
02d1639
refactor(home): kill decoration, reorder for narrative, add how-it-works
sidneyswift May 19, 2026
fda114a
refactor(home): stable, audience-led hero (Option A)
sidneyswift May 19, 2026
4ca1527
refactor(home): clean the hero, push proof into supporting sections
sidneyswift May 19, 2026
c955286
refactor(home): restore pixel-cycle headline, sharpen subhead for cla…
sidneyswift May 19, 2026
f32c06c
refactor(home): remove hero CTAs; let the demo be the primary action
sidneyswift May 19, 2026
baea003
copy(home): add "open source" to hero subhead
sidneyswift May 19, 2026
7f32c12
copy(home): tighten hero subhead with active transformation verb
sidneyswift May 19, 2026
135ead1
style(home): tighten hero subhead max-width
sidneyswift May 19, 2026
9e66c1f
copy(home): add "funds" to hero audience badge
sidneyswift May 19, 2026
f4ef972
feat(home): position Recoup as the open source AI lab for music
sidneyswift May 19, 2026
f13d229
refactor(home): hero sells the company, not just the product
sidneyswift May 19, 2026
e729fee
copy(home): two-line hero subhead — identity + product
sidneyswift May 19, 2026
7be77c3
copy(home): rewrite page-level narrative around the lab identity
sidneyswift May 19, 2026
536cfe1
copy(home): drop 'MCP-compatible' from architecture subhead
sidneyswift May 19, 2026
dd1c773
copy(home): swap badge and audience to fix grammar
sidneyswift May 19, 2026
8cb1e35
copy(home): logo-strip label 'Used by' -> 'Used by teams at'
sidneyswift May 19, 2026
3c224d4
refactor(home): reset positioning to 'AI agents for the music industry'
sidneyswift May 20, 2026
de93359
refactor(home): replace SaaS tiers with funnel-aligned pricing
sidneyswift May 20, 2026
c8a5024
copy(home): collapse hero subhead + drop duplicate demo caption
sidneyswift May 20, 2026
2fc7b8d
copy(home): drop the LIVE demo label
sidneyswift May 20, 2026
557afa6
copy(home): drop misleading 'Install in Claude' hero CTA
sidneyswift May 20, 2026
c9839c8
refactor(home): apply Nous + Water & Music positioning patterns
sidneyswift May 20, 2026
7784196
copy(home): kill 'production' (music-industry semantic collision)
sidneyswift May 20, 2026
a6cd37e
copy(home): commit to single identity — 'AI tools for the music indus…
sidneyswift May 20, 2026
88e6e2c
copy(home): 'AI research and tools for the music industry' (bolder)
sidneyswift May 20, 2026
811220f
copy(home): drop the hero badge
sidneyswift May 20, 2026
1ec511f
copy(home): 'AI research and agent tools for the music industry'
sidneyswift May 20, 2026
10cd783
style(home): bump hero H1 size
sidneyswift May 20, 2026
d8fd94b
copy(home): drop 'agent' from subtitle
sidneyswift May 20, 2026
48de7cc
feat(home): turn "In the box" into a real bento with visual previews
sidneyswift May 20, 2026
b103e37
refactor(home): cut repeats and fix orphan copy from scratchpad review
sidneyswift May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions app/api/customer-logos/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -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);

Comment on lines +7 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, locate the file
fd -t f "route.ts" -p "customer-logos"

Repository: recoupable/marketing

Length of output: 166


🏁 Script executed:

# Read the file to see the current implementation
cat -n "app/api/customer-logos/[slug]/route.ts"

Repository: recoupable/marketing

Length of output: 1924


🏁 Script executed:

# Check if Zod is already imported or used in this file
rg -i "zod" "app/api/customer-logos/[slug]/route.ts"

Repository: recoupable/marketing

Length of output: 46


🏁 Script executed:

# Check other API route files in the same directory structure to see patterns
find "app/api" -name "route.ts" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -20 {}'

Repository: recoupable/marketing

Length of output: 3279


🏁 Script executed:

# Check if Zod is used elsewhere in API routes
rg "from ['\"]zod['\"]" "app/api" -A 2

Repository: recoupable/marketing

Length of output: 221


🏁 Script executed:

# Check if there's a pattern of validation in the codebase for route params
rg "z\.object\|z\.parse" "app/api" -B 2 -A 2

Repository: recoupable/marketing

Length of output: 46


🏁 Script executed:

# Verify the coding guideline scope applies to API routes
grep -r "Use Zod" . --include="*.md" --include="*.ts" 2>/dev/null | head -5

Repository: recoupable/marketing

Length of output: 46


🏁 Script executed:

# Check how Zod is used in the subscribe route
cat -n "app/api/subscribe/route.ts" | head -40

Repository: recoupable/marketing

Length of output: 1543


🏁 Script executed:

# Search for any validation patterns with Zod in the codebase
rg "z\." "app/api" -A 1 | head -30

Repository: recoupable/marketing

Length of output: 575


🏁 Script executed:

# Check if there are other route files with params to understand the pattern
rg "params.*Promise" "app/api" --glob="*.ts"

Repository: recoupable/marketing

Length of output: 156


Validate route params with Zod before lookup.

This endpoint accepts slug from route params without validation. Per the coding guidelines ("Use Zod for all validation"), please add a Zod schema and parse params before the lookup.

🔧 Proposed fix
 import { CUSTOMER_LOGOS } from "`@/lib/customerLogos`";
 import { readFile } from "node:fs/promises";
 import path from "node:path";
+import { z } from "zod";
+
+const paramsSchema = z.object({
+  slug: z.string().min(1),
+});
 
 export const runtime = "nodejs";
 
 export async function GET(
   _request: Request,
   { params }: { params: Promise<{ slug: string }> },
 ) {
-  const { slug } = await params;
+  const { slug } = paramsSchema.parse(await params);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/customer-logos/`[slug]/route.ts around lines 7 - 13, Add Zod
validation for the route params inside the GET handler: import Zod, define a
schema like const ParamsSchema = z.object({ slug: z.string().min(1) }), then
replace the raw await params destructure with const { slug } =
ParamsSchema.parse(await params) (or use safeParse and return a 400 on failure).
Use the parsed slug for the CUSTOMER_LOGOS.find lookup and ensure any validation
failure returns an appropriate HTTP 400 response instead of proceeding.

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(/<svg\b([^>]*)>/, (_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 `<svg${nextAttrs}>`;
});

return new Response(normalizedSvg, {
headers: {
"Content-Type": "image/svg+xml; charset=utf-8",
"Cache-Control": "public, max-age=86400, stale-while-revalidate=604800",
},
});
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This catch block returns 404 for all errors, but since the slug is already validated against CUSTOMER_LOGOS above (line 12–15), a failure from readFile here indicates a filesystem/permission problem — not a missing resource. Return 500 for non-ENOENT errors to avoid masking server-side issues in production.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/customer-logos/[slug]/route.ts, line 53:

<comment>This catch block returns 404 for all errors, but since the slug is already validated against `CUSTOMER_LOGOS` above (line 12–15), a failure from `readFile` here indicates a filesystem/permission problem — not a missing resource. Return 500 for non-ENOENT errors to avoid masking server-side issues in production.</comment>

<file context>
@@ -0,0 +1,56 @@
+        "Cache-Control": "public, max-age=86400, stale-while-revalidate=604800",
+      },
+    });
+  } catch {
+    return new Response("Not found", { status: 404 });
+  }
</file context>

return new Response("Not found", { status: 404 });
}
Comment on lines +53 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n app/api/customer-logos/[slug]/route.ts

Repository: recoupable/marketing

Length of output: 1924


Distinguish between file-not-found and other filesystem errors in the catch block.

The current catch block returns 404 for all errors from readFile(), which masks real server-side failures like permission errors or I/O issues. Since the logo slug is already validated against CUSTOMER_LOGOS (line 12–15), an error at line 27 indicates a filesystem problem, not a missing entry—return 500 for these cases.

🔧 Proposed fix
-  } catch {
-    return new Response("Not found", { status: 404 });
+  } catch (error: unknown) {
+    const isNotFound =
+      typeof error === "object" &&
+      error !== null &&
+      "code" in error &&
+      (error as { code?: string }).code === "ENOENT";
+
+    return new Response(isNotFound ? "Not found" : "Internal server error", {
+      status: isNotFound ? 404 : 500,
+    });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch {
return new Response("Not found", { status: 404 });
}
} catch (error: unknown) {
const isNotFound =
typeof error === "object" &&
error !== null &&
"code" in error &&
(error as { code?: string }).code === "ENOENT";
return new Response(isNotFound ? "Not found" : "Internal server error", {
status: isNotFound ? 404 : 500,
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/customer-logos/`[slug]/route.ts around lines 53 - 55, The catch block
after the readFile() call currently returns a 404 for all errors; since the slug
is already validated against CUSTOMER_LOGOS, change the catch to distinguish
filesystem errors and return a 500 for non-missing-file failures (and only 404
if the error truly indicates ENOENT/not found). Update the catch around
readFile() in the route handler to inspect the thrown error (e.g., error.code or
instanceof where applicable), log the error, and return new Response("Internal
Server Error", { status: 500 }) for unexpected filesystem/IO/permission errors
while preserving a 404 only for an actual missing file.

}
146 changes: 146 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%);
}
Expand Down Expand Up @@ -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) */
Expand Down
Loading