From 054188db4acaea1b21a45aa9a6883e5373801cfb Mon Sep 17 00:00:00 2001
From: srivtx
Date: Thu, 3 Sep 2026 12:08:08 +0530
Subject: [PATCH] =?UTF-8?q?feat(hero):=20the=20wool=20world=20=E2=80=94=20?=
=?UTF-8?q?felt=20mat,=20pompoms,=20and=20a=20desk=20that=20yawns?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- hero-fabric.tsx: a hand-cut felt mat behind the bot (turbulence-
displaced edge, felt grain + brushed nap, pastel dye washes, running
stitch) with three arc-wound yarn pompoms bobbing on transform-only
keyframes — the bot itself never changes shape, only gains a stage
- dark desk: washes go screen-blend so pastels glow without a green
field; the egg gets a lifted body gradient + firmer outline so the
mascot reads in front of the mat; the hover eye-fill stays a
day-desk tell
- footer: the mat runs under the footer as a hand-cut felt band with
two pompoms tucked in its corners
- global paper grain: one fixed fractal-noise layer, soft-light, the
felt world's fiber over every surface
- mascot life: after 30 quiet seconds the bot yawns (eyes snap to
slits, smile spreads, pops back awake on input); the desk agent's
thinking tell is a small wobbling yarn ball
- stamps: the receipt's CLEARED slams in on mount
- video transcript rewritten in plain English
- reduced-motion kill-switch covers every new animation
---
VIDEO_TRANSCRIPT.md | 142 ++++++++++----------
src/app/globals.css | 114 +++++++++++++++-
src/components/customs/bits.tsx | 29 +++-
src/components/customs/chat-events.tsx | 2 +-
src/components/customs/floating-agent.tsx | 17 ++-
src/components/customs/footer.tsx | 116 +++++++++++++++-
src/components/customs/hero-bot.tsx | 50 ++++++-
src/components/customs/hero-fabric.tsx | 156 ++++++++++++++++++++++
src/components/customs/landing.tsx | 10 +-
9 files changed, 550 insertions(+), 86 deletions(-)
create mode 100644 src/components/customs/hero-fabric.tsx
diff --git a/VIDEO_TRANSCRIPT.md b/VIDEO_TRANSCRIPT.md
index e5e407b..b098a18 100644
--- a/VIDEO_TRANSCRIPT.md
+++ b/VIDEO_TRANSCRIPT.md
@@ -1,118 +1,126 @@
-# Video transcript — the 5:00 pitch (recorded at submission)
+# Video transcript — the 5:00 pitch (plain-English version)
-Shot list assumes the live demo (with the deterministic seed). Timestamps are
-beats, not hard cuts. One take, screen + voice, no intro animation — the first
-10 seconds must carry the thesis.
+Same beats as before, but the spoken lines are plain and simple — short
+sentences, no jargon without a quick explanation. Read it like you talk.
+Timestamps are beats, not hard cuts. One take, screen + voice. The first
+10 seconds carry the whole idea.
-## 0:00–0:20 — the thesis
+## 0:00–0:20 — the big idea
**On screen:** the landing page, hero stamp.
-> "Razorpay's homepage already sells the future: AI agents, shopping in your
-> app, paying on their own. Everyone is building the buyer side of that
-> counter. Somebody has to build the other side — the part a payments company
-> needs before it lets an agent spend. Customs is both sides, in one repo."
+> "AI agents are starting to shop and pay on their own. Almost everyone is
+> building the buying side. Almost nobody is building the selling side —
+> the part a payments company needs before it lets a robot spend money.
+> We built both sides, in one project. It's called Customs."
-## 0:20–1:05 — the buyer side (the brief's named example, live)
+## 0:20–1:05 — the buyer side (live)
**On screen:** Playground. Type "headphones under 5000".
-> "Here's the agent. Every step it takes is visible — the search, the tool
-> calls, the wire format, and the price, recomputed server-side. I'll take the
-> earbuds and check out."
+> "Here is the buying agent. I type: 'headphones under 5000.' Watch every
+> step it takes — the search, the tools it calls, the messages it sends,
+> and the price. The website re-checks the price on its own server, so
+> nobody can trick it with a fake number. Now I say: 'checkout.'"
Type "checkout".
-> "The desk signs a mandate: an amount cap, the item list, an expiry, and my
-> trust tier — Ed25519 over canonical JSON. As the buyer's principal, I approve
-> the envelope, and only then does the agent bind and pay."
+> "Before it pays, the agent gets a permission slip. It says the most it
+> can spend, what it can buy, when it expires, and how much this user is
+> trusted. It is signed with a strong digital signature. I approve it.
+> Only then can the agent pay."
Approve → gate checklist → capture.
-> "The gate re-verified everything at bind time — signature, tier bounds,
-> live prices, quantities. Deterministic code, zero tokens: the LLM never
-> decides money. That's the line the brief draws, and it's the line we hold."
+> "And just before money moves, plain code — not AI — checks everything
+> again: the signature, the spending limit, the prices, the counts. The
+> AI can talk. Only this code can move money. That is the rule, and we
+> never break it."
-## 1:05–1:50 — the refusal beat (failure, handled)
+## 1:05–1:50 — the attack (failure, handled)
**On screen:** red-team panel → "Overspend the tier" → BLOCKED.
-> "Now the interesting part. I attack my own product. An unverified session
-> tries a ₹2,199 mouse against a ₹500 tier — refused, with a reason code, and
-> the attempt is hash-chained into the audit log. Twelve authored attacks,
-> each with its expected verdict. Run `make fuzz` — twelve out of twelve,
-> every time. Not a demo: CI."
+> "Now the best part. I attack my own product. A fake session tries to
+> buy a 2,199-rupee mouse with a 500-rupee limit. Blocked — with a short
+> code that says why. And the attempt is saved in a log that cannot be
+> secretly edited. We wrote twelve attacks like this. All twelve are
+> blocked, every time we test. Not just a demo — it runs in our build
+> check."
**On screen:** "Tamper the signature" → BLOCKED · SIGNATURE_INVALID.
-> "Tamper the mandate after signing — the signature check kills it. And this
-> ledger is tamper-evident: `make audit` flips one historical byte and the
-> chain breaks exactly there."
+> "What if someone edits the permission slip after it is signed? The
+> signature catches it. And if anyone changes one old entry in the log,
+> the chain breaks at that exact spot. Nothing can be changed in
+> secret."
-## 1:50–2:35 — the merchant side (the empty cell)
+## 1:50–2:35 — the merchant side
**On screen:** Control Room. The P&L meter counting up.
-> "Now walk around the counter. The Control Room is what the merchant — and
-> honestly, what Razorpay — needs to see. Agent GMV, revenue at list MDR, the
-> AI serving cost from metered tokens, and the net, live. At one million
-> payments a month this channel nets eight figures — the formula and every
-> assumption ship in the repo, regenerated by command, never hand-written."
+> "Now walk around the counter. This is the merchant's room — what the
+> shop, and honestly what Razorpay, would want to see. How much agents
+> are selling. What it costs to serve them. What is left as profit.
+> Live. At one million payments a month, this channel earns real money
+> — and every number here can be re-made with one command. Nothing was
+> typed in by hand."
**On screen:** approval queue → approve the ₹18,999 hold.
-> "Orders over ten thousand rupees hold for a human, any tier. I approve — it
-> captures, and it's replayable span-by-span. This is the brief's 'one
-> failure handled gracefully', except it's twelve, and they're in CI."
+> "One more rule. Any order above ten thousand rupees waits for a human.
+> I approve this one — it completes, step by step, right in front of
+> you."
-## 2:35–3:20 — the protocol matrix (why this generalizes)
+## 2:35–3:20 — works with any agent protocol
**On screen:** adapter switcher → MCP-style → wire chip expands.
-> "The same gate behind three protocols — direct, MCP-shaped, ACP-shaped.
-> Same tool implementations, same verdicts, different wire overhead — and the
-> ablation measures exactly that: verdict parity everywhere, byte cost
-> published. When ACP and friends settle, the adapter is a transport swap,
-> not a rewrite."
+> "Agent systems speak different languages — direct calls, MCP, ACP. Our
+> gate does not care. The same checks sit behind all three. We measured
+> all three: same decisions, different message sizes — and we published
+> the byte cost. If a new standard wins, we swap the transport. We do
+> not rewrite the product."
-## 3:20–4:05 — the proof layer (built for machine judges)
+## 3:20–4:05 — built to be checked by a machine
**On screen:** terminal — `make triage` scrolling.
-> "Fifteen hundred repos will be submitted. Triage will be machine-assisted,
-> so this repo is built to be judged by a machine: JUDGE.md maps every claim
-> to a file and a regenerate command. `make verify` is the same check CI runs
-> — zero dependencies, plain node. No number in this repo was typed by a
-> human, no link ships until it answers 200, and the engineering log records
-> every incident, each one turned into a test."
+> "Thousands of projects will be judged. Many will be checked by a
+> machine first. So we built this repo to be checked by a machine. One
+> file maps every claim to a file and a command. One command — `make
+> verify` — checks the whole repo with plain node, no setup. No number
+> was typed by a human. No link ships unless it works. And every bug we
+> ever hit became a test — forever."
-## 4:05–4:45 — honesty slide (scope, limits, next)
+## 4:05–4:45 — what we have NOT done (honesty slide)
**On screen:** the honest scope ledger, one card at a time.
-> "What's not done, on purpose, said plainly: the payment rail runs labeled
-> simulation until test keys are attached — the spike that flips it ships
-> ready. x402 is a declared stretch, not a promise. Voice, later. The demo
-> history is a deterministic seed through the real engine — no hand-written
-> ledger lines anywhere. Every limit is labeled in the product, not hidden in
-> a footnote."
+> "Now, what we have not done — said plainly. The payment rail runs in
+> clearly labeled simulation mode until Razorpay test keys are attached.
+> The code to flip it on is ready, and we already tested it against the
+> real API. The demo history comes from a fixed seed through the real
+> engine — no fake rows anywhere. Voice is for later. Every limit is
+> written on the screen, not hidden in a footnote."
## 4:45–5:00 — close
**On screen:** the manifest receipt, stamp lands.
-> "The buyer side everyone is building. The desk nobody has built. Both
-> sides, bounded, metered, replayable — and provable to a machine in sixty
+> "Everyone is building the buyer. Nobody built the desk. Both sides —
+> limited, measured, replayable — and provable to a machine in sixty
> seconds. Customs. Thank you."
---
-**Recording notes**
+**Recording notes (simple)**
-- Use the live seeded state (reset demo before recording) — numbers match the
- committed results.
-- Cursor + typing must be visible; the step chips are the transparency story.
-- Keep the terminal font large enough to read `make triage` output at 720p.
-- The refusal beat (1:05) is the emotional center — don't rush it.
-- Total: 5:00 hard limit. If over, cut the protocol matrix interior, never the
- refusal or the honesty slide.
+- Reset the demo before recording, so the numbers on screen match the
+ numbers in the repo.
+- Keep the cursor and your typing visible — watching the steps happen is
+ the whole trust story.
+- Make the terminal text big enough to read at 720p.
+- Do not rush the attack part at 1:05 — it is the heart of the video.
+- Hard limit: 5:00. If you run long, cut inside the protocol section —
+ never the attack, never the honesty slide.
diff --git a/src/app/globals.css b/src/app/globals.css
index 31856fe..88276ff 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -207,6 +207,21 @@ html.light {
html:not(.light) ::-webkit-calendar-picker-indicator {
filter: invert(1);
}
+ /* the paper grain — the felt world's fiber at a whisper over every
+ surface. One fixed layer, one raster (a 180px fractal-noise tile,
+ desaturated), soft-light blend so it reads on both desks without
+ tinting either. pointer-events: none — it is texture, never a
+ surface, and it never animates. */
+ body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ z-index: 80;
+ pointer-events: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.6' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
+ mix-blend-mode: soft-light;
+ opacity: 0.28;
+ }
}
@layer utilities {
@@ -657,7 +672,10 @@ html.light {
.hb-glint {
animation: hb-glint 9.5s ease-in-out infinite;
}
- .hero-bot:hover .hb-eye {
+ /* the hover eye-fill is a day-desk tell — on the night desk the
+ cleared sage reads as the green the desk avoids; dark hover lifts
+ the aurora instead (below) and leaves the eyes alone */
+ html.light .hero-bot:hover .hb-eye {
fill: var(--cleared);
}
@@ -667,18 +685,61 @@ html.light {
and a small warm-up on hover. No CSS transform ever touches this
element — that is the containment contract. */
.hb-aurora {
- opacity: 0.55;
+ /* the night ledger keeps the bot from going green-dominant — but
+ not colorless: the aurora sits at a third strength, a balanced
+ pastel sweep instead of the day desk's saturated wash */
+ opacity: 0.26;
transition: opacity 0.6s ease;
}
html.light .hb-aurora {
opacity: 0.38;
}
.hero-bot:hover .hb-aurora {
- opacity: 0.65;
+ opacity: 0.34;
}
html.light .hero-bot:hover .hb-aurora {
opacity: 0.46;
}
+ /* the felt mat's dye washes follow the same law: all four colors
+ stay present on the night desk — SCREEN-blended so the pastels
+ glow on the dark felt instead of drowning in it, the same trick
+ light does with opacity alone. Colorful, never the green field
+ the day desk wears. The pompoms stay loud. */
+ .hfx-wash {
+ transition: opacity 0.6s ease;
+ }
+ html:not(.light) .hfx-wash {
+ opacity: 0.5;
+ mix-blend-mode: screen;
+ }
+
+ /* the egg vs the mat, night desk: the day body gradient is dark ink
+ on a light field — over the near-black felt it has no contrast, so
+ the mascot dissolves into the fabric. The dark variant lifts the
+ fill and firms the outline, so the orb reads IN FRONT of the mat. */
+ html:not(.light) .hb-body-path {
+ fill: url(#hb-body-dark);
+ stroke-opacity: 0.45;
+ }
+
+ /* the idle yawn — after thirty quiet seconds the desk yawns: the
+ eyes close to happy slits (the whole eye unit squishes, so the
+ glints can never float free), and the smile stretches wide on a
+ slower ease — eyes snap, mouth spreads. Blink is suspended for
+ the yawn; on wake it resumes and the eyes pop open. Transitions
+ and one transform: nothing structural moves. */
+ .hb-smile {
+ transform-box: fill-box;
+ transform-origin: center;
+ transition: transform 0.55s cubic-bezier(0.45, 0, 0.25, 1);
+ }
+ .hero-bot.yawning .hb-smile {
+ transform: scaleX(1.55) translateY(1.6px);
+ }
+ .hero-bot.yawning .hb-eye-unit {
+ animation: none;
+ transform: scaleY(0.3);
+ }
/* the blush warms a touch when visited — the smallest tell of a
happy bot; a paint-only transition, nothing structural moves */
.hb-blush {
@@ -766,6 +827,49 @@ html.light {
}
}
+/* ---------------- the wool mat's pompoms ---------------- */
+/* transform/opacity only — the felt's filters are static paint and
+ rasterize once; the yarn just bobs, each ball on its own clock */
+@keyframes hb-pom-bob {
+ 0%,
+ 100% {
+ transform: translateY(0) rotate(var(--tilt, 0deg));
+ }
+ 50% {
+ transform: translateY(-6px) rotate(calc(var(--tilt, 0deg) + 2.5deg));
+ }
+}
+.hb-pom {
+ transform-box: fill-box;
+ transform-origin: center;
+ animation: hb-pom-bob 5.2s ease-in-out infinite;
+}
+.hb-pom2 {
+ animation-duration: 6.1s;
+ animation-delay: -1.8s;
+}
+.hb-pom3 {
+ animation-duration: 5.7s;
+ animation-delay: -3.1s;
+}
+
+/* the desk agent's thinking yarn — a small ball tilting side to side
+ while it works; transform-only, killed for reduced motion */
+@keyframes agent-yarn-wobble {
+ 0%,
+ 100% {
+ transform: rotate(-9deg) translateY(0);
+ }
+ 50% {
+ transform: rotate(9deg) translateY(-1.5px);
+ }
+}
+.agent-yarn {
+ transform-box: fill-box;
+ transform-origin: center;
+ animation: agent-yarn-wobble 0.85s ease-in-out infinite;
+}
+
/* ---------------- the customs bot's moves ---------------- */
@keyframes hb-float {
from {
@@ -863,7 +967,9 @@ html.light {
.hb-eye-unit,
.hb-eyes,
.hb-glint,
- .hb-shadow {
+ .hb-shadow,
+ .hb-pom,
+ .agent-yarn {
animation: none !important;
}
.pulse-dot::after {
diff --git a/src/components/customs/bits.tsx b/src/components/customs/bits.tsx
index 1d8b68b..420e5c0 100644
--- a/src/components/customs/bits.tsx
+++ b/src/components/customs/bits.tsx
@@ -114,7 +114,7 @@ export function LogoMark({ size = 28, className }: { size?: number; className?:
export type StampKind = "cleared" | "refused" | "held" | "ink" | "sim";
-const STAMP_STYLES: Record = {
+export const STAMP_STYLES: Record = {
cleared: "border-cleared/50 text-cleared bg-cleared/15",
refused: "border-refused/50 text-refused bg-refused/15",
held: "border-held/50 text-held bg-held/15",
@@ -127,14 +127,32 @@ export function Stamp({
children,
className,
animate = true,
+ thump = false,
+ thumpRotate = 0,
}: {
kind: StampKind;
children: ReactNode;
className?: string;
animate?: boolean;
+ /* the thump: the stamp slams in from above — scale 1.55 → 1 with a
+ small rotate-settle, the rubber-stamp tell. Fires once, in view. */
+ thump?: boolean;
+ thumpRotate?: number;
}) {
const reduce = useReducedMotion();
if (animate && !reduce) {
+ if (thump) {
+ return (
+
+ {children}
+
+ );
+ }
return (
);
}
- return {children};
+ return (
+
+ {children}
+
+ );
}
/* ---------------- chips & labels ---------------- */
diff --git a/src/components/customs/chat-events.tsx b/src/components/customs/chat-events.tsx
index 5c27b9f..558810f 100644
--- a/src/components/customs/chat-events.tsx
+++ b/src/components/customs/chat-events.tsx
@@ -393,7 +393,7 @@ function ReceiptCard({ event }: { event: Extract
replay span-by-span from the control room
- CLEARED
+ CLEARED
);
diff --git a/src/components/customs/floating-agent.tsx b/src/components/customs/floating-agent.tsx
index 58f6d71..a88b5f3 100644
--- a/src/components/customs/floating-agent.tsx
+++ b/src/components/customs/floating-agent.tsx
@@ -373,7 +373,22 @@ export function FloatingAgent({ view }: { view: View }) {
)
)}
- {busy &&
the desk is working…
}
+ {busy && (
+
+ {/* the thinking tell: a small yarn ball unwinding while
+ the desk works — butter yarn, the held state's warmth */}
+
+ the desk is working…
+
+ )}
{/* the composer: a sunken well, one arrow — nothing else */}
diff --git a/src/components/customs/footer.tsx b/src/components/customs/footer.tsx
index 03f9a17..c5f9c9c 100644
--- a/src/components/customs/footer.tsx
+++ b/src/components/customs/footer.tsx
@@ -5,15 +5,124 @@
* a left column (mark, one copyright line, the theme toggle and the
* source pill at its foot) and quiet link columns on the right —
* 13px links at half opacity that only reach full ink on approach.
- * No paragraphs, no stamps, no second bar. A footer says where things
- * are; it does not repeat the site.
+ * A footer says where things are; it does not repeat the site.
+ *
+ * The one decoration: the wool world runs under the footer — a thin
+ * felt band whose top edge is hand-cut (the hero mat's same recipe,
+ * condensed to a strip), two pompoms tucked into its corners, and a
+ * quiet colophon row of verdict stamps that thump in once, in view.
+ * All of it static paint + transform/opacity, all of it token-colored.
*/
+import type { CSSProperties } from "react";
import { LogoMark } from "./bits";
import { ThemeToggle } from "./theme";
import type { View } from "./shell";
const GITHUB = "https://github.com/srivtx/customs";
+/* the hand-cut top edge of the felt band — one irregular line, drawn
+ once, stretched by the layout (preserveAspectRatio none). The wobble
+ amplitude already varies, so the stretch reads as more hand. */
+const BAND_EDGE =
+ "M 0 14 C 60 8 120 16 190 12 C 260 8 320 18 400 13 C 470 9 540 17 620 12 C 700 7 770 18 850 13 C 920 9 990 16 1070 11 C 1150 7 1220 18 1300 13 C 1370 9 1450 17 1520 12 C 1560 9 1580 13 1600 11";
+
+/**
+ * YarnPom — a mini yarn ball tucked on the band's edge. Same
+ * construction as the hero pompoms at pocket scale: shaded sphere,
+ * wound arcs, one fluff displacement, bobbing on .hb-pom.
+ */
+function YarnPom({
+ id,
+ light,
+ base,
+ dark,
+ className,
+ tilt,
+}: {
+ id: string;
+ light: string;
+ base: string;
+ dark: string;
+ className?: string;
+ tilt: number;
+}) {
+ return (
+
+ );
+}
+
+/**
+ * FeltBand — the footer's rug edge. A 1600×30 strip: hand-cut top
+ * edge, pastel washes, grain, running stitch. The pompoms live outside
+ * the stretched svg so they stay round.
+ */
+function FeltBand() {
+ return (
+