From fbf5d9babe2363eba9258304cb4cc9016f912eb9 Mon Sep 17 00:00:00 2001 From: Bob Scully Date: Tue, 5 May 2026 09:51:07 -0700 Subject: [PATCH 01/49] Add v2 homepage focused on Bitcoin gift cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the placeholder Coming Soon page with a real homepage focused on Bitcoin gift cards. Five sections plus consumer + merchant CTAs and a brand-foot AGICASH wordmark. - /home → v2 (default) - /home-v1 → v1 archived for reference (noindex) - Sticky nav with Log in / Sign up - Cabinet Grotesk + Kode Mono + Teko typography - Square / BTCPay Server / Shopify partner logos - Pixelated wipe transitions on the hero carousel - Interactive Agicash → Cash App buy flow with slide / fade transitions Co-Authored-By: Claude Opus 4.7 (1M context) --- app/assets/btcpay-logo.svg | 1 + app/assets/shopify-logo.svg | 52 + app/assets/square-logo.svg | 11 + .../components/join-beta-button.tsx | 34 + .../homepage-v1/components/marketing-card.tsx | 24 + .../homepage-v1/components/section-label.tsx | 20 + .../homepage-v1/components/section.tsx | 30 + .../components/terminal-mockup.tsx | 95 + .../homepage-v1/components/wallet-mockup.tsx | 123 ++ app/features/homepage-v1/marketing-page.tsx | 22 + .../homepage-v1/sections/agentic-section.tsx | 28 + .../homepage-v1/sections/cta-section.tsx | 29 + .../sections/gift-cards-section.tsx | 74 + .../homepage-v1/sections/hero-section.tsx | 35 + .../sections/merchants-section.tsx | 68 + .../homepage-v1/sections/wallet-section.tsx | 28 + app/features/homepage-v1/styles.css | 196 +++ .../homepage/components/join-beta-button.tsx | 34 + .../homepage/components/marketing-card.tsx | 24 + .../homepage/components/marketing-nav.tsx | 40 + .../homepage/components/section-label.tsx | 20 + app/features/homepage/components/section.tsx | 30 + app/features/homepage/marketing-page.tsx | 28 + .../homepage/sections/buy-section.tsx | 250 +++ .../homepage/sections/cta-section.tsx | 30 + .../homepage/sections/footer-section.tsx | 62 + .../homepage/sections/hero-section.tsx | 290 ++++ .../homepage/sections/merchants-section.tsx | 87 + .../homepage/sections/send-section.tsx | 127 ++ .../homepage/sections/spend-section.tsx | 161 ++ .../homepage/sections/wallet-section.tsx | 83 + app/features/homepage/styles.css | 1545 +++++++++++++++++ app/routes/_public.home-v1.tsx | 18 + app/routes/_public.home.tsx | 71 +- 34 files changed, 3716 insertions(+), 54 deletions(-) create mode 100644 app/assets/btcpay-logo.svg create mode 100755 app/assets/shopify-logo.svg create mode 100644 app/assets/square-logo.svg create mode 100644 app/features/homepage-v1/components/join-beta-button.tsx create mode 100644 app/features/homepage-v1/components/marketing-card.tsx create mode 100644 app/features/homepage-v1/components/section-label.tsx create mode 100644 app/features/homepage-v1/components/section.tsx create mode 100644 app/features/homepage-v1/components/terminal-mockup.tsx create mode 100644 app/features/homepage-v1/components/wallet-mockup.tsx create mode 100644 app/features/homepage-v1/marketing-page.tsx create mode 100644 app/features/homepage-v1/sections/agentic-section.tsx create mode 100644 app/features/homepage-v1/sections/cta-section.tsx create mode 100644 app/features/homepage-v1/sections/gift-cards-section.tsx create mode 100644 app/features/homepage-v1/sections/hero-section.tsx create mode 100644 app/features/homepage-v1/sections/merchants-section.tsx create mode 100644 app/features/homepage-v1/sections/wallet-section.tsx create mode 100644 app/features/homepage-v1/styles.css create mode 100644 app/features/homepage/components/join-beta-button.tsx create mode 100644 app/features/homepage/components/marketing-card.tsx create mode 100644 app/features/homepage/components/marketing-nav.tsx create mode 100644 app/features/homepage/components/section-label.tsx create mode 100644 app/features/homepage/components/section.tsx create mode 100644 app/features/homepage/marketing-page.tsx create mode 100644 app/features/homepage/sections/buy-section.tsx create mode 100644 app/features/homepage/sections/cta-section.tsx create mode 100644 app/features/homepage/sections/footer-section.tsx create mode 100644 app/features/homepage/sections/hero-section.tsx create mode 100644 app/features/homepage/sections/merchants-section.tsx create mode 100644 app/features/homepage/sections/send-section.tsx create mode 100644 app/features/homepage/sections/spend-section.tsx create mode 100644 app/features/homepage/sections/wallet-section.tsx create mode 100644 app/features/homepage/styles.css create mode 100644 app/routes/_public.home-v1.tsx diff --git a/app/assets/btcpay-logo.svg b/app/assets/btcpay-logo.svg new file mode 100644 index 000000000..5d8592b71 --- /dev/null +++ b/app/assets/btcpay-logo.svg @@ -0,0 +1 @@ +btcpay3 \ No newline at end of file diff --git a/app/assets/shopify-logo.svg b/app/assets/shopify-logo.svg new file mode 100755 index 000000000..551074c35 --- /dev/null +++ b/app/assets/shopify-logo.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/square-logo.svg b/app/assets/square-logo.svg new file mode 100644 index 000000000..50468c551 --- /dev/null +++ b/app/assets/square-logo.svg @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/features/homepage-v1/components/join-beta-button.tsx b/app/features/homepage-v1/components/join-beta-button.tsx new file mode 100644 index 000000000..be68b108b --- /dev/null +++ b/app/features/homepage-v1/components/join-beta-button.tsx @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { Link, useLocation } from 'react-router'; +import { authQueryOptions } from '~/features/user/auth'; +import { cn } from '~/lib/utils'; + +type JoinBetaButtonProps = { + size?: 'default' | 'lg'; + className?: string; +}; + +export function JoinBetaButton({ + size = 'default', + className, +}: JoinBetaButtonProps) { + const location = useLocation(); + const { data: authState } = useQuery(authQueryOptions()); + const isLoggedIn = authState?.isLoggedIn ?? false; + + const sizeClasses = + size === 'lg' ? 'h-12 px-7 text-base' : 'h-10 px-5 text-sm'; + + return ( + + {isLoggedIn ? 'Go to Wallet' : 'Join Beta'} + + ); +} diff --git a/app/features/homepage-v1/components/marketing-card.tsx b/app/features/homepage-v1/components/marketing-card.tsx new file mode 100644 index 000000000..773086296 --- /dev/null +++ b/app/features/homepage-v1/components/marketing-card.tsx @@ -0,0 +1,24 @@ +import type { HTMLAttributes, ReactNode } from 'react'; +import { cn } from '~/lib/utils'; + +type MarketingCardProps = HTMLAttributes & { + children: ReactNode; +}; + +export function MarketingCard({ + children, + className, + ...props +}: MarketingCardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/app/features/homepage-v1/components/section-label.tsx b/app/features/homepage-v1/components/section-label.tsx new file mode 100644 index 000000000..bf0421aa8 --- /dev/null +++ b/app/features/homepage-v1/components/section-label.tsx @@ -0,0 +1,20 @@ +import { cn } from '~/lib/utils'; + +type SectionLabelProps = { + children: string; + className?: string; +}; + +export function SectionLabel({ children, className }: SectionLabelProps) { + return ( +
+ + {children} +
+ ); +} diff --git a/app/features/homepage-v1/components/section.tsx b/app/features/homepage-v1/components/section.tsx new file mode 100644 index 000000000..a842ce747 --- /dev/null +++ b/app/features/homepage-v1/components/section.tsx @@ -0,0 +1,30 @@ +import type { HTMLAttributes, ReactNode } from 'react'; +import { cn } from '~/lib/utils'; + +type SectionProps = HTMLAttributes & { + id?: string; + children: ReactNode; + hairline?: boolean; +}; + +export function Section({ + id, + children, + hairline = true, + className, + ...props +}: SectionProps) { + return ( +
+
{children}
+
+ ); +} diff --git a/app/features/homepage-v1/components/terminal-mockup.tsx b/app/features/homepage-v1/components/terminal-mockup.tsx new file mode 100644 index 000000000..8e69fabb2 --- /dev/null +++ b/app/features/homepage-v1/components/terminal-mockup.tsx @@ -0,0 +1,95 @@ +type Line = + | { kind: 'prompt'; user: string; cmd: string } + | { kind: 'output'; text: string; tone?: 'default' | 'muted' | 'accent' } + | { kind: 'blank' }; + +const lines: Line[] = [ + { kind: 'prompt', user: 'agent', cmd: 'mcp connect agicash' }, + { + kind: 'output', + text: 'connected · mint.agi.cash · budget: 5,000 sats', + tone: 'muted', + }, + { kind: 'blank' }, + { kind: 'prompt', user: 'agent', cmd: 'pay api.weather.dev --amount 21' }, + { + kind: 'output', + text: 'paid 21 sats · 89ms · ref: a1f3…7c2e', + tone: 'accent', + }, + { kind: 'blank' }, + { kind: 'prompt', user: 'agent', cmd: 'pay api.search.dev --amount 100' }, + { + kind: 'output', + text: 'paid 100 sats · 142ms · ref: 9b4d…0f81', + tone: 'accent', + }, + { kind: 'blank' }, + { kind: 'prompt', user: 'agent', cmd: 'budget' }, + { + kind: 'output', + text: 'remaining: 4,879 sats · resets in 23h', + tone: 'muted', + }, +]; + +export function TerminalMockup() { + return ( +
+
+
+ +
+        
+          {lines.map((line, i) => {
+            if (line.kind === 'blank') {
+              // biome-ignore lint/suspicious/noArrayIndexKey: static list
+              return 
 
; + } + if (line.kind === 'prompt') { + return ( + // biome-ignore lint/suspicious/noArrayIndexKey: static list +
+ + {line.user} + + + {' › '} + + {line.cmd} +
+ ); + } + const toneClass = + line.tone === 'accent' + ? 'text-[#7be3b8]' + : line.tone === 'muted' + ? 'text-[color:var(--mk-text-muted)]' + : 'text-[color:var(--mk-text-dim)]'; + return ( + // biome-ignore lint/suspicious/noArrayIndexKey: static list +
+ {line.text} +
+ ); + })} +
+
+
+ ); +} diff --git a/app/features/homepage-v1/components/wallet-mockup.tsx b/app/features/homepage-v1/components/wallet-mockup.tsx new file mode 100644 index 000000000..d7eb98352 --- /dev/null +++ b/app/features/homepage-v1/components/wallet-mockup.tsx @@ -0,0 +1,123 @@ +type Transaction = { + label: string; + meta: string; + amount: string; + direction: 'in' | 'out'; +}; + +const transactions: Transaction[] = [ + { + label: 'Pink Owl Coffee', + meta: '2026-04-29 · 14:32', + amount: '4,200', + direction: 'out', + }, + { + label: 'Received from Sasha', + meta: '2026-04-28 · 09:15', + amount: '25,000', + direction: 'in', + }, + { + label: 'PubKey NYC', + meta: '2026-04-26 · 19:48', + amount: '8,500', + direction: 'out', + }, + { + label: 'Lightning deposit', + meta: '2026-04-24 · 11:02', + amount: '50,000', + direction: 'in', + }, +]; + +export function WalletMockup() { + return ( +
+
+
+ bitcoin · spark +
+ + +
+
+ balance +
+
+ + 142,800 + + + sats + +
+
+ ≈ $145.62 +
+
+ +
+ + +
+ +
+
+
+ recent +
+
+ mint.agi.cash +
+
+
    + {transactions.map((tx) => ( +
  • +
    +
    + {tx.label} +
    +
    + {tx.meta} +
    +
    +
    + {tx.direction === 'in' ? '+' : '−'} + {tx.amount} +
    +
  • + ))} +
+
+
+ ); +} diff --git a/app/features/homepage-v1/marketing-page.tsx b/app/features/homepage-v1/marketing-page.tsx new file mode 100644 index 000000000..77cc306b9 --- /dev/null +++ b/app/features/homepage-v1/marketing-page.tsx @@ -0,0 +1,22 @@ +import { AgenticSection } from './sections/agentic-section'; +import { CtaSection } from './sections/cta-section'; +import { GiftCardsSection } from './sections/gift-cards-section'; +import { HeroSection } from './sections/hero-section'; +import { MerchantsSection } from './sections/merchants-section'; +import { WalletSection } from './sections/wallet-section'; +import './styles.css'; + +export function MarketingPage() { + return ( +
+
+ + + + + + +
+
+ ); +} diff --git a/app/features/homepage-v1/sections/agentic-section.tsx b/app/features/homepage-v1/sections/agentic-section.tsx new file mode 100644 index 000000000..a9efbced4 --- /dev/null +++ b/app/features/homepage-v1/sections/agentic-section.tsx @@ -0,0 +1,28 @@ +import { Section } from '../components/section'; +import { SectionLabel } from '../components/section-label'; +import { TerminalMockup } from '../components/terminal-mockup'; + +export function AgenticSection() { + return ( +
+
+
+ +
+
+ 03_agents +

+ MCP-native machine payments. +

+

+ Agents push payments per call. Per-service budgets. No credentials + shared. +

+
+ mcp · push-only · micropayments +
+
+
+
+ ); +} diff --git a/app/features/homepage-v1/sections/cta-section.tsx b/app/features/homepage-v1/sections/cta-section.tsx new file mode 100644 index 000000000..da40c3b7d --- /dev/null +++ b/app/features/homepage-v1/sections/cta-section.tsx @@ -0,0 +1,29 @@ +import { JoinBetaButton } from '../components/join-beta-button'; +import { MarketingCard } from '../components/marketing-card'; +import { SectionLabel } from '../components/section-label'; + +export function CtaSection() { + return ( +
+
+ +
+ for_users +
+

+ Be among the first. +

+

+ Agicash is in private beta. Get on the list. +

+
+ +
+
+
+
+ ); +} diff --git a/app/features/homepage-v1/sections/gift-cards-section.tsx b/app/features/homepage-v1/sections/gift-cards-section.tsx new file mode 100644 index 000000000..d1da34981 --- /dev/null +++ b/app/features/homepage-v1/sections/gift-cards-section.tsx @@ -0,0 +1,74 @@ +import { useEffect, useRef, useState } from 'react'; +import blockAndBean from '~/assets/gift-cards/blockandbean.agi.cash.webp'; +import pinkOwl from '~/assets/gift-cards/pinkowl.agi.cash.webp'; +import pubkey from '~/assets/gift-cards/pubkey.agi.cash.webp'; +import shack from '~/assets/gift-cards/shack.agi.cash.webp'; +import epicurean from '~/assets/gift-cards/theepicureantrader.agi.cash.webp'; +import { Section } from '../components/section'; +import { SectionLabel } from '../components/section-label'; + +const cards = [ + { src: pubkey, label: 'PubKey NYC' }, + { src: pinkOwl, label: 'Pink Owl Coffee' }, + { src: shack, label: 'The Shack' }, + { src: blockAndBean, label: 'Block & Bean' }, + { src: epicurean, label: 'The Epicurean Trader' }, +]; + +export function GiftCardsSection() { + const stackRef = useRef(null); + const [fanned, setFanned] = useState(false); + + useEffect(() => { + const node = stackRef.current; + if (!node) return; + const root = node.closest('.marketing'); + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + setFanned(true); + observer.disconnect(); + } + }, + { + root: root as Element | null, + threshold: 0.4, + }, + ); + observer.observe(node); + return () => observer.disconnect(); + }, []); + + return ( +
+
+
+ 02_gift_cards +

+ Closed-loop ecash for merchants. +

+

+ Issue gift cards and stack rewards on the same primitive. 1% fees, + instant Bitcoin settlement, scan-to-pay at any Square terminal. +

+
+ +
+ {cards.map((card) => ( +
+ {`${card.label} +
+ ))} +
+
+
+ ); +} diff --git a/app/features/homepage-v1/sections/hero-section.tsx b/app/features/homepage-v1/sections/hero-section.tsx new file mode 100644 index 000000000..0f8272a78 --- /dev/null +++ b/app/features/homepage-v1/sections/hero-section.tsx @@ -0,0 +1,35 @@ +import logoUrl from '~/assets/full_logo.png'; +import { JoinBetaButton } from '../components/join-beta-button'; + +export function HeroSection() { + return ( +
+
+ Agicash +
+ +
+
+

+ + Your Bitcoin{' '} + wallet for + + + Gift cards, Rewards and Agents + +

+ +

+ Bitcoin payments, built for humans and machines. Self-custodial + wallet, closed-loop merchant ecash, and MCP-native machine payments. +

+ +
+ +
+
+
+
+ ); +} diff --git a/app/features/homepage-v1/sections/merchants-section.tsx b/app/features/homepage-v1/sections/merchants-section.tsx new file mode 100644 index 000000000..a4b679c8b --- /dev/null +++ b/app/features/homepage-v1/sections/merchants-section.tsx @@ -0,0 +1,68 @@ +import { MarketingCard } from '../components/marketing-card'; +import { SectionLabel } from '../components/section-label'; + +export function MerchantsSection() { + return ( +
+
+ +
+ for_merchants +
+

+ For merchants. +

+

+ Offer Bitcoin gift cards and rewards, accept Lightning payments at + checkout, or enable agentic payments for your service. We'd + love to talk. +

+ +
+
+ + +
+ ); +} diff --git a/app/features/homepage-v1/sections/wallet-section.tsx b/app/features/homepage-v1/sections/wallet-section.tsx new file mode 100644 index 000000000..3b2f738cb --- /dev/null +++ b/app/features/homepage-v1/sections/wallet-section.tsx @@ -0,0 +1,28 @@ +import { Section } from '../components/section'; +import { SectionLabel } from '../components/section-label'; +import { WalletMockup } from '../components/wallet-mockup'; + +export function WalletSection() { + return ( +
+
+
+ 01_wallet +

+ Bitcoin payments. Non-custodial. +

+

+ Lightning and Cashu in one wallet. Your keys, your sats, your + contacts. +

+
+ lightning · cashu · spark +
+
+
+ +
+
+
+ ); +} diff --git a/app/features/homepage-v1/styles.css b/app/features/homepage-v1/styles.css new file mode 100644 index 000000000..d2b51fc9d --- /dev/null +++ b/app/features/homepage-v1/styles.css @@ -0,0 +1,196 @@ +@import url("https://api.fontshare.com/v2/css?f[]=cabinet-grotesk@400,500,600,700,800&display=swap"); + +.marketing { + --mk-bg: #04080f; + --mk-bg-card: rgba(12, 20, 36, 0.5); + --mk-text: #e8edf8; + --mk-text-dim: #8094b8; + --mk-text-muted: #455575; + --mk-brand: #00d4ff; + --mk-border: rgba(255, 255, 255, 0.06); + --mk-border-bright: rgba(255, 255, 255, 0.12); + --mk-font-display: "Cabinet Grotesk", -apple-system, BlinkMacSystemFont, + sans-serif; + --mk-font-mono: "Kode Mono", ui-monospace, monospace; + + background: var(--mk-bg); + color: var(--mk-text); + font-family: var(--mk-font-display); + font-feature-settings: "ss01", "ss02"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + height: 100dvh; + overflow-y: auto; + overflow-x: hidden; + + -ms-overflow-style: none; + scrollbar-width: none; +} + +.marketing::-webkit-scrollbar { + display: none; +} + +.marketing .font-mono { + font-family: var(--mk-font-mono); + font-feature-settings: "tnum"; +} + +.marketing a { + color: inherit; + text-decoration: none; +} + +@media (prefers-reduced-motion: no-preference) { + .marketing .stagger > * { + opacity: 0; + transform: translateY(8px); + animation: mk-fade-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; + } + .marketing .stagger > *:nth-child(1) { + animation-delay: 0ms; + } + .marketing .stagger > *:nth-child(2) { + animation-delay: 80ms; + } + .marketing .stagger > *:nth-child(3) { + animation-delay: 160ms; + } + .marketing .stagger > *:nth-child(4) { + animation-delay: 240ms; + } + .marketing .stagger > *:nth-child(5) { + animation-delay: 320ms; + } + .marketing .stagger > *:nth-child(6) { + animation-delay: 400ms; + } + + @keyframes mk-fade-up { + to { + opacity: 1; + transform: translateY(0); + } + } +} + +.marketing .mk-cta { + background: var(--mk-brand); + color: #04080f; + border: 1px solid var(--mk-brand); + transition: background-color 200ms ease, color 200ms ease; +} +.marketing .mk-cta:hover { + background: transparent; + color: var(--mk-brand); +} + +.marketing .mk-link { + color: var(--mk-text-muted); + transition: color 200ms ease; +} +.marketing .mk-link:hover { + color: var(--mk-text-dim); +} + +.marketing .mk-mailto { + color: var(--mk-text); + transition: color 200ms ease; +} +.marketing .mk-mailto:hover { + color: var(--mk-brand); +} + +/* ── Gift card stack ── */ +.marketing .gift-card-stack { + position: relative; + width: 100%; + height: 220px; + display: flex; + align-items: center; + justify-content: center; +} + +.marketing .gift-card-stack .stack-card { + position: absolute; + width: 170px; + transition: transform 700ms cubic-bezier(0.16, 1, 0.3, 1); + transform-origin: 50% 90%; + will-change: transform; +} + +.marketing .gift-card-stack .stack-card img { + display: block; + width: 100%; + height: auto; +} + +@media (min-width: 768px) { + .marketing .gift-card-stack { + height: 280px; + } + .marketing .gift-card-stack .stack-card { + width: 230px; + } +} + +/* Default: stacked deck */ +.marketing .gift-card-stack .stack-card:nth-child(1) { + transform: rotate(-3deg) translateY(-6px); + z-index: 1; +} +.marketing .gift-card-stack .stack-card:nth-child(2) { + transform: rotate(-1.5deg) translateY(-3px); + z-index: 2; +} +.marketing .gift-card-stack .stack-card:nth-child(3) { + transform: rotate(0deg); + z-index: 5; +} +.marketing .gift-card-stack .stack-card:nth-child(4) { + transform: rotate(1.5deg) translateY(3px); + z-index: 4; +} +.marketing .gift-card-stack .stack-card:nth-child(5) { + transform: rotate(3deg) translateY(6px); + z-index: 3; +} + +/* Fanned out — added by IntersectionObserver when section enters viewport */ +.marketing .gift-card-stack.fanned .stack-card:nth-child(1) { + transform: rotate(-15deg) translateX(-150px) translateY(-12px); +} +.marketing .gift-card-stack.fanned .stack-card:nth-child(2) { + transform: rotate(-7.5deg) translateX(-75px) translateY(-6px); +} +.marketing .gift-card-stack.fanned .stack-card:nth-child(3) { + transform: rotate(0deg) translateY(0); +} +.marketing .gift-card-stack.fanned .stack-card:nth-child(4) { + transform: rotate(7.5deg) translateX(75px) translateY(-6px); +} +.marketing .gift-card-stack.fanned .stack-card:nth-child(5) { + transform: rotate(15deg) translateX(150px) translateY(-12px); +} + +@media (min-width: 768px) { + .marketing .gift-card-stack.fanned .stack-card:nth-child(1) { + transform: rotate(-18deg) translateX(-210px) translateY(-18px); + } + .marketing .gift-card-stack.fanned .stack-card:nth-child(2) { + transform: rotate(-9deg) translateX(-105px) translateY(-9px); + } + .marketing .gift-card-stack.fanned .stack-card:nth-child(4) { + transform: rotate(9deg) translateX(105px) translateY(-9px); + } + .marketing .gift-card-stack.fanned .stack-card:nth-child(5) { + transform: rotate(18deg) translateX(210px) translateY(-18px); + } +} + +@media (prefers-reduced-motion: reduce) { + .marketing .gift-card-stack .stack-card { + transition: none; + } +} diff --git a/app/features/homepage/components/join-beta-button.tsx b/app/features/homepage/components/join-beta-button.tsx new file mode 100644 index 000000000..c98e50a4f --- /dev/null +++ b/app/features/homepage/components/join-beta-button.tsx @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { Link, useLocation } from 'react-router'; +import { authQueryOptions } from '~/features/user/auth'; +import { cn } from '~/lib/utils'; + +type JoinBetaButtonProps = { + size?: 'default' | 'lg'; + className?: string; +}; + +export function JoinBetaButton({ + size = 'default', + className, +}: JoinBetaButtonProps) { + const location = useLocation(); + const { data: authState } = useQuery(authQueryOptions()); + const isLoggedIn = authState?.isLoggedIn ?? false; + + const sizeClasses = + size === 'lg' ? 'h-12 px-7 text-base' : 'h-10 px-5 text-sm'; + + return ( + + {isLoggedIn ? 'Go to Wallet' : 'Get Started'} + + ); +} diff --git a/app/features/homepage/components/marketing-card.tsx b/app/features/homepage/components/marketing-card.tsx new file mode 100644 index 000000000..773086296 --- /dev/null +++ b/app/features/homepage/components/marketing-card.tsx @@ -0,0 +1,24 @@ +import type { HTMLAttributes, ReactNode } from 'react'; +import { cn } from '~/lib/utils'; + +type MarketingCardProps = HTMLAttributes & { + children: ReactNode; +}; + +export function MarketingCard({ + children, + className, + ...props +}: MarketingCardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/app/features/homepage/components/marketing-nav.tsx b/app/features/homepage/components/marketing-nav.tsx new file mode 100644 index 000000000..fd09ae60b --- /dev/null +++ b/app/features/homepage/components/marketing-nav.tsx @@ -0,0 +1,40 @@ +import { useQuery } from '@tanstack/react-query'; +import { Link } from 'react-router'; +import logoUrl from '~/assets/full_logo.png'; +import { authQueryOptions } from '~/features/user/auth'; + +export function MarketingNav() { + const { data: authState } = useQuery(authQueryOptions()); + const isLoggedIn = authState?.isLoggedIn ?? false; + + return ( +
+
+ + Agicash + + + +
+
+ ); +} diff --git a/app/features/homepage/components/section-label.tsx b/app/features/homepage/components/section-label.tsx new file mode 100644 index 000000000..dc7dc82e5 --- /dev/null +++ b/app/features/homepage/components/section-label.tsx @@ -0,0 +1,20 @@ +import { cn } from '~/lib/utils'; + +type SectionLabelProps = { + children: string; + className?: string; +}; + +export function SectionLabel({ children, className }: SectionLabelProps) { + return ( +
+ + {children} +
+ ); +} diff --git a/app/features/homepage/components/section.tsx b/app/features/homepage/components/section.tsx new file mode 100644 index 000000000..a842ce747 --- /dev/null +++ b/app/features/homepage/components/section.tsx @@ -0,0 +1,30 @@ +import type { HTMLAttributes, ReactNode } from 'react'; +import { cn } from '~/lib/utils'; + +type SectionProps = HTMLAttributes & { + id?: string; + children: ReactNode; + hairline?: boolean; +}; + +export function Section({ + id, + children, + hairline = true, + className, + ...props +}: SectionProps) { + return ( +
+
{children}
+
+ ); +} diff --git a/app/features/homepage/marketing-page.tsx b/app/features/homepage/marketing-page.tsx new file mode 100644 index 000000000..f9868a040 --- /dev/null +++ b/app/features/homepage/marketing-page.tsx @@ -0,0 +1,28 @@ +import { MarketingNav } from './components/marketing-nav'; +import { BuySection } from './sections/buy-section'; +import { CtaSection } from './sections/cta-section'; +import { FooterSection } from './sections/footer-section'; +import { HeroSection } from './sections/hero-section'; +import { MerchantsSection } from './sections/merchants-section'; +import { SendSection } from './sections/send-section'; +import { SpendSection } from './sections/spend-section'; +import { WalletSection } from './sections/wallet-section'; +import './styles.css'; + +export function MarketingPage() { + return ( +
+ +
+ + + + + + + +
+ +
+ ); +} diff --git a/app/features/homepage/sections/buy-section.tsx b/app/features/homepage/sections/buy-section.tsx new file mode 100644 index 000000000..2c0a29ba1 --- /dev/null +++ b/app/features/homepage/sections/buy-section.tsx @@ -0,0 +1,250 @@ +import { useEffect, useRef, useState } from 'react'; +import { Section } from '../components/section'; +import { SectionLabel } from '../components/section-label'; + +type State = 'idle' | 'loading' | 'review' | 'paid' | 'received'; + +const HEADERS: Record = { + idle: { brand: 'agicash', right: 'pink owl' }, + loading: { brand: 'cash app', right: 'pay $7.98' }, + review: { brand: 'cash app', right: 'pay $7.98' }, + paid: { brand: 'cash app', right: 'complete' }, + received: { brand: 'agicash', right: 'received' }, +}; + +const isCashApp = (s: State) => + s === 'loading' || s === 'review' || s === 'paid'; + +const NEXT: Record = { + idle: 'loading', + loading: 'review', // auto-advances after a brief delay + review: 'paid', + paid: 'received', + received: 'idle', +}; + +export function BuySection() { + const prevStateRef = useRef('idle'); + const [state, setState] = useState('idle'); + + // Determine transition kind based on whether we're crossing brands. + // Slide between Agicash ↔ Cash App; fade within Cash App's own steps. + const transitionClass = + isCashApp(prevStateRef.current) === isCashApp(state) + ? 'transition-fade' + : 'transition-slide'; + + useEffect(() => { + prevStateRef.current = state; + }, [state]); + + // Loading auto-advances quickly (no button); all other states wait for a + // click but fall back to advancing automatically after 6s of inactivity. + useEffect(() => { + const delay = state === 'loading' ? 1500 : 6000; + const t = window.setTimeout(() => { + setState(NEXT[state]); + }, delay); + return () => window.clearTimeout(t); + }, [state]); + + const handleAdvance = () => { + setState(NEXT[state]); + }; + + const header = HEADERS[state]; + + return ( +
+
+
+ 01_buy +

+ Buy a card in seconds. +

+

+ Buy a gift card with bitcoin, or use the Cash App to buy directly + from your bank account. +

+
+ lightning · cash app · settled instantly +
+
+ +
+
+
+ {header.brand} + {header.right} +
+
+
+ {state === 'idle' && } + {state === 'loading' && } + {state === 'review' && } + {state === 'paid' && } + {state === 'received' && } +
+ {state !== 'loading' && ( +
{renderCta(state, handleAdvance)}
+ )} +
+
+
+
+
+ ); +} + +function renderCta(state: State, onClick: () => void) { + const button = (label: string, primary = false) => ( + + ); + switch (state) { + case 'idle': + return button('Pay'); + case 'review': + return button('Confirm and pay', true); + case 'paid': + return button('Done', true); + case 'received': + return button('OK'); + default: + return null; + } +} + +function PayBody() { + return ( + <> +
+ + 10,000 + + $7.98 +
+
+
+ From + Cash App +
+
+ To + Pink Owl Coffee +
+
+ + ); +} + +function LoadingBody() { + return ( +
+
+ ); +} + +function ReviewBody() { + return ( +
+ +
Pay $7.98
+
+
+ Funding source + Debit 9687 +
+
+ To + lnbc100u…q97c +
+
+ Fees + Free +
+
+
+ ); +} + +function PaidBody() { + return ( +
+ +
You paid $7.98
+
+ ); +} + +function ReceivedBody() { + return ( + <> +
+ + 10,000 + + $7.98 +
+
+
Details
+
Today at 4:48 PM
+
+ + Bought +
+
+ + Pink Owl Coffee +
+
+ + ); +} diff --git a/app/features/homepage/sections/cta-section.tsx b/app/features/homepage/sections/cta-section.tsx new file mode 100644 index 000000000..370aea9f9 --- /dev/null +++ b/app/features/homepage/sections/cta-section.tsx @@ -0,0 +1,30 @@ +import { JoinBetaButton } from '../components/join-beta-button'; +import { MarketingCard } from '../components/marketing-card'; +import { SectionLabel } from '../components/section-label'; + +export function CtaSection() { + return ( +
+
+ +
+ for_users +
+

+ Be early. +

+

+ Agicash is in public beta. Join now to experience the best in + bitcoin. +

+
+ +
+
+
+
+ ); +} diff --git a/app/features/homepage/sections/footer-section.tsx b/app/features/homepage/sections/footer-section.tsx new file mode 100644 index 000000000..14b111eeb --- /dev/null +++ b/app/features/homepage/sections/footer-section.tsx @@ -0,0 +1,62 @@ +import { Link } from 'react-router'; +import discordLogo from '~/assets/discord_logo.svg'; +import githubLogo from '~/assets/github.svg'; +import nostrLogo from '~/assets/nostr_logo.svg'; +import xLogo from '~/assets/x_logo.svg'; + +const SOCIALS = [ + { + label: 'Discord', + href: 'https://discord.gg/e2TSCfXxhd', + src: discordLogo, + }, + { label: 'X', href: 'https://x.com/agi_cash', src: xLogo }, + { + label: 'Nostr', + href: 'https://njump.me/nprofile1qqsw3u8v7rz83txuy8nc0eth6rsqh4z935fs3t6ugwc7364gpzy5psce64r7c', + src: nostrLogo, + }, + { + label: 'GitHub', + href: 'https://github.com/MakePrisms/agicash', + src: githubLogo, + }, +]; + +export function FooterSection() { + return ( +
+
+
+ {SOCIALS.map((s) => ( + + + + ))} +
+ +
+ + Terms of Service + + + + Privacy Notice + + + © 2026 MakePrisms, Inc. All rights reserved. +
+
+ +
+ AGICASH +
+
+ ); +} diff --git a/app/features/homepage/sections/hero-section.tsx b/app/features/homepage/sections/hero-section.tsx new file mode 100644 index 000000000..97af764f9 --- /dev/null +++ b/app/features/homepage/sections/hero-section.tsx @@ -0,0 +1,290 @@ +import { useCallback, useEffect, useId, useRef, useState } from 'react'; +import blockAndBean from '~/assets/gift-cards/blockandbean.agi.cash.webp'; +import pinkOwl from '~/assets/gift-cards/pinkowl.agi.cash.webp'; +import pubkey from '~/assets/gift-cards/pubkey.agi.cash.webp'; +import shack from '~/assets/gift-cards/shack.agi.cash.webp'; +import epicurean from '~/assets/gift-cards/theepicureantrader.agi.cash.webp'; +import { JoinBetaButton } from '../components/join-beta-button'; + +const cards = [ + { src: pubkey, label: 'PUBKEY DC', location: 'WASHINGTON, D.C.' }, + { src: pinkOwl, label: 'PINK OWL COFFEE', location: 'CALIFORNIA' }, + { src: shack, label: 'THE SHACK', location: 'CALIFORNIA' }, + { src: blockAndBean, label: 'BLOCK & BEAN', location: 'CALIFORNIA' }, + { src: epicurean, label: 'EPICUREAN TRADER', location: 'CALIFORNIA' }, +]; + +function pad3(n: number) { + return String(n + 1).padStart(3, '0'); +} + +const PIXEL_COLS = 28; +const PIXEL_ROWS = 18; +const PIXEL_CELLS = PIXEL_COLS * PIXEL_ROWS; +const PIXEL_MAX_DELAY = 240; +const PIXEL_CELL_DURATION = 360; +const TRANSITION_END = PIXEL_MAX_DELAY + PIXEL_CELL_DURATION + 40; // ~640ms + +type Cell = { col: number; row: number; delay: number }; + +function makePixelCells(): Cell[] { + const cells: Cell[] = []; + for (let row = 0; row < PIXEL_ROWS; row++) { + for (let col = 0; col < PIXEL_COLS; col++) { + cells.push({ + col, + row, + delay: Math.floor(Math.random() * PIXEL_MAX_DELAY), + }); + } + } + return cells; +} + +function PixelWipe({ cells, src }: { cells: Cell[]; src: string }) { + const patternId = useId(); + return ( + + ); +} + +export function HeroSection() { + // imgIdx — drives the underlying (lags during transition; swaps at end) + // activeIdx — drives meta labels + active dot (updates immediately at start) + const [imgIdx, setImgIdx] = useState(0); + const [activeIdx, setActiveIdx] = useState(0); + const [paused, setPaused] = useState(false); + const [visible, setVisible] = useState(true); + const [wipe, setWipe] = useState<{ + id: number; + cells: Cell[]; + src: string; + } | null>(null); + const cardRef = useRef(null); + const activeIdxRef = useRef(activeIdx); + const transitioningRef = useRef(false); + const timersRef = useRef([]); + + activeIdxRef.current = activeIdx; + + const advanceTo = useCallback((nextIdx: number) => { + if (transitioningRef.current) return; + if (nextIdx === activeIdxRef.current) return; + transitioningRef.current = true; + + // Update meta + active dot IMMEDIATELY at start of transition. + setActiveIdx(nextIdx); + + // Mount wipe overlay rendering slices of the NEXT card. + // Cells fade in over ~600ms revealing the next image piece by piece. + const id = Date.now(); + setWipe({ + id, + cells: makePixelCells(), + src: cards[nextIdx]?.src ?? '', + }); + + // Once all cells are fully opaque (showing next image), atomically swap + // the underlying img src AND unmount the wipe in the same render batch + // — no flash because cells are already showing the new image. + const t = window.setTimeout(() => { + setImgIdx(nextIdx); + setWipe((w) => (w?.id === id ? null : w)); + transitioningRef.current = false; + }, TRANSITION_END); + + timersRef.current = [t]; + }, []); + + // Track tab visibility — when tab becomes visible, the auto-advance effect + // re-runs and the 5s timer resets fresh (no "catch-up" speed bursts). + useEffect(() => { + if (typeof document === 'undefined') return; + setVisible(!document.hidden); + const onVisibilityChange = () => setVisible(!document.hidden); + document.addEventListener('visibilitychange', onVisibilityChange); + return () => + document.removeEventListener('visibilitychange', onVisibilityChange); + }, []); + + // Auto-advance carousel — paused when hovered or tab hidden. + // First switch fires at 3.5s so the initial card doesn't feel stuck during + // hydration; subsequent switches every 5s. + useEffect(() => { + if (paused || !visible) return; + let intervalId: number | undefined; + const firstTimeout = window.setTimeout(() => { + advanceTo((activeIdxRef.current + 1) % cards.length); + intervalId = window.setInterval(() => { + advanceTo((activeIdxRef.current + 1) % cards.length); + }, 5000); + }, 3500); + return () => { + window.clearTimeout(firstTimeout); + if (intervalId !== undefined) window.clearInterval(intervalId); + }; + }, [paused, visible, advanceTo]); + + // Cleanup any pending timers on unmount + useEffect( + () => () => { + for (const t of timersRef.current) { + window.clearTimeout(t); + } + }, + [], + ); + + // Mouse parallax tilt (desktop only) + useEffect(() => { + if (typeof window === 'undefined') return; + if (!window.matchMedia('(hover: hover)').matches) return; + const card = cardRef.current; + const wrap = card?.parentElement; + if (!card || !wrap) return; + + let raf = 0; + const onMove = (e: MouseEvent) => { + const r = wrap.getBoundingClientRect(); + const x = ((e.clientX - r.left) / r.width - 0.5) * 2; + const y = ((e.clientY - r.top) / r.height - 0.5) * 2; + cancelAnimationFrame(raf); + raf = requestAnimationFrame(() => { + card.style.transform = `rotateY(${x * 8}deg) rotateX(${-y * 6}deg)`; + }); + }; + const onLeave = () => { + cancelAnimationFrame(raf); + card.style.transform = ''; + }; + + wrap.addEventListener('mousemove', onMove); + wrap.addEventListener('mouseleave', onLeave); + return () => { + wrap.removeEventListener('mousemove', onMove); + wrap.removeEventListener('mouseleave', onLeave); + cancelAnimationFrame(raf); + }; + }, []); + + // Meta uses the active (target) card so the labels update at transition START. + // The img element below uses imgIdx so it lags until the wipe completes. + const meta = cards[activeIdx]; + const imgCard = cards[imgIdx]; + + return ( +
+
+
+
+ agi.cash · public beta +
+ +

+ Bitcoin +
+ Gift Cards. +

+ +

+ Buy, send and spend bitcoin gift cards from your favorite merchants. + All on the most advanced Bitcoin wallet. +

+ +
+ +
+ +
+ buy · send · spend +
+
+ +
+
setPaused(true)} + onMouseLeave={() => setPaused(false)} + > + + + + + + + {pad3(activeIdx)} / {String(cards.length).padStart(3, '0')} + + btc gift card + {meta?.label} + {meta?.location} + +
+
+
+
+ ); +} diff --git a/app/features/homepage/sections/merchants-section.tsx b/app/features/homepage/sections/merchants-section.tsx new file mode 100644 index 000000000..8d83eebff --- /dev/null +++ b/app/features/homepage/sections/merchants-section.tsx @@ -0,0 +1,87 @@ +import btcpayLogoUrl from '~/assets/btcpay-logo.svg'; +import shopifyLogoUrl from '~/assets/shopify-logo.svg'; +import squareLogoUrl from '~/assets/square-logo.svg'; +import { MarketingCard } from '../components/marketing-card'; +import { SectionLabel } from '../components/section-label'; + +// Brand wordmarks rendered as inline SVG so they sit on the dark card cleanly +function SquareLogo() { + return ( + Square + ); +} + +function BTCPayServerLogo() { + return ( + BTCPay Server + ); +} + +function ShopifyLogo() { + return ( + Shopify + ); +} + +export function MerchantsSection() { + return ( +
+
+ +
+ for_merchants +
+

+ Run a store? Issue bitcoin gift cards today. +

+

+ Closed-loop bitcoin gift cards for your shop. No fees, instant + settlement, no new hardware. We'd love to talk. +

+ + +
+
supported systems
+
+ + + +
+
more coming soon
+
+
+
+
+ ); +} diff --git a/app/features/homepage/sections/send-section.tsx b/app/features/homepage/sections/send-section.tsx new file mode 100644 index 000000000..002f9d363 --- /dev/null +++ b/app/features/homepage/sections/send-section.tsx @@ -0,0 +1,127 @@ +import { useEffect, useRef, useState } from 'react'; +import { Section } from '../components/section'; +import { SectionLabel } from '../components/section-label'; + +const TRANSIT_TARGET_MS = 83; // simulated lightning latency +const TRANSIT_DURATION_MS = 1500; // matches transit-fly keyframes + +export function SendSection() { + const railRef = useRef(null); + const [played, setPlayed] = useState(false); + const [elapsedMs, setElapsedMs] = useState(0); + + useEffect(() => { + const node = railRef.current; + if (!node) return; + const root = node.closest('.marketing'); + const obs = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + setPlayed(true); + obs.disconnect(); + } + }, + { + root: root as Element | null, + // Trigger only when the section reaches the middle of the viewport — + // shrinks the viewport's effective bottom so it must scroll up further. + rootMargin: '0px 0px -45% 0px', + threshold: 0, + }, + ); + obs.observe(node); + return () => obs.disconnect(); + }, []); + + // Counter ticks 0 → 0.083s in lockstep with the card animation + useEffect(() => { + if (!played) { + setElapsedMs(0); + return; + } + const start = performance.now(); + let raf = 0; + const tick = (now: number) => { + const wall = now - start; + const t = Math.min(1, wall / TRANSIT_DURATION_MS); + setElapsedMs(Math.round(t * TRANSIT_TARGET_MS)); + if (t < 1) raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + }, [played]); + + const handleReplay = () => { + setPlayed(false); + requestAnimationFrame(() => { + requestAnimationFrame(() => setPlayed(true)); + }); + }; + + const formattedTime = (elapsedMs / 1000).toFixed(3); + + return ( +
+
+
+ 02_send +

+ Send over text or email. +

+

+ Share gift cards over text, email or on any social media platform. + Gift cards settle instantly and can immediately be spent at the + merchant's point of sale. +

+
+ text · email · qr +
+
+ +
+
+
+
+
+ origin +
+
+ @you +
+
+ +
+
+ +
+ transit · {formattedTime}s + +
+
+
+
+
+ ); +} diff --git a/app/features/homepage/sections/spend-section.tsx b/app/features/homepage/sections/spend-section.tsx new file mode 100644 index 000000000..2665a364a --- /dev/null +++ b/app/features/homepage/sections/spend-section.tsx @@ -0,0 +1,161 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { Section } from '../components/section'; +import { SectionLabel } from '../components/section-label'; + +const QR_SIZE = 21; +const FINDER_SIZE = 7; + +// Mulberry32 — small deterministic PRNG so the QR pattern is stable +function mulberry32(seed: number) { + let a = seed; + return () => { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = a; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +function buildQrPattern(size: number, seed: number): boolean[][] { + const grid: boolean[][] = Array.from({ length: size }, () => + Array(size).fill(false), + ); + + const drawFinder = (r0: number, c0: number) => { + for (let dr = 0; dr < FINDER_SIZE; dr++) { + for (let dc = 0; dc < FINDER_SIZE; dc++) { + const isOuter = dr === 0 || dr === 6 || dc === 0 || dc === 6; + const isInner = dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4; + grid[r0 + dr][c0 + dc] = isOuter || isInner; + } + } + }; + + drawFinder(0, 0); + drawFinder(0, size - FINDER_SIZE); + drawFinder(size - FINDER_SIZE, 0); + + const inFinderZone = (r: number, c: number) => + (r < FINDER_SIZE + 1 && c < FINDER_SIZE + 1) || + (r < FINDER_SIZE + 1 && c >= size - FINDER_SIZE - 1) || + (r >= size - FINDER_SIZE - 1 && c < FINDER_SIZE + 1); + + const rng = mulberry32(seed); + for (let r = 0; r < size; r++) { + for (let c = 0; c < size; c++) { + if (inFinderZone(r, c)) continue; + // Timing pattern on row 6 + col 6 + if (r === 6) { + grid[r][c] = c % 2 === 0; + continue; + } + if (c === 6) { + grid[r][c] = r % 2 === 0; + continue; + } + grid[r][c] = rng() < 0.46; + } + } + + return grid; +} + +function QrPattern() { + const grid = useMemo(() => buildQrPattern(QR_SIZE, 4242), []); + return ( + + ); +} + +export function SpendSection() { + const stageRef = useRef(null); + const [playing, setPlaying] = useState(false); + + useEffect(() => { + const node = stageRef.current; + if (!node) return; + const root = node.closest('.marketing'); + const obs = new IntersectionObserver( + ([entry]) => { + setPlaying(Boolean(entry?.isIntersecting)); + }, + { + root: root as Element | null, + rootMargin: '0px 0px -45% 0px', + threshold: 0, + }, + ); + obs.observe(node); + return () => obs.disconnect(); + }, []); + + return ( +
+
+
+ 03_spend +

+ Spend. Scan. Done. +

+

+ Pay with bitcoin at checkout. Scan the QR code with your Agicash + wallet. Bitcoin lands instantly at the merchant. No card networks or + bank intermediaries. +

+
+ settled instantly · 0% fee +
+
+ +
+
+
+ pay with bitcoin + pubkey dc +
+ +
+ +
+ +
+ + 5,634 + + $4.50 +
+ +
+ scan to pay + paid +
+
+
+
+
+ ); +} diff --git a/app/features/homepage/sections/wallet-section.tsx b/app/features/homepage/sections/wallet-section.tsx new file mode 100644 index 000000000..a672d5dd7 --- /dev/null +++ b/app/features/homepage/sections/wallet-section.tsx @@ -0,0 +1,83 @@ +import { Section } from '../components/section'; +import { SectionLabel } from '../components/section-label'; + +type Spec = { label: string; value: string; mono?: boolean }; + +const specs: Spec[] = [ + { label: 'Protocol', value: 'cashu · spark', mono: true }, + { label: 'Payments', value: 'lightning, lightning address', mono: true }, + { + label: 'Features', + value: 'cross-device sync, encrypted backups', + mono: true, + }, + { label: 'Login', value: 'email · google oauth', mono: true }, +]; + +export function WalletSection() { + return ( +
+
+
+ 04_wallet +

+ The most advanced bitcoin wallet. +

+

+ Agicash is a non-custodial bitcoin wallet built on secure enclaves + to enable cross-device sync and email login. Built on the latest + bitcoin payment protocols. +

+ +
+
+
+ bitcoin +
+
bob@agi.cash
+
+ 142,800 +
+
$145.62
+
+
+ Receive + Buy +
+ Send +
+
+
+
+
+
+ +
+

+ The most advanced bitcoin wallet. +

+

+ Agicash is a non-custodial bitcoin wallet built on secure enclaves + to enable cross-device sync and email login. Built on the latest + bitcoin payment protocols. +

+ +
+
specification
+ {specs.map((s) => ( +
+ {s.label} +
+ ))} +
+
+
+
+ ); +} diff --git a/app/features/homepage/styles.css b/app/features/homepage/styles.css new file mode 100644 index 000000000..1c1f1d040 --- /dev/null +++ b/app/features/homepage/styles.css @@ -0,0 +1,1545 @@ +@import url("https://api.fontshare.com/v2/css?f[]=cabinet-grotesk@400,500,600,700,800&display=swap"); + +.marketing { + --mk-bg: #04080f; + --mk-bg-card: rgba(12, 20, 36, 0.5); + --mk-text: #e8edf8; + --mk-text-dim: #8094b8; + --mk-text-muted: #455575; + --mk-brand: #00d4ff; + --mk-border: rgba(255, 255, 255, 0.06); + --mk-border-bright: rgba(255, 255, 255, 0.12); + --mk-font-display: "Cabinet Grotesk", -apple-system, BlinkMacSystemFont, + sans-serif; + --mk-font-mono: "Kode Mono", ui-monospace, monospace; + --mk-font-numeric: "Teko", sans-serif; + + background: var(--mk-bg); + color: var(--mk-text); + font-family: var(--mk-font-display); + font-feature-settings: "ss01", "ss02"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + height: 100dvh; + overflow-y: auto; + overflow-x: hidden; + + -ms-overflow-style: none; + scrollbar-width: none; +} + +.marketing::-webkit-scrollbar { + display: none; +} + +.marketing .font-mono { + font-family: var(--mk-font-mono); + font-feature-settings: "tnum"; +} + +.marketing a { + color: inherit; + text-decoration: none; +} + +@media (prefers-reduced-motion: no-preference) { + .marketing .stagger > * { + opacity: 0; + transform: translateY(8px); + animation: mk-fade-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; + } + .marketing .stagger > *:nth-child(1) { + animation-delay: 0ms; + } + .marketing .stagger > *:nth-child(2) { + animation-delay: 80ms; + } + .marketing .stagger > *:nth-child(3) { + animation-delay: 160ms; + } + .marketing .stagger > *:nth-child(4) { + animation-delay: 240ms; + } + .marketing .stagger > *:nth-child(5) { + animation-delay: 320ms; + } + .marketing .stagger > *:nth-child(6) { + animation-delay: 400ms; + } + + @keyframes mk-fade-up { + to { + opacity: 1; + transform: translateY(0); + } + } +} + +.marketing .mk-cta { + background: var(--mk-brand); + color: #04080f; + border: 1px solid var(--mk-brand); + transition: background-color 200ms ease, color 200ms ease; +} +.marketing .mk-cta:hover { + background: transparent; + color: var(--mk-brand); +} + +.marketing .mk-link { + color: var(--mk-text-muted); + transition: color 200ms ease; +} +.marketing .mk-link:hover { + color: var(--mk-text-dim); +} + +.marketing .mk-mailto { + color: var(--mk-text); + transition: color 200ms ease; +} +.marketing .mk-mailto:hover { + color: var(--mk-brand); +} + +/* ────────────────────────────────────────── */ +/* STICKY NAV */ +/* ────────────────────────────────────────── */ + +.marketing .marketing-nav { + position: sticky; + top: 0; + z-index: 50; + width: 100%; + background: rgba(4, 8, 15, 0.78); + backdrop-filter: saturate(140%) blur(14px); + -webkit-backdrop-filter: saturate(140%) blur(14px); + border-bottom: 1px solid var(--mk-border); +} + +.marketing .marketing-nav-inner { + width: 100%; + padding: 14px 20px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +@media (min-width: 768px) { + .marketing .marketing-nav-inner { + padding: 16px 32px; + } +} + +.marketing .marketing-nav-logo-link { + display: inline-flex; + align-items: center; +} + +.marketing .marketing-nav-logo { + height: 22px; + width: auto; + display: block; + opacity: 0.92; +} + +@media (min-width: 768px) { + .marketing .marketing-nav-logo { + height: 26px; + } +} + +.marketing .marketing-nav-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.marketing .marketing-nav-btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 36px; + padding: 0 16px; + border-radius: 9999px; + font-family: var(--mk-font-mono); + font-size: 12px; + letter-spacing: 0.04em; + white-space: nowrap; + transition: background-color 200ms ease, color 200ms ease, border-color 200ms + ease; + border: 1px solid transparent; +} + +@media (min-width: 768px) { + .marketing .marketing-nav-btn { + height: 38px; + padding: 0 20px; + font-size: 13px; + } +} + +.marketing .marketing-nav-btn.login { + border-color: var(--mk-brand); + color: var(--mk-brand); + background: transparent; +} +.marketing .marketing-nav-btn.login:hover { + background: rgba(0, 212, 255, 0.08); +} + +.marketing .marketing-nav-btn.signup { + background: var(--mk-brand); + color: #04080f; + border-color: var(--mk-brand); +} +.marketing .marketing-nav-btn.signup:hover { + background: transparent; + color: var(--mk-brand); +} + +/* ────────────────────────────────────────── */ +/* FOOTER */ +/* ────────────────────────────────────────── */ + +.marketing .marketing-footer { + margin-top: 24px; + padding-top: 48px; + border-top: 1px solid var(--mk-border); + width: 100%; + overflow: hidden; +} + +.marketing .marketing-footer-top { + max-width: 1100px; + margin: 0 auto; + padding: 0 20px; + text-align: center; + margin-bottom: 60px; +} + +@media (min-width: 768px) { + .marketing .marketing-footer-top { + padding: 0 32px; + margin-bottom: 80px; + } +} + +.marketing .footer-socials { + display: flex; + justify-content: center; + align-items: center; + gap: 14px; + margin-bottom: 28px; +} + +.marketing .footer-socials a { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + border: 1px solid var(--mk-border); + color: var(--mk-text-muted); + transition: color 200ms ease, border-color 200ms ease, background-color 200ms + ease; +} + +.marketing .footer-socials a:hover { + color: var(--mk-text); + border-color: var(--mk-border-bright); + background: rgba(255, 255, 255, 0.03); +} + +.marketing .footer-socials img { + width: 16px; + height: 16px; + display: block; + /* Off-white silhouettes on dark bg */ + filter: invert(56%) sepia(8%) saturate(750%) hue-rotate(190deg) + brightness(95%) contrast(85%); + opacity: 0.9; + transition: filter 200ms ease, opacity 200ms ease; +} + +.marketing .footer-socials a:hover img { + filter: invert(94%) sepia(5%) saturate(380%) hue-rotate(190deg) + brightness(102%) contrast(94%); + opacity: 1; +} + +.marketing .footer-meta { + font-family: var(--mk-font-mono); + font-size: 11px; + letter-spacing: 0.08em; + color: var(--mk-text-muted); + text-transform: uppercase; + line-height: 1.7; +} + +.marketing .footer-meta-link { + color: var(--mk-text-muted); + transition: color 200ms ease; +} +.marketing .footer-meta-link:hover { + color: var(--mk-text-dim); +} + +.marketing .footer-mark { + font-family: var(--mk-font-mono); + font-weight: 700; + font-size: 24vw; + line-height: 0.86; + letter-spacing: -0.04em; + color: var(--mk-brand); + text-align: center; + white-space: nowrap; + display: block; + user-select: none; + text-transform: uppercase; + margin: 0; + padding: 0; +} + +/* ────────────────────────────────────────── */ +/* SUPPORTED SYSTEMS (merchants) */ +/* ────────────────────────────────────────── */ + +.marketing .supported-systems { + margin-top: 28px; + padding-top: 22px; + border-top: 1px solid var(--mk-border); + text-align: center; +} +.marketing .supported-heading { + font-family: var(--mk-font-mono); + font-size: 10px; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--mk-text-muted); + margin-bottom: 16px; +} +.marketing .supported-row { + display: flex; + align-items: center; + justify-content: center; + gap: 28px; + flex-wrap: wrap; + color: var(--mk-text-dim); + margin-bottom: 14px; +} +.marketing .supported-row .brand-logo { + width: 28px; + height: 28px; + display: block; +} +.marketing .supported-row .brand-logo-img { + height: 30px; + width: auto; + display: block; + opacity: 0.9; +} +.marketing .supported-row .brand-wordmark { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--mk-font-mono); + font-size: 13px; + font-weight: 500; + letter-spacing: -0.01em; + color: var(--mk-text-dim); +} +.marketing .supported-row .brand-wordmark .brand-icon { + width: 18px; + height: 18px; +} +.marketing .supported-more { + font-family: var(--mk-font-mono); + font-size: 10px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--mk-text-muted); + opacity: 0.7; +} + +/* ────────────────────────────────────────── */ +/* SPECIMEN PLATE (hero) */ +/* ────────────────────────────────────────── */ + +.marketing .specimen-plate { + position: relative; + width: 100%; + max-width: 460px; + aspect-ratio: 4 / 3; + margin: 0 auto; +} + +.marketing .specimen-corner { + position: absolute; + width: 14px; + height: 14px; + border: 1px solid var(--mk-text-muted); + pointer-events: none; +} +.marketing .specimen-corner.tl { + top: 0; + left: 0; + border-right: none; + border-bottom: none; +} +.marketing .specimen-corner.tr { + top: 0; + right: 0; + border-left: none; + border-bottom: none; +} +.marketing .specimen-corner.bl { + bottom: 0; + left: 0; + border-right: none; + border-top: none; +} +.marketing .specimen-corner.br { + bottom: 0; + right: 0; + border-left: none; + border-top: none; +} + +.marketing .specimen-meta { + position: absolute; + font-family: var(--mk-font-mono); + font-size: 10px; + letter-spacing: 0.1em; + color: var(--mk-text-muted); + text-transform: uppercase; +} +.marketing .specimen-meta.tl { + top: -22px; + left: 0; +} +.marketing .specimen-meta.tr { + top: -22px; + right: 0; +} +.marketing .specimen-meta.br { + bottom: -22px; + right: 0; +} +.marketing .specimen-meta.bl { + bottom: -22px; + left: 0; +} + +.marketing .specimen-card-wrap { + position: absolute; + inset: 12%; + perspective: 1200px; + display: grid; + place-items: center; +} + +.marketing .specimen-card { + position: relative; + width: 100%; + aspect-ratio: 1.6 / 1; + border-radius: 12px; + transition: transform 400ms cubic-bezier(0.16, 1, 0.3, 1); + transform-style: preserve-3d; + will-change: transform; + animation: spec-enter 600ms cubic-bezier(0.16, 1, 0.3, 1) both; + box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.35), + 0 10px 20px -6px rgba(0, 0, 0, 0.55), + 0 24px 48px -14px rgba(0, 0, 0, 0.7), + 0 50px 90px -22px rgba(0, 0, 0, 0.85); +} + +.marketing .specimen-card img { + display: block; + width: 100%; + height: 100%; + object-fit: fill; + border-radius: 12px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), inset 0 -1px 0 + rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255, 255, 255, 0.08); + transition: opacity 220ms ease; +} + +/* Pixelated wipe — SVG-based to avoid sub-pixel grid gaps */ +.marketing .pixel-wipe { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border-radius: 12px; + overflow: hidden; + z-index: 3; + pointer-events: none; +} + +.marketing .pixel-wipe rect { + opacity: 0; + animation: pixel-cell 360ms cubic-bezier(0.65, 0, 0.35, 1) forwards; +} + +@keyframes pixel-cell { + to { + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .marketing .pixel-wipe rect { + animation: none; + opacity: 1; + } +} + +.marketing .specimen-ruler { + position: absolute; + top: 12%; + bottom: 12%; + right: -20px; + width: 8px; + border-right: 1px solid var(--mk-border); + background-image: repeating-linear-gradient( + to bottom, + var(--mk-border) 0, + var(--mk-border) 1px, + transparent 1px, + transparent 24px + ); + background-position: right; + background-size: 6px 24px; + background-repeat: repeat-y; +} + +.marketing .specimen-index { + position: absolute; + bottom: -50px; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} +.marketing .specimen-index button { + width: 24px; + height: 2px; + background: var(--mk-border); + border: none; + padding: 0; + cursor: pointer; + transition: background-color 220ms ease, width 220ms ease; +} +.marketing .specimen-index button.active { + background: var(--mk-brand); + width: 36px; +} +.marketing .specimen-index button:hover { + background: var(--mk-border-bright); +} +.marketing .specimen-index button.active:hover { + background: var(--mk-brand); +} + +@keyframes spec-enter { + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* Smaller on mobile */ +@media (max-width: 767px) { + .marketing .specimen-plate { + max-width: 320px; + aspect-ratio: 1 / 1; + } + .marketing .specimen-card-wrap { + inset: 14%; + } + .marketing .specimen-meta { + font-size: 9px; + } + .marketing .specimen-ruler { + display: none; + } +} + +/* ────────────────────────────────────────── */ +/* TRANSIT RAIL (send) */ +/* ────────────────────────────────────────── */ + +.marketing .transit-rail { + position: relative; + width: 100%; + max-width: 560px; + margin: 0 auto; + padding: 0 12px; +} + +.marketing .transit-track { + position: relative; + height: 80px; + margin: 0 12px; +} + +.marketing .transit-track::before { + content: ""; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background-image: linear-gradient( + to right, + var(--mk-border) 50%, + transparent 50% + ); + background-size: 8px 1px; + transform: translateY(-50%); +} + +.marketing .transit-tick { + position: absolute; + top: 50%; + width: 1px; + height: 8px; + background: var(--mk-text-muted); + transform: translate(-50%, -50%); +} +.marketing .transit-tick.start { + left: 0; +} +.marketing .transit-tick.end { + left: 100%; +} + +.marketing .transit-card { + position: absolute; + top: 50%; + left: 0; + width: 44px; + height: 28px; + border-radius: 4px; + background: var(--mk-bg-card); + border: 1px solid var(--mk-border); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.6); + transform: translate(-50%, -50%); + opacity: 0; +} + +.marketing .transit-card::before { + content: ""; + position: absolute; + inset: 4px; + border-radius: 2px; + background: linear-gradient( + 135deg, + rgba(0, 212, 255, 0.18), + rgba(0, 212, 255, 0.05) + ); +} + +.marketing .transit-rail.played .transit-card { + animation: transit-fly 1500ms cubic-bezier(0.65, 0, 0.35, 1) forwards; +} + +@keyframes transit-fly { + 0% { + left: 0%; + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + left: 100%; + opacity: 1; + } +} + +.marketing .transit-replay { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--mk-font-mono); + font-size: 11px; + color: var(--mk-text-muted); + background: none; + border: none; + padding: 0; + cursor: pointer; + transition: color 200ms ease; +} +.marketing .transit-replay:hover { + color: var(--mk-text-dim); +} + +@media (prefers-reduced-motion: reduce) { + .marketing .transit-rail.played .transit-card { + animation: none; + left: 100%; + opacity: 1; + } +} + +/* ────────────────────────────────────────── */ +/* PAY STAGE (spend — QR scan) */ +/* ────────────────────────────────────────── */ + +.marketing .pay-stage { + position: relative; + width: 100%; + max-width: 320px; + margin: 0 auto; + padding: 24px 22px 22px; + border-radius: 18px; + border: 1px solid var(--mk-border); + background: linear-gradient(180deg, #070d18 0%, #050a13 100%); + box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.8), inset 0 1px 0 + rgba(255, 255, 255, 0.04); + font-family: var(--mk-font-display); +} + +.marketing .pay-head { + display: flex; + align-items: center; + justify-content: space-between; + font-family: var(--mk-font-mono); + font-size: 10px; + letter-spacing: 0.18em; + color: var(--mk-text-muted); + text-transform: uppercase; + padding-bottom: 14px; + border-bottom: 1px solid var(--mk-border); + margin-bottom: 18px; +} +.marketing .pay-head .merchant { + color: var(--mk-text); +} + +.marketing .pay-qr-wrap { + position: relative; + width: 100%; + aspect-ratio: 1 / 1; + max-width: 200px; + margin: 0 auto; + border-radius: 8px; + background: #f4f7ff; + padding: 10px; + overflow: hidden; + transition: opacity 360ms ease; +} + +.marketing .pay-qr { + width: 100%; + height: 100%; + display: block; + color: #04080f; +} +.marketing .pay-qr rect { + fill: currentColor; + shape-rendering: crispEdges; +} + +.marketing .pay-scanline { + position: absolute; + left: 6%; + right: 6%; + height: 2px; + background: var(--mk-brand); + box-shadow: 0 0 12px rgba(0, 212, 255, 0.6); + opacity: 0; + pointer-events: none; +} + +.marketing .pay-paid { + position: absolute; + inset: 10px; + display: grid; + place-items: center; + border-radius: 6px; + background: #04080f; + opacity: 0; + pointer-events: none; +} +.marketing .pay-paid .check-circle { + width: 64px; + height: 64px; + border-radius: 50%; + border: 2px solid var(--mk-brand); + display: grid; + place-items: center; + color: var(--mk-brand); + font-size: 32px; +} + +.marketing .pay-amount { + margin-top: 18px; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + text-align: center; +} +.marketing .pay-amount .sats { + font-family: var(--mk-font-numeric); + font-feature-settings: "tnum"; + font-size: 36px; + font-weight: 600; + line-height: 1; + letter-spacing: 0.01em; + color: var(--mk-text); +} +.marketing .pay-amount .sats .btc-symbol { + font-family: var(--mk-font-mono); + font-weight: 700; + font-size: 0.86em; + margin-right: 0.06em; + display: inline-block; + vertical-align: 0.02em; +} +.marketing .pay-amount .usd { + font-family: var(--mk-font-numeric); + font-feature-settings: "tnum"; + font-size: 16px; + font-weight: 500; + line-height: 1; + letter-spacing: 0.02em; + color: var(--mk-text-muted); +} + +.marketing .pay-status { + position: relative; + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid var(--mk-border); + height: 28px; + font-family: var(--mk-font-mono); + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; +} +.marketing .pay-status .text { + position: absolute; + inset: 14px 0 0; + display: grid; + place-items: center; + opacity: 0; +} +.marketing .pay-status .text.scan { + color: var(--mk-text-muted); + opacity: 1; +} +.marketing .pay-status .text.paid { + color: var(--mk-brand); +} + +.marketing .pay-stage.playing .pay-qr-wrap { + animation: pay-qr-cycle 6s linear infinite; +} +.marketing .pay-stage.playing .pay-scanline { + animation: pay-scan-cycle 6s ease-in-out infinite; +} +.marketing .pay-stage.playing .pay-paid { + animation: pay-paid-cycle 6s ease-in-out infinite; +} +.marketing .pay-stage.playing .pay-status .text.scan { + animation: pay-status-scan 6s linear infinite; +} +.marketing .pay-stage.playing .pay-status .text.paid { + animation: pay-status-paid 6s linear infinite; +} + +@keyframes pay-qr-cycle { + 0%, + 55% { + opacity: 1; + } + 62%, + 92% { + opacity: 0.18; + } + 98%, + 100% { + opacity: 1; + } +} +@keyframes pay-scan-cycle { + 0%, + 35% { + opacity: 0; + top: 10px; + } + 38% { + opacity: 1; + top: 10px; + } + 55% { + opacity: 1; + top: calc(100% - 12px); + } + 58% { + opacity: 0; + top: calc(100% - 12px); + } + 100% { + opacity: 0; + top: 10px; + } +} +@keyframes pay-paid-cycle { + 0%, + 60% { + opacity: 0; + transform: scale(0.96); + } + 64% { + opacity: 1; + transform: scale(1); + } + 92% { + opacity: 1; + transform: scale(1); + } + 96%, + 100% { + opacity: 0; + transform: scale(0.96); + } +} +@keyframes pay-status-scan { + 0%, + 55% { + opacity: 1; + } + 60%, + 95% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes pay-status-paid { + 0%, + 60% { + opacity: 0; + } + 64%, + 92% { + opacity: 1; + } + 96%, + 100% { + opacity: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .marketing .pay-stage.playing .pay-qr-wrap, + .marketing .pay-stage.playing .pay-scanline, + .marketing .pay-stage.playing .pay-paid, + .marketing .pay-stage.playing .pay-status .text { + animation: none; + } + .marketing .pay-stage.playing .pay-paid { + opacity: 1; + } + .marketing .pay-stage.playing .pay-qr-wrap { + opacity: 0.18; + } + .marketing .pay-stage.playing .pay-status .text.scan { + opacity: 0; + } + .marketing .pay-stage.playing .pay-status .text.paid { + opacity: 1; + } +} + +/* ────────────────────────────────────────── */ +/* BUY PANEL (buy section — multi-state flow) */ +/* ────────────────────────────────────────── */ + +.marketing .buy-stage { + position: relative; + width: 100%; + max-width: 320px; + margin: 0 auto; + min-height: 380px; + padding: 22px 22px 18px; + border-radius: 18px; + border: 1px solid var(--mk-border); + background: linear-gradient(180deg, #070d18 0%, #050a13 100%); + box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.8), inset 0 1px 0 + rgba(255, 255, 255, 0.04); + font-family: var(--mk-font-display); + display: flex; + flex-direction: column; +} + +/* Header bar (Agicash side) */ +.marketing .buy-stage .buy-head { + display: flex; + align-items: center; + justify-content: space-between; + font-family: var(--mk-font-mono); + font-size: 10px; + letter-spacing: 0.18em; + color: var(--mk-text-muted); + text-transform: uppercase; + padding-bottom: 14px; + border-bottom: 1px solid var(--mk-border); + margin-bottom: 18px; +} +.marketing .buy-stage .buy-head .merchant { + color: var(--mk-text); +} + +/* State-content wrapper — keyed by state, animates on mount */ +.marketing .buy-stage { + overflow: hidden; +} + +.marketing .buy-stage .state-content { + flex: 1; + display: flex; + flex-direction: column; +} + +/* Slide between Agicash ↔ Cash App (brand changes) */ +.marketing .buy-stage .state-content.transition-slide { + animation: state-slide 2000ms cubic-bezier(0.22, 1, 0.36, 1); +} + +@keyframes state-slide { + 0% { + opacity: 0; + transform: translateX(80px); + } + 60% { + opacity: 1; + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +/* Fade within Cash App's own steps (loading → review → paid) */ +.marketing .buy-stage .state-content.transition-fade { + animation: state-fade 1700ms ease-out; +} + +@keyframes state-fade { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .marketing .buy-stage .state-content.transition-slide, + .marketing .buy-stage .state-content.transition-fade { + animation: none; + } +} + +/* Pay panel (idle state — Agicash buy screen) */ +.marketing .buy-stage .buy-body { + flex: 1; + display: flex; + flex-direction: column; +} + +.marketing .buy-stage .buy-amount { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + text-align: center; + margin-top: 6px; + margin-bottom: 18px; +} +.marketing .buy-stage .buy-amount .sats { + font-family: var(--mk-font-numeric); + font-feature-settings: "tnum"; + font-size: 38px; + font-weight: 700; + line-height: 1; + letter-spacing: 0.01em; + color: var(--mk-text); +} +.marketing .buy-stage .buy-amount .sats .btc-symbol { + font-family: var(--mk-font-mono); + font-weight: 700; + font-size: 0.86em; + margin-right: 0.06em; + display: inline-block; + vertical-align: 0.02em; +} +.marketing .buy-stage .buy-amount .usd { + font-family: var(--mk-font-numeric); + font-feature-settings: "tnum"; + font-size: 18px; + font-weight: 600; + line-height: 1; + color: var(--mk-text-muted); +} + +.marketing .buy-stage .buy-rows { + margin-top: auto; + border: 1px solid var(--mk-border); + border-radius: 12px; + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 10px; + font-family: var(--mk-font-mono); +} +.marketing .buy-stage .buy-row { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 11px; +} +.marketing .buy-stage .buy-row .label { + color: var(--mk-text-muted); + letter-spacing: 0.06em; + text-transform: uppercase; +} +.marketing .buy-stage .buy-row .value { + color: var(--mk-text); +} + +.marketing .buy-stage .buy-cta { + margin-top: 16px; + display: flex; + justify-content: center; +} +.marketing .buy-stage .buy-pay-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 12px 28px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--mk-border); + color: var(--mk-text); + font-family: var(--mk-font-mono); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background-color 200ms ease, border-color 200ms ease; +} +.marketing .buy-stage .buy-pay-button:hover { + background: rgba(255, 255, 255, 0.08); + border-color: var(--mk-border-bright); +} +.marketing .buy-stage .buy-pay-button-primary:hover { + background: rgba(255, 255, 255, 0.92); +} +.marketing .buy-stage .buy-pay-button-primary { + background: #fff; + color: #000; + border-color: #fff; + border-radius: 9999px; + font-family: var(--mk-font-display); +} +.marketing .buy-stage .buy-pay-button-disabled { + background: transparent; + border-color: var(--mk-border); + color: var(--mk-text-muted); +} + +.marketing .buy-stage .buy-center { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.marketing .buy-stage .buy-cashapp-content { + display: flex; + flex-direction: column; + align-items: flex-start; +} +.marketing .buy-stage .buy-cashapp-content.buy-center { + align-items: center; + justify-content: center; +} + +/* Received panel (back to Agicash side after Cash App) */ +.marketing .buy-stage .buy-head-centered { + justify-content: space-between; +} +.marketing .buy-stage .received-close, +.marketing .buy-stage .received-spacer { + width: 16px; + display: inline-block; + font-size: 14px; + color: var(--mk-text); +} +.marketing .buy-stage .received-title { + font-family: var(--mk-font-mono); + font-size: 12px; + letter-spacing: 0.06em; + color: var(--mk-text); + text-transform: lowercase; +} + +.marketing .received-details { + margin-top: auto; + border: 1px solid var(--mk-border); + border-radius: 12px; + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 10px; + font-family: var(--mk-font-mono); +} +.marketing .received-details-head { + font-size: 13px; + color: var(--mk-text); + margin-bottom: 2px; +} +.marketing .received-details-time { + font-size: 11px; + color: var(--mk-text-muted); + margin-bottom: 4px; +} +.marketing .received-detail-row { + display: flex; + align-items: center; + gap: 10px; + font-size: 12px; + color: var(--mk-text); +} +.marketing .received-icon { + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.marketing .received-icon.check { + color: #34c759; +} +.marketing .received-icon.gift { + color: var(--mk-text-muted); +} + +/* Cash App content elements (rendered inside the standard buy-stage panel) */ + +/* Loading spinner */ +.marketing .cashapp-spinner { + width: 36px; + height: 36px; + border-radius: 50%; + border: 3px dashed rgba(255, 255, 255, 0.85); + border-right-color: transparent; + border-bottom-color: transparent; + animation: cashapp-spin 1s linear infinite; +} +@keyframes cashapp-spin { + to { + transform: rotate(360deg); + } +} + +/* Cash App $ icon */ +.marketing .cashapp-icon { + width: 38px; + height: 38px; + border-radius: 50%; + background: #00d54f; + display: grid; + place-items: center; + color: #000; + font-family: var(--mk-font-display); + font-weight: 800; + font-size: 22px; + margin-bottom: 10px; +} + +.marketing .cashapp-headline { + font-family: var(--mk-font-display); + font-size: 22px; + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.02em; + color: var(--mk-text); + margin-bottom: 14px; +} + +.marketing .cashapp-rows { + display: flex; + flex-direction: column; + gap: 8px; + font-family: var(--mk-font-mono); + font-size: 11px; + width: 100%; + margin-top: 2px; +} +.marketing .cashapp-row { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; +} +.marketing .cashapp-row .label { + color: var(--mk-text-muted); + letter-spacing: 0.06em; + text-transform: uppercase; +} +.marketing .cashapp-row .value { + color: var(--mk-text); + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 58%; +} + +/* Confirmation check */ +.marketing .cashapp-check { + width: 48px; + height: 48px; + border-radius: 50%; + background: #00d54f; + display: grid; + place-items: center; + color: #000; + margin-bottom: 16px; + animation: confirm-pop 380ms cubic-bezier(0.16, 1, 0.3, 1); +} +.marketing .cashapp-check svg { + width: 24px; + height: 24px; + display: block; +} + +/* Pop-in animation for paid state */ +.marketing .buy-stage.paid .cashapp-check, +.marketing .buy-stage.paid .cashapp-headline, +.marketing .buy-stage.paid .cashapp-done { + animation: confirm-pop 380ms cubic-bezier(0.16, 1, 0.3, 1) both; +} +.marketing .buy-stage.paid .cashapp-headline { + animation-delay: 80ms; +} +.marketing .buy-stage.paid .cashapp-done { + animation-delay: 160ms; +} + +@media (prefers-reduced-motion: reduce) { + .marketing .cashapp-spinner, + .marketing .buy-stage.paid .cashapp-check, + .marketing .buy-stage.paid .cashapp-headline, + .marketing .buy-stage.paid .cashapp-done { + animation: none; + } +} + +/* ────────────────────────────────────────── */ +/* SPEC ROWS (wallet) */ +/* ────────────────────────────────────────── */ + +.marketing .spec-table { + width: 100%; +} +.marketing .spec-head { + font-family: var(--mk-font-mono); + font-size: 10px; + letter-spacing: 0.2em; + color: var(--mk-text-muted); + text-transform: uppercase; + margin-bottom: 6px; + padding-bottom: 8px; + border-bottom: 1px solid var(--mk-border); +} +.marketing .spec-row { + display: grid; + grid-template-columns: 88px 1fr auto; + align-items: baseline; + gap: 12px; + padding: 14px 0; + border-bottom: 1px solid var(--mk-border); +} +.marketing .spec-row:last-child { + border-bottom: none; +} +.marketing .spec-label { + font-family: var(--mk-font-mono); + font-size: 11px; + letter-spacing: 0.12em; + color: var(--mk-text-muted); + text-transform: uppercase; +} +.marketing .spec-leader { + height: 1px; + background-image: radial-gradient( + circle, + var(--mk-text-muted) 0.6px, + transparent 0.6px + ); + background-size: 6px 1px; + background-repeat: repeat-x; + background-position: 0 50%; + align-self: center; + opacity: 0.6; +} +.marketing .spec-value { + text-align: right; + font-family: var(--mk-font-mono); + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--mk-text); + font-feature-settings: "tnum"; +} + +@media (max-width: 767px) { + .marketing .spec-row { + grid-template-columns: 80px 1fr; + } + .marketing .spec-leader { + display: none; + } + .marketing .spec-value { + text-align: right; + } +} + +/* ────────────────────────────────────────── */ +/* WALLET CARD (used in wallet section) */ +/* ────────────────────────────────────────── */ + +.marketing .wallet-card { + width: 100%; + max-width: 320px; + margin: 0 auto; + border-radius: 18px; + border: 1px solid var(--mk-border); + background: linear-gradient(180deg, #070d18 0%, #050a13 100%); + box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.7); + padding: 22px 22px 20px; + font-family: var(--mk-font-display); +} +.marketing .wallet-card .head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 18px; +} +.marketing .wallet-card .head .label { + font-family: var(--mk-font-mono); + font-size: 10px; + letter-spacing: 0.18em; + color: var(--mk-text-muted); + text-transform: uppercase; +} +.marketing .wallet-card .head .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--mk-brand); + box-shadow: 0 0 8px rgba(0, 212, 255, 0.5); +} +.marketing .wallet-card .handle { + font-family: var(--mk-font-mono); + font-size: 12px; + color: var(--mk-text-dim); + margin-bottom: 18px; +} +.marketing .wallet-card .balance { + font-family: var(--mk-font-numeric); + font-size: 52px; + letter-spacing: 0.01em; + font-weight: 600; + line-height: 1; + color: var(--mk-text); + font-feature-settings: "tnum"; + text-align: center; +} +.marketing .wallet-card .balance .btc-symbol { + font-family: var(--mk-font-mono); + font-weight: 700; + font-size: 0.86em; + margin-right: 0.06em; + display: inline-block; + vertical-align: 0.02em; +} +.marketing .wallet-card .usd { + font-family: var(--mk-font-numeric); + font-size: 18px; + font-weight: 500; + letter-spacing: 0.01em; + line-height: 1; + color: var(--mk-text-muted); + font-feature-settings: "tnum"; + margin-top: 6px; + margin-bottom: 22px; + text-align: center; +} +.marketing .wallet-card .actions { + display: flex; + flex-direction: column; + gap: 8px; +} +.marketing .wallet-card .actions .actions-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} +.marketing .wallet-card .actions .btn { + font-family: var(--mk-font-mono); + font-size: 13px; + padding: 12px 0; + border-radius: 10px; + text-align: center; + background: transparent; + border: 1px solid var(--mk-border); + color: var(--mk-text); +} +.marketing .wallet-card .actions .send { + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--mk-border); +} +.marketing .wallet-card .footer { + margin-top: 18px; + display: flex; + align-items: center; + gap: 8px; + font-family: var(--mk-font-mono); + font-size: 10px; + letter-spacing: 0.1em; + color: var(--mk-text-muted); + text-transform: uppercase; +} +.marketing .wallet-card .footer .syncdot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--mk-brand); + opacity: 0.7; +} diff --git a/app/routes/_public.home-v1.tsx b/app/routes/_public.home-v1.tsx new file mode 100644 index 000000000..125f44f46 --- /dev/null +++ b/app/routes/_public.home-v1.tsx @@ -0,0 +1,18 @@ +import { MarketingPage } from '~/features/homepage-v1/marketing-page'; +import type { Route } from './+types/_public.home-v1'; + +export const meta = (_: Route.MetaArgs) => { + const title = 'Agicash · v1'; + const description = + 'Self-custodial Bitcoin wallet, closed-loop merchant ecash, and MCP-native machine payments.'; + + return [ + { title }, + { name: 'description', content: description }, + { name: 'robots', content: 'noindex' }, + ]; +}; + +export default function HomeV1Page() { + return ; +} diff --git a/app/routes/_public.home.tsx b/app/routes/_public.home.tsx index ac1f87232..ac5d218e8 100644 --- a/app/routes/_public.home.tsx +++ b/app/routes/_public.home.tsx @@ -1,58 +1,21 @@ -import { useQuery } from '@tanstack/react-query'; -import { Link, useLocation } from 'react-router'; -import DiscordLogo from '~/assets/discord_logo.svg'; -import { Page, PageContent } from '~/components/page'; -import { Button } from '~/components/ui/button'; -import { authQueryOptions } from '~/features/user/auth'; +import { MarketingPage } from '~/features/homepage/marketing-page'; +import type { Route } from './+types/_public.home'; -export default function HomePage() { - const location = useLocation(); - const { data: authState } = useQuery(authQueryOptions()); - - const isLoggedIn = authState?.isLoggedIn ?? false; +export const meta = (_: Route.MetaArgs) => { + const title = 'Agicash'; + const description = + 'Closed-loop ecash for the merchants you actually visit. Buy a card. Send it. Spend it at the counter. In public beta.'; - return ( - - -
-
-

- Coming Soon -

+ return [ + { title }, + { name: 'description', content: description }, + { property: 'og:title', content: title }, + { property: 'og:description', content: description }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + ]; +}; - - Discord - - -
- {isLoggedIn ? ( - - ) : ( - - )} -
-
-
-
-
- ); +export default function HomePage() { + return ; } From 4ef4836d972acae4ecccc70a1eff192bda35a173 Mon Sep 17 00:00:00 2001 From: orveth Date: Tue, 5 May 2026 10:32:57 -0700 Subject: [PATCH 02/49] fix(homepage): resolve lint errors blocking CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mark decorative SVGs in buy-section as aria-hidden (matches spend/hero pattern). - Suppress noArrayIndexKey on the static QR grid — pattern is deterministic and never reorders. - Drop unused PIXEL_CELLS constant in hero-section. Co-Authored-By: Claude Opus 4.7 --- app/features/homepage/sections/buy-section.tsx | 5 +++-- app/features/homepage/sections/hero-section.tsx | 1 - app/features/homepage/sections/spend-section.tsx | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/features/homepage/sections/buy-section.tsx b/app/features/homepage/sections/buy-section.tsx index 2c0a29ba1..a91176c4c 100644 --- a/app/features/homepage/sections/buy-section.tsx +++ b/app/features/homepage/sections/buy-section.tsx @@ -180,7 +180,7 @@ function PaidBody() { return (