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 ( + + ); +} + const SITE: { label: string; view: View }[] = [ { label: "Overview", view: "home" }, { label: "Why it exists", view: "why" }, @@ -37,7 +146,8 @@ const COMMANDS: [string, string][] = [ export function SiteFooter({ onEnter }: { onEnter: (view: View) => void }) { return ( -