feat(docs): add OG image and prune unused assets

This commit is contained in:
2026-07-16 20:38:53 +02:00
parent 68b379e6b5
commit ebff97d5e2
22 changed files with 458 additions and 3622 deletions

View File

@@ -1,86 +0,0 @@
"use client";
import { useState } from "react";
import { Icon } from "@/components/icons";
type ApprovalState = "pending" | "approved" | "refused";
export function ApprovalDemo() {
const [state, setState] = useState<ApprovalState>("pending");
return (
<div className={`approval-demo state-${state}`}>
<div className="approval-window-bar">
<span />
<span />
<span />
<small>Receiver requests</small>
</div>
<div className="approval-body" aria-live="polite">
<div className="approval-heading">
<div className="device-avatar">
<span className="device-avatar-screen" />
<span className="device-avatar-glow" />
</div>
<div>
<span className="mono-label">
{state === "pending" ? "NEW REQUEST" : state === "approved" ? "IN PROGRESS" : "REQUEST CLOSED"}
</span>
<h3>
{state === "pending" && "Mayas phone wants to receive"}
{state === "approved" && "Sending securely to Maya"}
{state === "refused" && "Request refused"}
</h3>
</div>
</div>
<div className="approval-file">
<span className="approval-file-icon">
<Icon name="folder" />
</span>
<span>
<strong>Summer photos</strong>
<small>42 files · 1.8 GB</small>
</span>
{state === "approved" && <span className="approval-percent">68%</span>}
</div>
{state === "pending" && (
<div className="approval-actions">
<button className="approval-refuse" type="button" onClick={() => setState("refused")}>
Refuse
</button>
<button className="approval-accept" type="button" onClick={() => setState("approved")}>
<Icon name="check" />
Approve
</button>
</div>
)}
{state === "approved" && (
<div className="approval-progress">
<div className="approval-progress-track">
<span />
</div>
<div className="approval-progress-meta">
<span><i className="live-dot" /> Direct connection</span>
<button type="button" onClick={() => setState("pending")}>Reset demo</button>
</div>
</div>
)}
{state === "refused" && (
<div className="approval-refused">
<span><Icon name="x" /></span>
<p>This device was not given access.</p>
<button type="button" onClick={() => setState("pending")}>Try again</button>
</div>
)}
</div>
<div className="approval-footer">
<Icon name="shield" />
Approval is required by default
</div>
</div>
);
}

View File

@@ -1,267 +0,0 @@
/* Interactive receiver approval */
.approval-demo {
overflow: hidden;
border: 1px solid #ded6e2;
border-radius: 24px;
background: #fbf9fc;
box-shadow: 0 35px 90px rgba(49, 29, 58, 0.16);
transform: rotate(1deg);
transition: transform 280ms ease, border-color 280ms ease;
}
.approval-demo:hover { transform: rotate(0deg) translateY(-3px); }
.approval-demo.state-approved { border-color: rgba(45, 185, 117, 0.4); }
.approval-demo.state-refused { border-color: rgba(221, 92, 75, 0.35); }
.approval-window-bar {
display: flex;
height: 48px;
align-items: center;
gap: 6px;
padding: 0 16px;
border-bottom: 1px solid #ded6e2;
background: #f0ecf2;
}
.approval-window-bar > span {
width: 8px;
height: 8px;
border-radius: 50%;
background: #d1c7d5;
}
.approval-window-bar > span:nth-child(1) { background: #f1a191; }
.approval-window-bar > span:nth-child(2) { background: #f1ca72; }
.approval-window-bar > span:nth-child(3) { background: #72cf9b; }
.approval-window-bar small {
margin-left: auto;
color: #8a7f8f;
font-family: var(--font-mono);
font-size: 8px;
font-weight: 700;
letter-spacing: 0.06em;
}
.approval-body {
min-height: 385px;
padding: 35px;
background:
radial-gradient(circle at 90% 0%, rgba(192, 132, 252, 0.16), transparent 27%),
white;
}
.approval-heading {
display: flex;
align-items: center;
gap: 17px;
margin-bottom: 30px;
}
.device-avatar {
position: relative;
width: 52px;
height: 60px;
flex: 0 0 auto;
border: 4px solid #352e39;
border-radius: 13px;
background: #211d24;
box-shadow: 0 10px 25px rgba(42, 30, 48, 0.16);
}
.device-avatar::before {
position: absolute;
top: 4px;
left: 50%;
width: 16px;
height: 3px;
border-radius: 999px;
background: #0c0a0d;
content: "";
transform: translateX(-50%);
}
.device-avatar-screen {
position: absolute;
inset: 14px 8px 8px;
border-radius: 7px;
background: linear-gradient(145deg, #b55efa, #6f27e9);
}
.device-avatar-glow {
position: absolute;
inset: -5px;
z-index: -1;
border-radius: 16px;
box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.25);
animation: avatar-pulse 2.7s ease-out infinite;
}
.approval-heading h3 {
max-width: 360px;
margin: 5px 0 0;
font-size: 21px;
letter-spacing: -0.035em;
line-height: 1.22;
}
.approval-file {
display: flex;
align-items: center;
gap: 13px;
padding: 16px;
border: 1px solid #e5dde8;
border-radius: 14px;
background: #f8f5f9;
}
.approval-file-icon {
display: grid;
width: 42px;
height: 42px;
flex: 0 0 auto;
place-items: center;
border-radius: 11px;
background: var(--brand-100);
color: var(--brand-600);
}
.approval-file-icon svg { width: 21px; }
.approval-file strong,
.approval-file small { display: block; }
.approval-file strong { font-size: 12px; }
.approval-file small { color: var(--ink-muted); font-size: 9px; }
.approval-percent {
margin-left: auto;
color: var(--brand-600);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 800;
}
.approval-actions {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 10px;
margin-top: 22px;
}
.approval-actions button,
.approval-refused button {
min-height: 47px;
border: 1px solid #ded4e2;
border-radius: 12px;
font-size: 12px;
font-weight: 750;
cursor: pointer;
transition: transform 160ms ease, background 160ms ease, box-shadow 160ms ease;
}
.approval-actions button:hover,
.approval-refused button:hover { transform: translateY(-2px); }
.approval-refuse { background: white; color: #625769; }
.approval-accept {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border-color: transparent !important;
background: linear-gradient(135deg, var(--brand-500), var(--brand-700));
box-shadow: 0 10px 24px rgba(132, 47, 238, 0.22);
color: white;
}
.approval-accept svg { width: 17px; }
.approval-progress {
margin-top: 25px;
animation: approval-state-enter 350ms ease;
}
.approval-progress-track {
overflow: hidden;
height: 8px;
border-radius: 999px;
background: #eee8f1;
}
.approval-progress-track span {
display: block;
width: 68%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--brand-500), var(--brand-700));
box-shadow: 0 0 15px rgba(168, 85, 247, 0.38);
animation: progress-fill 900ms cubic-bezier(0.25, 0.8, 0.25, 1) both;
}
.approval-progress-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 13px;
color: var(--ink-muted);
font-size: 9px;
}
.approval-progress-meta > span {
display: flex;
align-items: center;
gap: 9px;
}
.approval-progress-meta button,
.approval-refused button {
border: 0;
background: transparent;
color: var(--brand-600);
font-size: 9px;
font-weight: 750;
cursor: pointer;
}
.approval-refused {
display: flex;
align-items: center;
gap: 12px;
margin-top: 23px;
padding: 14px;
border: 1px solid #f0d5d0;
border-radius: 12px;
background: #fff8f7;
animation: approval-state-enter 350ms ease;
}
.approval-refused > span {
display: grid;
width: 30px;
height: 30px;
flex: 0 0 auto;
place-items: center;
border-radius: 50%;
background: #fde7e3;
color: #d85848;
}
.approval-refused svg { width: 15px; }
.approval-refused p { margin: 0; color: #705c59; font-size: 10px; }
.approval-refused button { margin-left: auto; }
.approval-footer {
display: flex;
height: 50px;
align-items: center;
justify-content: center;
gap: 8px;
border-top: 1px solid #ded6e2;
background: #f2eef4;
color: #766b7b;
font-family: var(--font-mono);
font-size: 8px;
font-weight: 700;
letter-spacing: 0.04em;
}
.approval-footer svg { width: 15px; color: var(--brand-600); }

View File

@@ -20,28 +20,14 @@ export function BrandMark({ className, title }: BrandAssetProps) {
);
}
export function AppIcon({ className, title }: BrandAssetProps) {
return (
<Image
className={className}
src="/icon.svg"
width={1024}
height={1024}
alt={title ?? ""}
aria-hidden={title ? undefined : true}
unoptimized
/>
);
}
export function Brand({ compact = false }: { compact?: boolean }) {
export function Brand() {
return (
<Link className="brand" href="/" aria-label="VniDrop home">
<span className="brand-primary">
<BrandMark className="brand-mark-image" />
{!compact && <span className="brand-name">VniDrop</span>}
<span className="brand-name">VniDrop</span>
</span>
{!compact && <span className="brand-tagline">Send files directly. Stay in control.</span>}
<span className="brand-tagline">Send files directly. Stay in control.</span>
</Link>
);
}

View File

@@ -1,490 +0,0 @@
/* Receiver hardware, status overlays, and responsive stage composition */
.receiver-phone-shell {
position: absolute;
z-index: 5;
top: 17%;
right: 1.5%;
width: 23.5%;
aspect-ratio: 0.49;
transform: translate(
calc(var(--pointer-x) * 7px),
calc(var(--pointer-y) * 5px)
)
rotateY(calc(var(--pointer-x) * -1.6deg))
rotateX(calc(var(--pointer-y) * 1.3deg));
transform-origin: center;
transition: transform 180ms ease-out;
}
.receiver-phone-frame {
position: absolute;
inset: 0;
padding: 7px;
border: 1px solid #bdb7c1;
border-radius: 38px;
background: linear-gradient(145deg, #e7e4e9, #aaa4ae 42%, #f3f1f4 72%, #aaa3ae);
box-shadow:
0 38px 85px rgba(43, 27, 51, 0.22),
0 9px 25px rgba(43, 27, 51, 0.12),
inset 0 0 0 1px rgba(255, 255, 255, 0.85);
}
.receiver-phone-frame::before {
position: absolute;
z-index: 3;
inset: 2px;
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 35px;
content: "";
pointer-events: none;
}
.phone-side-button {
position: absolute;
right: -3px;
width: 3px;
border-radius: 0 2px 2px 0;
background: #98929d;
}
.phone-side-button-one { top: 24%; height: 14%; }
.phone-side-button-two { top: 42%; height: 21%; }
.phone-screen {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 31px;
background:
radial-gradient(circle at 80% 18%, rgba(192, 132, 252, 0.12), transparent 28%),
#faf9fb;
box-shadow: inset 0 0 0 1px rgba(26, 22, 29, 0.18);
}
.phone-screen::after {
position: absolute;
top: -28%;
left: -42%;
width: 82%;
height: 130%;
background: linear-gradient(110deg, transparent 35%, rgba(255, 255, 255, 0.32), transparent 66%);
content: "";
pointer-events: none;
transform: rotate(8deg);
}
.phone-status-bar {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
height: 24px;
padding: 0 10px;
color: #2e2931;
font-size: 6px;
font-weight: 760;
}
.phone-island {
width: 42px;
height: 12px;
justify-self: center;
border-radius: 999px;
background: #171419;
}
.phone-signal {
justify-self: end;
font-size: 5px;
letter-spacing: 1px;
}
.phone-app-bar {
display: grid;
grid-template-columns: 26px 1fr 25px;
align-items: center;
padding: 5px 11px 8px;
}
.phone-brand-mark {
width: 26px;
height: 26px;
object-fit: contain;
}
.phone-app-bar strong {
color: #29242c;
font-size: 9px;
letter-spacing: -0.02em;
}
.phone-app-bar > span {
display: grid;
width: 23px;
height: 23px;
place-items: center;
border: 1px solid #e9e3ec;
border-radius: 8px;
background: white;
color: var(--brand-700);
}
.phone-app-bar svg { width: 11px; }
.phone-transfer-card {
display: grid;
margin: 6px 10px 0;
padding: 11px 9px 10px;
border: 1px solid #e6e0e9;
border-radius: 14px;
background: white;
box-shadow: 0 9px 25px rgba(48, 33, 55, 0.055);
text-align: center;
}
.phone-file-art {
position: relative;
display: grid;
width: 46px;
height: 46px;
margin: 0 auto 8px;
place-items: center;
overflow: hidden;
border-radius: 13px;
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.2), transparent 48%),
linear-gradient(135deg, #c084fc, #7c2aef);
color: white;
box-shadow: 0 10px 22px rgba(132, 47, 238, 0.22);
}
.phone-file-art::after {
position: absolute;
top: -8px;
right: -8px;
width: 24px;
height: 24px;
background: rgba(235, 211, 255, 0.65);
content: "";
transform: rotate(45deg);
}
.phone-file-art svg { width: 21px; }
.phone-transfer-card small {
color: #9a8fa0;
font-family: var(--font-mono);
font-size: 5px;
font-weight: 800;
letter-spacing: 0.09em;
}
.phone-transfer-card strong {
margin-top: 3px;
color: #29232d;
font-size: 9px;
letter-spacing: -0.02em;
}
.phone-transfer-card > span {
color: #8d8391;
font-size: 6px;
}
.phone-state-area {
position: relative;
height: 88px;
margin: 9px 10px 0;
overflow: hidden;
border-radius: 13px;
background: #f2eff4;
}
.phone-state {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}
.state-symbol {
display: grid;
width: 31px;
height: 31px;
margin-bottom: 6px;
place-items: center;
border-radius: 10px;
background: #e5d3f3;
color: var(--brand-700);
}
.state-symbol.is-success {
background: #d8f4e7;
color: #188457;
}
.state-symbol svg { width: 15px; }
.phone-state strong {
color: #39313d;
font-size: 7px;
}
.phone-state small {
color: #8d8291;
font-size: 5.5px;
}
.transfer-percentage {
margin-bottom: 3px;
color: var(--brand-700);
font-family: var(--font-display);
font-size: 24px;
font-weight: 740;
line-height: 1;
letter-spacing: -0.08em;
}
.transfer-percentage span {
margin-left: 1px;
font-size: 10px;
}
.phone-home-indicator {
position: absolute;
bottom: 5px;
left: 50%;
width: 42px;
height: 3px;
border-radius: 999px;
background: #27222a;
transform: translateX(-50%);
}
.hero-route-card,
.hero-verified-card {
position: absolute;
z-index: 8;
display: flex;
align-items: center;
border: 1px solid rgba(214, 204, 220, 0.92);
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 15px 45px rgba(49, 27, 61, 0.12);
backdrop-filter: blur(16px);
}
.hero-route-card {
top: 3%;
left: 36%;
gap: 8px;
min-width: 176px;
padding: 9px 10px;
border-radius: 13px;
}
.route-pulse {
width: 8px;
height: 8px;
flex: 0 0 auto;
border: 2px solid #def7eb;
border-radius: 50%;
background: #2fbc7a;
box-shadow: 0 0 0 4px rgba(47, 188, 122, 0.1);
}
.hero-route-card > span:nth-child(2),
.hero-verified-card > span:nth-child(2) {
display: grid;
}
.hero-route-card small {
color: #978d9b;
font-family: var(--font-mono);
font-size: 5px;
font-weight: 800;
letter-spacing: 0.12em;
}
.hero-route-card strong,
.hero-verified-card strong {
color: #342d37;
font-size: 7.5px;
}
.route-latency {
margin-left: auto;
color: var(--brand-700);
font-family: var(--font-mono);
font-size: 6px;
font-weight: 800;
}
.hero-verified-card {
right: 0;
bottom: 12%;
gap: 8px;
padding: 9px 11px 9px 8px;
border-radius: 13px;
}
.hero-verified-card > span:first-child {
display: grid;
width: 31px;
height: 31px;
place-items: center;
border-radius: 10px;
background: #dcf6e9;
color: #1b8d5d;
}
.hero-verified-card svg { width: 16px; }
.hero-verified-card small {
color: #8e8492;
font-size: 6px;
}
.hero-privacy-rail {
position: absolute;
z-index: 6;
bottom: 3.5%;
left: 8%;
display: inline-flex;
align-items: center;
gap: 9px;
padding: 7px 10px;
border: 1px solid rgba(217, 208, 222, 0.82);
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
color: #786e7c;
font-family: var(--font-mono);
font-size: 5.5px;
font-weight: 750;
letter-spacing: 0.035em;
text-transform: uppercase;
backdrop-filter: blur(12px);
}
.hero-privacy-rail span {
display: inline-flex;
align-items: center;
gap: 4px;
}
.hero-privacy-rail svg {
width: 9px;
color: var(--brand-700);
}
.hero-privacy-rail i {
width: 3px;
height: 3px;
border-radius: 50%;
background: #c4b9c8;
}
@media (max-width: 680px) {
.hero-stage {
aspect-ratio: 1.02;
}
.hero-stage-grid {
inset: 2% -2% 2%;
border-radius: 30px;
background-size: 22px 22px, 22px 22px, auto, auto;
}
.desktop-product-shell {
top: 12%;
left: -2%;
width: 70%;
height: 56%;
}
.receiver-phone-shell {
top: 22%;
right: -1%;
width: 26%;
}
.desktop-app-layout { grid-template-columns: 42px 1fr; }
.desktop-transfer-view { padding: 11px 10px 9px; }
.desktop-sidebar { gap: 4px; padding-top: 7px; }
.product-brand-mark { width: 29px; height: 29px; }
.desktop-sidebar-item { width: 26px; height: 26px; }
.desktop-sidebar-item svg { width: 12px; }
.file-collection-card { grid-template-columns: 51px 1fr; gap: 7px; }
.file-collage { width: 51px; height: 45px; }
.collection-menu { display: none; }
.receiver-state-stack { width: 72px; }
.desktop-transfer-foot { display: none; }
.approval-knock-card {
right: -6%;
bottom: -22%;
width: 200px;
}
.hero-route-card {
top: 1%;
left: 27%;
min-width: 156px;
}
.hero-verified-card { right: -1%; bottom: 8%; }
.hero-privacy-rail { bottom: 0; left: 3%; }
.stage-coordinate-side { display: none; }
}
@media (max-width: 480px) {
.desktop-window-bar {
grid-template-columns: 1fr auto;
height: 28px;
}
.window-title { display: none; }
.desktop-app-layout { height: calc(100% - 28px); }
.desktop-view-heading { margin-bottom: 7px; }
.desktop-view-heading strong { font-size: 12px; }
.file-collection-card { padding: 7px; }
.receiver-section-heading { margin-top: 8px; }
.desktop-receiver-row { grid-template-columns: 27px 1fr; }
.receiver-avatar { width: 26px; height: 26px; }
.receiver-state-stack { display: none; }
.receiver-progress-track { left: 43px; }
.approval-knock-card { bottom: -27%; width: 184px; padding: 9px; }
.approval-knock-card p { margin: 6px 0; }
.phone-app-bar { grid-template-columns: 22px 1fr 22px; padding-inline: 7px; }
.phone-brand-mark { width: 22px; height: 22px; }
.phone-transfer-card { margin-inline: 7px; padding-inline: 7px; }
.phone-file-art { width: 38px; height: 38px; }
.phone-state-area { height: 73px; margin-inline: 7px; }
.state-symbol { width: 27px; height: 27px; margin-bottom: 3px; }
.transfer-percentage { font-size: 20px; }
.hero-route-card {
left: 20%;
min-width: 142px;
padding: 7px 8px;
}
.route-latency { display: none; }
.hero-verified-card { padding: 7px; }
.hero-verified-card > span:first-child { width: 27px; height: 27px; }
.hero-privacy-rail {
gap: 6px;
font-size: 5px;
}
.hero-privacy-rail span:nth-of-type(3),
.hero-privacy-rail i:nth-of-type(2) {
display: none;
}
}

View File

@@ -1,386 +0,0 @@
"use client";
import type { CSSProperties, PointerEvent } from "react";
import { useRef } from "react";
import { motion, useReducedMotion } from "motion/react";
import { BrandMark } from "@/components/brand";
import { Icon } from "@/components/icons";
type StageStyle = CSSProperties & {
"--pointer-x": string;
"--pointer-y": string;
};
const cycle = 10.5;
const rest = 1.2;
export function HeroIllustration() {
const stageRef = useRef<HTMLDivElement>(null);
const reduceMotion = useReducedMotion();
const updateParallax = (event: PointerEvent<HTMLDivElement>) => {
const stage = stageRef.current;
if (!stage || reduceMotion) return;
const bounds = stage.getBoundingClientRect();
const x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 2;
const y = ((event.clientY - bounds.top) / bounds.height - 0.5) * 2;
stage.style.setProperty("--pointer-x", x.toFixed(3));
stage.style.setProperty("--pointer-y", y.toFixed(3));
};
const resetParallax = () => {
stageRef.current?.style.setProperty("--pointer-x", "0");
stageRef.current?.style.setProperty("--pointer-y", "0");
};
const repeating = { duration: cycle, repeat: Infinity, repeatDelay: rest };
return (
<figure className="hero-visual">
<div
ref={stageRef}
className="hero-stage"
style={{ "--pointer-x": "0", "--pointer-y": "0" } as StageStyle}
onPointerMove={updateParallax}
onPointerLeave={resetParallax}
aria-hidden="true"
>
<div className="hero-stage-grid" />
<div className="hero-stage-glow" />
<span className="stage-coordinate stage-coordinate-top">NODE / SEND · 01</span>
<span className="stage-coordinate stage-coordinate-side">SECURE CHANNEL</span>
<div className="desktop-product-shell">
<div className="desktop-product-window">
<div className="desktop-window-bar">
<span className="window-controls"><i /><i /><i /></span>
<span className="window-title">VniDrop · Send</span>
<span className="window-security"><Icon name="lock" /> Encrypted</span>
</div>
<div className="desktop-app-layout">
<aside className="desktop-sidebar">
<BrandMark className="product-brand-mark" />
<span className="desktop-sidebar-item is-active"><Icon name="file" /></span>
<span className="desktop-sidebar-item"><Icon name="devices" /></span>
<span className="desktop-sidebar-item"><Icon name="shield" /></span>
<span className="desktop-sidebar-spacer" />
<span className="desktop-profile">AH</span>
</aside>
<div className="desktop-transfer-view">
<div className="desktop-view-heading">
<div>
<small>OUTGOING TRANSFER</small>
<strong>Summer archive</strong>
</div>
<span className="sharing-pill"><i /> Sharing</span>
</div>
<div className="file-collection-card">
<div className="file-collage" aria-hidden="true">
<span /><span /><span /><span />
</div>
<div className="file-collection-copy">
<strong>summer-photos</strong>
<span>42 files · 486 MB</span>
<small><Icon name="folder" /> Structure preserved</small>
</div>
<span className="collection-menu"></span>
</div>
<div className="receiver-section-heading">
<span>Receivers</span>
<small>1 connected</small>
</div>
<div className="desktop-receiver-row">
<span className="receiver-avatar">M</span>
<span className="receiver-identity">
<strong>Maya&apos;s iPhone</strong>
<small>iOS · nearby device</small>
</span>
<span className="receiver-state-stack">
<motion.span
className="receiver-row-state is-waiting"
animate={
reduceMotion
? { opacity: 0 }
: { opacity: [1, 1, 0, 0] }
}
transition={{ ...repeating, times: [0, 0.4, 0.48, 1] }}
>
Approval required
</motion.span>
<motion.span
className="receiver-row-state is-sending"
animate={
reduceMotion
? { opacity: 0 }
: { opacity: [0, 0, 1, 1, 0, 0] }
}
transition={{ ...repeating, times: [0, 0.43, 0.5, 0.71, 0.78, 1] }}
>
Sending securely
</motion.span>
<motion.span
className="receiver-row-state is-complete"
animate={
reduceMotion
? { opacity: 1 }
: { opacity: [0, 0, 1, 1] }
}
transition={{ ...repeating, times: [0, 0.72, 0.79, 1] }}
>
<Icon name="verified" /> Verified
</motion.span>
</span>
<div className="receiver-progress-track">
<motion.span
animate={
reduceMotion
? { scaleX: 1 }
: { scaleX: [0, 0, 0.04, 0.72, 1, 1] }
}
transition={{ ...repeating, times: [0, 0.46, 0.51, 0.67, 0.76, 1] }}
/>
</div>
</div>
<div className="desktop-transfer-foot">
<span><Icon name="shield" /> Approval by default</span>
<span><Icon name="lock" /> End-to-end encrypted</span>
</div>
</div>
</div>
<motion.div
className="approval-knock-card"
animate={
reduceMotion
? { opacity: 0, y: 8, scale: 0.98 }
: {
opacity: [0, 0, 1, 1, 0, 0],
y: [12, 12, 0, 0, -5, -5],
scale: [0.97, 0.97, 1, 1, 0.985, 0.985],
}
}
transition={{ ...repeating, times: [0, 0.24, 0.3, 0.42, 0.49, 1], ease: "easeInOut" }}
>
<div className="approval-knock-top">
<span className="receiver-avatar">M</span>
<span>
<small>NEW RECEIVER</small>
<strong>Maya&apos;s iPhone</strong>
</span>
<span className="approval-lock"><Icon name="lock" /></span>
</div>
<p>Wants to receive <strong>summer-photos</strong></p>
<div className="approval-knock-actions">
<span>Refuse</span>
<motion.span
className="approval-accept"
animate={reduceMotion ? undefined : { scale: [1, 1, 1.055, 1, 1] }}
transition={{ ...repeating, times: [0, 0.38, 0.42, 0.46, 1] }}
>
<Icon name="check" /> Approve
</motion.span>
</div>
</motion.div>
</div>
</div>
<svg className="hero-conduit" viewBox="0 0 760 620" fill="none">
<defs>
<linearGradient id="conduit-gradient" x1="410" y1="340" x2="650" y2="390" gradientUnits="userSpaceOnUse">
<stop stopColor="#C084FC" />
<stop offset="1" stopColor="#7C2AEF" />
</linearGradient>
<filter id="conduit-blur" x="-30%" y="-80%" width="160%" height="260%">
<feGaussianBlur stdDeviation="8" />
</filter>
</defs>
<path
d="M414 342C480 344 508 392 571 390C606 389 624 373 646 351"
stroke="#D9D0DE"
strokeWidth="14"
strokeLinecap="round"
/>
<motion.path
d="M414 342C480 344 508 392 571 390C606 389 624 373 646 351"
stroke="#A855F7"
strokeOpacity="0.24"
strokeWidth="28"
strokeLinecap="round"
filter="url(#conduit-blur)"
animate={
reduceMotion
? { opacity: 0.55, pathLength: 1 }
: { opacity: [0.05, 0.05, 0.55, 0.55, 0.15], pathLength: [0, 0, 1, 1, 1] }
}
transition={{ ...repeating, times: [0, 0.46, 0.7, 0.82, 1], ease: "easeInOut" }}
/>
<motion.path
d="M414 342C480 344 508 392 571 390C606 389 624 373 646 351"
stroke="url(#conduit-gradient)"
strokeWidth="6"
strokeLinecap="round"
animate={
reduceMotion
? { pathLength: 1, opacity: 0.85 }
: { pathLength: [0, 0, 1, 1, 1], opacity: [0.25, 0.25, 1, 1, 0.45] }
}
transition={{ ...repeating, times: [0, 0.46, 0.7, 0.82, 1], ease: "easeInOut" }}
/>
<motion.circle
r="8"
fill="#FCF9FF"
stroke="#A855F7"
strokeWidth="3"
animate={
reduceMotion
? { cx: 646, cy: 351, opacity: 0 }
: {
cx: [414, 414, 448, 505, 570, 620, 646, 646],
cy: [342, 342, 348, 382, 390, 372, 351, 351],
opacity: [0, 0, 1, 1, 1, 1, 0, 0],
scale: [0.7, 0.7, 1, 1, 1, 1, 0.7, 0.7],
}
}
transition={{ ...repeating, times: [0, 0.49, 0.53, 0.59, 0.65, 0.7, 0.74, 1], ease: "easeInOut" }}
/>
<motion.circle
r="5"
fill="#D8B4FE"
animate={
reduceMotion
? { cx: 646, cy: 351, opacity: 0 }
: {
cx: [414, 414, 448, 505, 570, 620, 646, 646],
cy: [342, 342, 348, 382, 390, 372, 351, 351],
opacity: [0, 0, 1, 1, 1, 1, 0, 0],
}
}
transition={{ ...repeating, times: [0, 0.56, 0.6, 0.66, 0.72, 0.77, 0.81, 1], ease: "easeInOut" }}
/>
</svg>
<div className="receiver-phone-shell">
<span className="phone-side-button phone-side-button-one" />
<span className="phone-side-button phone-side-button-two" />
<div className="receiver-phone-frame">
<div className="phone-screen">
<div className="phone-status-bar">
<span>9:41</span>
<span className="phone-island" />
<span className="phone-signal"> </span>
</div>
<div className="phone-app-bar">
<BrandMark className="phone-brand-mark" />
<strong>Receive</strong>
<span><Icon name="shield" /></span>
</div>
<div className="phone-transfer-card">
<div className="phone-file-art"><Icon name="folder" /></div>
<small>INCOMING FROM ALEX</small>
<strong>summer-photos</strong>
<span>42 files · 486 MB</span>
</div>
<div className="phone-state-area">
<motion.div
className="phone-state is-waiting"
animate={
reduceMotion
? { opacity: 0 }
: { opacity: [1, 1, 0, 0] }
}
transition={{ ...repeating, times: [0, 0.43, 0.5, 1] }}
>
<span className="state-symbol"><Icon name="lock" /></span>
<strong>Waiting for approval</strong>
<small>The sender stays in control.</small>
</motion.div>
<motion.div
className="phone-state is-transferring"
animate={
reduceMotion
? { opacity: 0 }
: { opacity: [0, 0, 1, 1, 0, 0] }
}
transition={{ ...repeating, times: [0, 0.45, 0.51, 0.71, 0.78, 1] }}
>
<span className="transfer-percentage">68<span>%</span></span>
<strong>Receiving securely</strong>
<small>Direct path · 38 MB/s</small>
</motion.div>
<motion.div
className="phone-state is-verified"
animate={
reduceMotion
? { opacity: 1, scale: 1 }
: { opacity: [0, 0, 1, 1], scale: [0.92, 0.92, 1, 1] }
}
transition={{ ...repeating, times: [0, 0.72, 0.79, 1], ease: "easeOut" }}
>
<span className="state-symbol is-success"><Icon name="verified" /></span>
<strong>Received &amp; verified</strong>
<small>Saved to Downloads</small>
</motion.div>
</div>
<div className="phone-home-indicator" />
</div>
</div>
</div>
<motion.div
className="hero-route-card"
animate={
reduceMotion
? { opacity: 1, y: 0 }
: { opacity: [0, 1, 1, 1], y: [-8, 0, 0, 0] }
}
transition={{ ...repeating, times: [0, 0.08, 0.92, 1], ease: "easeOut" }}
>
<span className="route-pulse" />
<span>
<small>ROUTE</small>
<strong>Direct path established</strong>
</span>
<span className="route-latency">18 ms</span>
</motion.div>
<motion.div
className="hero-verified-card"
animate={
reduceMotion
? { opacity: 1, y: 0, scale: 1 }
: {
opacity: [0, 0, 1, 1],
y: [8, 8, 0, 0],
scale: [0.96, 0.96, 1, 1],
}
}
transition={{ ...repeating, times: [0, 0.74, 0.81, 1], ease: "easeOut" }}
>
<span><Icon name="verified" /></span>
<span>
<strong>Content verified</strong>
<small>42 / 42 files match</small>
</span>
</motion.div>
<div className="hero-privacy-rail">
<span><Icon name="lock" /> E2E encrypted</span>
<i />
<span>No hosted copy</span>
<i />
<span>Approval required</span>
</div>
</div>
<figcaption className="sr-only">
A VniDrop transfer moves from a desktop sender to a phone after receiver approval, then is
verified on arrival.
</figcaption>
</figure>
);
}

View File

@@ -1,606 +0,0 @@
/* Coordinated product transfer stage */
.hero-visual {
position: relative;
width: min(100%, 760px);
margin: 0;
}
.hero-stage {
--pointer-x: 0;
--pointer-y: 0;
position: relative;
width: 100%;
aspect-ratio: 1.17;
isolation: isolate;
perspective: 1400px;
}
.hero-stage-grid {
position: absolute;
z-index: -3;
inset: 3% 0 4% 1%;
overflow: hidden;
border: 1px solid rgba(159, 132, 173, 0.15);
border-radius: 44px;
background:
linear-gradient(rgba(110, 86, 122, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(110, 86, 122, 0.055) 1px, transparent 1px),
radial-gradient(circle at 72% 35%, rgba(192, 132, 252, 0.13), transparent 38%),
rgba(255, 255, 255, 0.2);
background-size: 28px 28px, 28px 28px, auto, auto;
mask-image: linear-gradient(to bottom, transparent 0, black 9%, black 88%, transparent 100%);
}
.hero-stage-grid::before,
.hero-stage-grid::after {
position: absolute;
width: 32px;
height: 32px;
border-style: solid;
border-color: rgba(168, 85, 247, 0.35);
content: "";
}
.hero-stage-grid::before {
top: 18px;
left: 18px;
border-width: 1px 0 0 1px;
}
.hero-stage-grid::after {
right: 18px;
bottom: 18px;
border-width: 0 1px 1px 0;
}
.hero-stage-glow {
position: absolute;
z-index: -2;
top: 18%;
right: 9%;
width: 45%;
aspect-ratio: 1;
border-radius: 50%;
background: rgba(168, 85, 247, 0.16);
filter: blur(70px);
transform: translate(
calc(var(--pointer-x) * 9px),
calc(var(--pointer-y) * 9px)
);
}
.stage-coordinate {
position: absolute;
z-index: 1;
color: rgba(87, 70, 95, 0.48);
font-family: var(--font-mono);
font-size: 7px;
font-weight: 750;
letter-spacing: 0.13em;
}
.stage-coordinate-top {
top: 1.5%;
left: 5%;
}
.stage-coordinate-side {
top: 27%;
right: -3%;
transform: rotate(90deg);
}
.desktop-product-shell {
position: absolute;
z-index: 3;
top: 11%;
left: 1.5%;
width: 64%;
height: 55%;
transform: translate(
calc(var(--pointer-x) * -5px),
calc(var(--pointer-y) * -4px)
)
rotateY(calc(var(--pointer-x) * 1.4deg))
rotateX(calc(var(--pointer-y) * -1.2deg));
transform-origin: center;
transition: transform 180ms ease-out;
}
.desktop-product-window {
position: relative;
width: 100%;
height: 100%;
overflow: visible;
border: 1px solid rgba(195, 186, 200, 0.95);
border-radius: 20px;
background: #fcfbfc;
box-shadow:
0 38px 90px rgba(52, 34, 62, 0.16),
0 8px 24px rgba(52, 34, 62, 0.08),
inset 0 1px white;
}
.desktop-product-window::after {
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(118deg, rgba(255, 255, 255, 0.5), transparent 24%, transparent 72%, rgba(168, 85, 247, 0.025));
content: "";
pointer-events: none;
}
.desktop-window-bar {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: 34px;
padding: 0 11px;
border-bottom: 1px solid #e9e5eb;
border-radius: 20px 20px 0 0;
background: rgba(248, 247, 249, 0.92);
}
.window-controls {
display: flex;
gap: 5px;
}
.window-controls i {
width: 7px;
height: 7px;
border-radius: 50%;
background: #d8d2dc;
}
.window-controls i:first-child { background: #c084fc; }
.window-title {
color: #777078;
font-size: 8px;
font-weight: 650;
}
.window-security {
display: inline-flex;
align-items: center;
justify-self: end;
gap: 4px;
color: #7b707f;
font-family: var(--font-mono);
font-size: 6px;
font-weight: 750;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.window-security svg {
width: 9px;
height: 9px;
color: var(--brand-600);
}
.desktop-app-layout {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 50px 1fr;
height: calc(100% - 34px);
overflow: hidden;
border-radius: 0 0 20px 20px;
}
.desktop-sidebar {
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
padding: 9px 0 10px;
border-right: 1px solid #ebe7ed;
background: #f7f6f8;
}
.product-brand-mark {
width: 34px;
height: 34px;
margin-bottom: 2px;
object-fit: contain;
}
.desktop-sidebar-item {
display: grid;
width: 30px;
height: 30px;
place-items: center;
border-radius: 9px;
color: #8c848f;
}
.desktop-sidebar-item.is-active {
background: linear-gradient(135deg, #ead5fe, #f5eafe);
color: var(--brand-700);
box-shadow: inset 0 0 0 1px rgba(168, 85, 247, 0.1);
}
.desktop-sidebar-item svg {
width: 14px;
height: 14px;
}
.desktop-sidebar-spacer { flex: 1; }
.desktop-profile {
display: grid;
width: 26px;
height: 26px;
place-items: center;
border-radius: 9px;
background: #232027;
color: white;
font-size: 7px;
font-weight: 780;
}
.desktop-transfer-view {
display: flex;
min-width: 0;
flex-direction: column;
padding: 14px 16px 12px;
background: #fdfcfd;
}
.desktop-view-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 11px;
}
.desktop-view-heading div {
display: grid;
gap: 2px;
}
.desktop-view-heading small {
color: #9a919e;
font-family: var(--font-mono);
font-size: 6px;
font-weight: 800;
letter-spacing: 0.11em;
}
.desktop-view-heading strong {
color: #211d24;
font-family: var(--font-display);
font-size: 15px;
line-height: 1.2;
letter-spacing: -0.035em;
}
.sharing-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 7px;
border: 1px solid #e2d2ee;
border-radius: 999px;
background: #f6edfe;
color: #6f27e9;
font-size: 7px;
font-weight: 750;
}
.sharing-pill i {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--brand-500);
box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.1);
}
.file-collection-card {
position: relative;
display: grid;
grid-template-columns: 64px 1fr auto;
align-items: center;
gap: 11px;
padding: 9px;
border: 1px solid #e9e5eb;
border-radius: 14px;
background: #f7f6f8;
}
.file-collage {
display: grid;
width: 64px;
height: 52px;
grid-template-columns: 1.15fr 0.85fr;
grid-template-rows: repeat(2, 1fr);
gap: 2px;
overflow: hidden;
border-radius: 9px;
background: #e9e5eb;
}
.file-collage span:first-child {
grid-row: 1 / 3;
background:
radial-gradient(circle at 35% 23%, #ffe6b6 0 8%, transparent 9%),
linear-gradient(145deg, #94b8d5 0 38%, #5a8b8f 39% 57%, #efbc7b 58%);
}
.file-collage span:nth-child(2) {
background: linear-gradient(150deg, #8a76a8 0 35%, #d4a985 36% 68%, #5c4356 69%);
}
.file-collage span:nth-child(3) {
background: linear-gradient(140deg, #ceddca 0 38%, #6d9b77 39% 61%, #d7bd92 62%);
}
.file-collage span:nth-child(4) { display: none; }
.file-collection-copy {
display: grid;
min-width: 0;
}
.file-collection-copy strong {
overflow: hidden;
color: #27222a;
font-size: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-collection-copy > span {
color: #7f7683;
font-size: 7px;
}
.file-collection-copy small {
display: flex;
align-items: center;
gap: 4px;
margin-top: 5px;
color: #85798a;
font-size: 6px;
}
.file-collection-copy svg {
width: 9px;
color: var(--brand-600);
}
.collection-menu {
align-self: start;
padding: 0 2px;
color: #9c929f;
font-size: 9px;
letter-spacing: 1px;
}
.receiver-section-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
margin: 11px 2px 6px;
}
.receiver-section-heading span {
color: #373039;
font-size: 8px;
font-weight: 750;
}
.receiver-section-heading small {
color: #938a96;
font-size: 6px;
}
.desktop-receiver-row {
position: relative;
display: grid;
grid-template-columns: 31px 1fr auto;
align-items: center;
gap: 8px;
min-height: 48px;
padding: 8px 9px 10px;
border: 1px solid #e8e3ea;
border-radius: 12px;
background: white;
box-shadow: 0 5px 15px rgba(49, 36, 55, 0.035);
}
.receiver-avatar {
display: grid;
width: 29px;
height: 29px;
place-items: center;
border-radius: 10px;
background: linear-gradient(135deg, #ead2fe, #c084fc);
color: #5c1bb8;
font-size: 9px;
font-weight: 800;
}
.receiver-identity {
display: grid;
min-width: 0;
}
.receiver-identity strong {
overflow: hidden;
color: #302a33;
font-size: 8px;
text-overflow: ellipsis;
white-space: nowrap;
}
.receiver-identity small {
color: #938a96;
font-size: 6px;
}
.receiver-state-stack {
position: relative;
width: 86px;
height: 16px;
}
.receiver-row-state {
position: absolute;
inset: 0;
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 3px;
font-size: 6px;
font-weight: 720;
white-space: nowrap;
}
.receiver-row-state.is-waiting { color: #9a6716; }
.receiver-row-state.is-sending { color: var(--brand-700); }
.receiver-row-state.is-complete { color: #23855a; }
.receiver-row-state svg { width: 9px; }
.receiver-progress-track {
position: absolute;
right: 9px;
bottom: 5px;
left: 48px;
height: 2px;
overflow: hidden;
border-radius: 999px;
background: #ede8ef;
}
.receiver-progress-track span {
display: block;
width: 100%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--brand-400), var(--brand-700));
transform: scaleX(0);
transform-origin: left;
}
.desktop-transfer-foot {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
padding: 8px 2px 0;
color: #8e8492;
font-size: 6px;
}
.desktop-transfer-foot span {
display: inline-flex;
align-items: center;
gap: 4px;
}
.desktop-transfer-foot svg {
width: 9px;
color: var(--brand-600);
}
.approval-knock-card {
position: absolute;
z-index: 8;
right: -8%;
bottom: -19%;
width: 238px;
padding: 12px;
border: 1px solid rgba(215, 202, 222, 0.95);
border-radius: 16px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 24px 65px rgba(55, 31, 66, 0.2);
backdrop-filter: blur(18px);
}
.approval-knock-top {
display: grid;
grid-template-columns: 30px 1fr 27px;
align-items: center;
gap: 8px;
}
.approval-knock-top > span:nth-child(2) {
display: grid;
}
.approval-knock-top small {
color: #9a8da0;
font-family: var(--font-mono);
font-size: 5.5px;
font-weight: 800;
letter-spacing: 0.09em;
}
.approval-knock-top strong {
color: #27212b;
font-size: 8px;
}
.approval-lock {
display: grid;
width: 27px;
height: 27px;
place-items: center;
border-radius: 9px;
background: var(--brand-100);
color: var(--brand-700);
}
.approval-lock svg { width: 13px; }
.approval-knock-card p {
margin: 9px 0;
color: #756c79;
font-size: 7px;
}
.approval-knock-card p strong { color: #3b323e; }
.approval-knock-actions {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 7px;
}
.approval-knock-actions > span {
display: inline-flex;
min-height: 28px;
align-items: center;
justify-content: center;
gap: 4px;
border: 1px solid #e6e0e9;
border-radius: 8px;
color: #685f6c;
font-size: 7px;
font-weight: 720;
}
.approval-knock-actions .approval-accept {
border-color: transparent;
background: linear-gradient(135deg, var(--brand-500), var(--brand-700));
box-shadow: 0 8px 18px rgba(132, 47, 238, 0.22);
color: white;
}
.approval-accept svg { width: 10px; }
.hero-conduit {
position: absolute;
z-index: 2;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
}

View File

@@ -2,24 +2,13 @@ import type { SVGProps } from "react";
export type IconName =
| "arrow"
| "check"
| "chevron"
| "close"
| "code"
| "devices"
| "file"
| "folder"
| "github"
| "globe"
| "lock"
| "menu"
| "nfc"
| "qr"
| "shield"
| "spark"
| "stop"
| "verified"
| "x";
| "verified";
type IconProps = SVGProps<SVGSVGElement> & {
name: IconName;
@@ -33,21 +22,6 @@ export function Icon({ name, ...props }: IconProps) {
<path d="m14 7 5 5-5 5" />
</>
),
check: <path d="m5 12 4 4L19 6" />,
chevron: <path d="m9 18 6-6-6-6" />,
close: (
<>
<path d="M6 6l12 12" />
<path d="M18 6 6 18" />
</>
),
code: (
<>
<path d="m8 9-4 3 4 3" />
<path d="m16 9 4 3-4 3" />
<path d="m14 5-4 14" />
</>
),
devices: (
<>
<rect x="3" y="5" width="13" height="10" rx="2" />
@@ -55,12 +29,6 @@ export function Icon({ name, ...props }: IconProps) {
<rect x="17" y="8" width="4" height="10" rx="1" />
</>
),
file: (
<>
<path d="M6 3h8l4 4v14H6z" />
<path d="M14 3v5h4M9 13h6M9 17h4" />
</>
),
folder: (
<path d="M3 7.5A2.5 2.5 0 0 1 5.5 5H10l2 2h6.5A2.5 2.5 0 0 1 21 9.5v7a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 16.5z" />
),
@@ -72,30 +40,12 @@ export function Icon({ name, ...props }: IconProps) {
fill="currentColor"
/>
),
globe: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18" />
</>
),
lock: (
<>
<rect x="5" y="10" width="14" height="11" rx="3" />
<path d="M8 10V7a4 4 0 0 1 8 0v3M12 14v3" />
</>
),
menu: (
<>
<path d="M4 7h16M4 12h16M4 17h16" />
</>
),
nfc: (
<>
<path d="M8 7a7 7 0 0 1 0 10M5 4a11 11 0 0 1 0 16" />
<path d="M16 7a7 7 0 0 0 0 10M19 4a11 11 0 0 0 0 16" />
<circle cx="12" cy="12" r="1" />
</>
),
qr: (
<>
<rect x="3" y="3" width="7" height="7" rx="1" />
@@ -110,22 +60,12 @@ export function Icon({ name, ...props }: IconProps) {
<path d="m8.5 12 2.2 2.2 4.8-5" />
</>
),
spark: (
<path d="m12 2 1.6 6.4L20 10l-6.4 1.6L12 18l-1.6-6.4L4 10l6.4-1.6z" />
),
stop: <rect x="5" y="5" width="14" height="14" rx="3" />,
verified: (
<>
<path d="m12 3 2 2.1 2.9-.1.1 2.9 2 2.1-2 2.1-.1 2.9-2.9-.1-2 2.1-2-2.1-2.9.1-.1-2.9-2-2.1 2-2.1.1-2.9 2.9.1z" />
<path d="m9 10 2 2 4-4" />
</>
),
x: (
<>
<path d="M6 6l12 12" />
<path d="M18 6 6 18" />
</>
),
};
return (

View File

@@ -1,277 +0,0 @@
@keyframes scene-orbits { to { transform: rotate(360deg); } }
@keyframes laptop-files {
0%, 100% { transform: translateY(0) rotate(0); }
50% { transform: translateY(-5px) rotate(1deg); }
}
@keyframes conduit-energy { to { stroke-dashoffset: -38; } }
@keyframes gate-hover {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes received-file {
0%, 9% { opacity: 0.55; transform: translateY(12px) scale(0.92); }
17%, 70% { opacity: 1; transform: translateY(0) scale(1); }
83%, 100% { opacity: 0.55; transform: translateY(12px) scale(0.92); }
}
@keyframes hero-packet-one {
0%, 6% { opacity: 0; transform: translate(245px, 320px) scale(0.55) rotate(-8deg); }
14% { opacity: 1; }
43% { opacity: 1; transform: translate(380px, 427px) scale(0.82) rotate(5deg); }
54% { opacity: 1; transform: translate(415px, 430px) scale(0.82) rotate(2deg); }
84% { opacity: 1; transform: translate(556px, 296px) scale(0.65) rotate(10deg); }
92%, 100% { opacity: 0; transform: translate(585px, 270px) scale(0.35) rotate(14deg); }
}
@keyframes hero-packet-two {
0%, 6% { opacity: 0; transform: translate(255px, 327px) scale(0.5) rotate(8deg); }
15% { opacity: 0.85; }
45% { opacity: 0.9; transform: translate(382px, 440px) scale(0.75) rotate(-4deg); }
56% { opacity: 0.9; transform: translate(414px, 436px) scale(0.75) rotate(-2deg); }
85% { opacity: 0.8; transform: translate(560px, 308px) scale(0.55) rotate(-10deg); }
93%, 100% { opacity: 0; transform: translate(588px, 278px) scale(0.25) rotate(-15deg); }
}
@keyframes hero-spark-one {
0%, 12% { opacity: 0; transform: translate(266px, 346px) scale(0); }
19% { opacity: 1; transform: translate(285px, 360px) scale(1); }
29%, 100% { opacity: 0; transform: translate(310px, 380px) scale(0); }
}
@keyframes hero-spark-two {
0%, 77% { opacity: 0; transform: translate(545px, 307px) scale(0); }
87% { opacity: 1; transform: translate(575px, 279px) scale(1); }
97%, 100% { opacity: 0; transform: translate(603px, 260px) scale(0); }
}
@keyframes status-hover {
0%, 100% { margin-top: 0; }
50% { margin-top: -7px; }
}
@keyframes status-pulse {
0%, 100% { box-shadow: 0 0 0 3px rgba(43, 196, 119, 0.1); }
50% { box-shadow: 0 0 0 7px rgba(43, 196, 119, 0); }
}
@keyframes flow-glow {
0%, 100% { transform: scale(0.8); opacity: 0.6; }
50% { transform: scale(1.2); opacity: 1; }
}
@keyframes flow-drop {
0%, 7% { offset-distance: 0%; opacity: 0; }
13% { opacity: 1; }
47% { offset-distance: 51%; opacity: 1; }
57% { offset-distance: 51%; opacity: 1; }
91% { offset-distance: 100%; opacity: 1; }
97%, 100% { offset-distance: 100%; opacity: 0; }
}
@keyframes gate-breathe {
0%, 100% { transform: scale(0.98); }
50% { transform: scale(1.04); }
}
@keyframes complete-ring {
0%, 100% { box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.18); }
50% { box-shadow: 0 0 0 13px rgba(168, 85, 247, 0); }
}
@keyframes invite-panel-enter {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: none; }
}
@keyframes qr-card-float {
0%, 100% { transform: rotate(2deg) translateY(0); }
50% { transform: rotate(-1deg) translateY(-8px); }
}
@keyframes qr-cell {
from { opacity: 0; transform: scale(0.2); }
to { opacity: 1; transform: scale(1); }
}
@keyframes qr-scan {
0%, 100% { top: 40px; opacity: 0; }
12%, 88% { opacity: 1; }
50% { top: 222px; }
}
@keyframes nfc-dot {
0%, 100% { box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.2); }
50% { box-shadow: 0 0 0 19px rgba(168, 85, 247, 0); }
}
@keyframes nfc-wave {
0%, 100% { opacity: 0.25; transform: translateY(-50%) scale(0.88); }
50% { opacity: 1; transform: translateY(-50%) scale(1.04); }
}
@keyframes vnd-drop {
0%, 8% { transform: translateX(-50%) translateY(-16px) rotate(-5deg); }
20%, 78% { transform: translateX(-50%) translateY(0) rotate(-2deg); }
88%, 100% { transform: translateX(-50%) translateY(-16px) rotate(-5deg); }
}
@keyframes avatar-pulse {
0% { box-shadow: 0 0 0 0 rgba(168, 85, 247, 0.24); }
75%, 100% { box-shadow: 0 0 0 14px rgba(168, 85, 247, 0); }
}
@keyframes approval-state-enter {
from { opacity: 0; transform: translateY(7px); }
to { opacity: 1; transform: none; }
}
@keyframes progress-fill {
from { width: 0; }
to { width: 68%; }
}
@keyframes network-dash { to { stroke-dashoffset: -44; } }
@keyframes relay-ring {
0% { opacity: 0.6; transform: translateX(-50%) scale(0.72); }
85%, 100% { opacity: 0; transform: translateX(-50%) scale(1.2); }
}
@keyframes network-packet-top {
0%, 8% { left: 15%; top: 40%; opacity: 0; transform: rotate(-7deg) scale(0.7); }
15% { opacity: 1; }
48% { left: 47%; top: 20%; opacity: 1; transform: rotate(3deg) scale(0.9); }
85% { left: 82%; top: 40%; opacity: 1; transform: rotate(8deg) scale(0.72); }
94%, 100% { left: 85%; top: 42%; opacity: 0; transform: rotate(10deg) scale(0.4); }
}
@keyframes network-packet-bottom {
0%, 8% { left: 16%; top: 50%; opacity: 0; transform: rotate(8deg) scale(0.65); }
15% { opacity: 0.8; }
48% { left: 48%; top: 69%; opacity: 0.8; transform: rotate(-3deg) scale(0.8); }
84% { left: 82%; top: 50%; opacity: 0.8; transform: rotate(-8deg) scale(0.7); }
94%, 100% { left: 85%; top: 48%; opacity: 0; transform: rotate(-10deg) scale(0.4); }
}
@keyframes orbit-spin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes platform-glow {
0%, 100% { opacity: 0.45; transform: scale(0.82); }
50% { opacity: 1; transform: scale(1.15); }
}
@keyframes platform-chip-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@media (max-width: 1120px) {
.flow-layout {
grid-template-columns: minmax(450px, 1fr) minmax(350px, 0.8fr);
gap: 45px;
}
.flow-device { transform: scale(0.88); }
.flow-device-sender { transform: scale(0.88) rotate(-6deg); transform-origin: top left; }
.flow-device-receiver { transform: scale(0.88) rotate(5deg); transform-origin: bottom right; }
.invite-panel {
grid-template-columns: minmax(0, 0.85fr) minmax(420px, 1.15fr);
}
.invite-copy { padding: 45px; }
.network-visual { min-height: 500px; }
}
@media (max-width: 920px) {
.hero-visual {
max-width: 720px;
margin-inline: auto;
}
.flow-layout {
grid-template-columns: 1fr;
}
.flow-visual {
position: relative;
top: auto;
}
.invite-panel {
grid-template-columns: 1fr;
}
.invite-art {
min-height: 450px;
border-top: 1px solid var(--line);
border-left: 0;
}
.network-visual {
min-height: 520px;
}
.platform-orbit {
max-width: 620px;
}
}
@media (max-width: 680px) {
.hero-stage {
width: 112%;
margin-left: -6%;
}
.hero-status-secure {
top: 0;
right: 2%;
transform: scale(0.88) rotate(2deg);
transform-origin: top right;
}
.hero-status-route { left: 4%; transform: scale(0.88) rotate(-3deg); transform-origin: top left; }
.hero-file-label { bottom: 3%; left: 4%; transform: scale(0.88) rotate(-2deg); transform-origin: bottom left; }
.flow-visual {
height: 540px;
border-radius: 21px;
}
.flow-device {
width: 137px;
height: 215px;
}
.flow-device-sender { top: 24px; left: 16px; }
.flow-device-receiver { right: 14px; bottom: 20px; }
.flow-track { inset: 0 -2%; width: 104%; height: 100%; }
.flow-annotation { display: none; }
.flow-step {
grid-template-columns: auto 1fr;
padding: 20px;
}
.invite-tabs button {
min-height: 59px;
flex-direction: column;
gap: 3px;
font-size: 10px;
}
.invite-tabs svg { width: 17px; }
.invite-copy { padding: 42px 24px; }
.invite-art { min-height: 390px; }
.invite-qr-art { transform: scale(0.86) rotate(2deg); }
.invite-nfc-art { transform: scale(0.82); }
.invite-file-art { transform: scale(0.85); }
.approval-body { min-height: 370px; padding: 28px 20px; }
.approval-heading h3 { font-size: 18px; }
.approval-actions { grid-template-columns: 1fr 1.2fr; }
.network-visual {
min-height: 430px;
border-radius: 21px;
}
.network-device {
top: 150px;
width: 77px;
height: 112px;
}
.network-device-left { left: 13px; }
.network-device-right { right: 13px; }
.network-device svg { width: 24px; }
.network-relay-node { bottom: 29px; }
.network-relay-node small { max-width: 160px; text-align: center; }
.network-direct-label { top: 43px; }
.platform-orbit { width: 112%; margin-left: -6%; }
.platform-center { width: 112px; height: 112px; border-radius: 30px; }
.platform-center img { width: 68px; height: 68px; }
.platform-chip { width: 48px; height: 48px; padding: 0; }
.platform-chip .platform-glyph { width: 25px; height: 25px; }
.platform-chip-1 { left: 10%; }
.platform-chip-2 { right: 0; }
.platform-chip-3 { right: -2%; }
.platform-chip-4 { left: 17%; }
}

View File

@@ -1,405 +0,0 @@
/* Invitation methods */
.invite-showcase {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 25px;
background: white;
box-shadow: var(--shadow-lg);
}
.invite-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
border-bottom: 1px solid var(--line);
background: #f7f4f8;
}
.invite-tabs button {
display: inline-flex;
min-height: 66px;
align-items: center;
justify-content: center;
gap: 9px;
border: 0;
border-right: 1px solid var(--line);
background: transparent;
color: var(--ink-muted);
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: color 180ms ease, background 180ms ease;
}
.invite-tabs button:last-child { border-right: 0; }
.invite-tabs button:hover { color: var(--ink); }
.invite-tabs button.is-active {
position: relative;
background: white;
color: var(--brand-600);
}
.invite-tabs button.is-active::after {
position: absolute;
right: 20%;
bottom: -1px;
left: 20%;
height: 2px;
border-radius: 999px 999px 0 0;
background: var(--brand-500);
content: "";
}
.invite-tabs svg { width: 19px; }
.invite-panel {
display: grid;
min-height: 490px;
grid-template-columns: minmax(0, 0.83fr) minmax(480px, 1.17fr);
animation: invite-panel-enter 420ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.invite-copy {
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: center;
padding: 60px;
}
.mono-label {
color: var(--brand-600);
font-family: var(--font-mono);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.11em;
}
.invite-copy h3 {
max-width: 430px;
margin: 12px 0 18px;
font-size: clamp(2rem, 3vw, 3.15rem);
letter-spacing: -0.055em;
line-height: 1.06;
}
.invite-copy > p {
max-width: 440px;
margin-bottom: 30px;
color: var(--ink-soft);
font-size: 15px;
line-height: 1.7;
}
.invite-detail {
display: flex;
gap: 11px;
padding-top: 20px;
border-top: 1px solid var(--line);
}
.invite-detail > svg {
width: 19px;
flex: 0 0 auto;
color: var(--brand-600);
}
.invite-detail strong,
.invite-detail small {
display: block;
}
.invite-detail strong {
margin-bottom: 3px;
color: #3c3440;
font-size: 11px;
}
.invite-detail small {
color: var(--ink-muted);
font-size: 9px;
line-height: 1.5;
}
.invite-art {
position: relative;
display: grid;
overflow: hidden;
place-items: center;
border-left: 1px solid var(--line);
background:
linear-gradient(rgba(116, 86, 130, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(116, 86, 130, 0.05) 1px, transparent 1px),
radial-gradient(circle at center, #fbf7fd, #f2edf5);
background-size: 24px 24px, 24px 24px, auto;
}
.invite-art::before {
position: absolute;
width: 330px;
aspect-ratio: 1;
border-radius: 50%;
background: rgba(168, 85, 247, 0.14);
content: "";
filter: blur(55px);
}
.invite-qr-art {
position: relative;
display: grid;
width: 265px;
height: 265px;
place-items: center;
border: 1px solid #e0d5e5;
border-radius: 25px;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 30px 80px rgba(59, 35, 70, 0.18);
transform: rotate(2deg);
animation: qr-card-float 6s ease-in-out infinite;
}
.invite-qr-grid {
display: grid;
width: 178px;
height: 178px;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(9, 1fr);
gap: 4px;
}
.invite-qr-grid span {
border-radius: 2px;
background: transparent;
}
.invite-qr-grid span.is-filled {
background: #2b2230;
animation: qr-cell 600ms ease backwards;
}
.invite-qr-grid span:nth-child(3n) { animation-delay: 80ms; }
.invite-qr-grid span:nth-child(4n) { animation-delay: 160ms; }
.invite-qr-grid span:nth-child(7n) { animation-delay: 230ms; }
.qr-scan-line {
position: absolute;
right: 34px;
left: 34px;
height: 2px;
background: linear-gradient(90deg, transparent, var(--brand-500), transparent);
box-shadow: 0 0 13px var(--brand-400);
animation: qr-scan 3.2s ease-in-out infinite;
}
.qr-corner {
position: absolute;
width: 25px;
height: 25px;
border-color: var(--brand-500);
border-style: solid;
}
.qr-corner-one { top: 21px; left: 21px; border-width: 2px 0 0 2px; border-radius: 7px 0 0; }
.qr-corner-two { top: 21px; right: 21px; border-width: 2px 2px 0 0; border-radius: 0 7px 0 0; }
.qr-corner-three { right: 21px; bottom: 21px; border-width: 0 2px 2px 0; border-radius: 0 0 7px; }
.qr-corner-four { bottom: 21px; left: 21px; border-width: 0 0 2px 2px; border-radius: 0 0 0 7px; }
.invite-nfc-art {
position: relative;
width: 440px;
height: 330px;
}
.nfc-device {
position: absolute;
top: 10px;
left: 10px;
width: 155px;
height: 300px;
border: 7px solid #d7cfdc;
border-radius: 30px;
background: #211c25;
box-shadow: var(--shadow-lg);
transform: rotate(-8deg);
}
.nfc-device::after {
position: absolute;
top: 11px;
left: 50%;
width: 50px;
height: 7px;
border-radius: 999px;
background: #09070a;
content: "";
transform: translateX(-50%);
}
.nfc-screen-dot {
position: absolute;
top: 50%;
left: 50%;
width: 54px;
height: 54px;
border: 1px solid rgba(192, 132, 252, 0.55);
border-radius: 50%;
background: rgba(168, 85, 247, 0.2);
transform: translate(-50%, -50%);
animation: nfc-dot 2.5s ease-in-out infinite;
}
.nfc-tag {
position: absolute;
right: 25px;
bottom: 30px;
display: grid;
width: 138px;
height: 138px;
place-items: center;
border: 1px solid #d9cfe0;
border-radius: 28px;
background: white;
box-shadow: 0 25px 70px rgba(52, 31, 62, 0.18);
color: var(--brand-600);
transform: rotate(8deg);
}
.nfc-tag svg { width: 46px; }
.nfc-tag small {
position: absolute;
bottom: 18px;
color: var(--ink-muted);
font-family: var(--font-mono);
font-size: 8px;
font-weight: 700;
letter-spacing: 0.08em;
}
.nfc-waves {
position: absolute;
top: 75px;
left: 145px;
width: 185px;
height: 160px;
}
.nfc-waves span {
position: absolute;
top: 50%;
left: 0;
border: 2px solid var(--brand-500);
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
border-radius: 50%;
transform: translateY(-50%);
animation: nfc-wave 2.4s ease-in-out infinite;
}
.nfc-waves span:nth-child(1) { width: 55px; height: 70px; }
.nfc-waves span:nth-child(2) { width: 98px; height: 112px; animation-delay: 180ms; }
.nfc-waves span:nth-child(3) { width: 142px; height: 154px; animation-delay: 360ms; }
.nfc-waves i {
position: absolute;
top: 50%;
left: 15px;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--brand-500);
box-shadow: 0 0 0 8px rgba(168, 85, 247, 0.13);
transform: translateY(-50%);
}
.invite-file-art {
position: relative;
width: 410px;
height: 350px;
}
.vnd-file {
position: absolute;
z-index: 2;
top: 13px;
left: 50%;
display: flex;
width: 218px;
height: 270px;
align-items: center;
flex-direction: column;
justify-content: center;
border: 1px solid #d8cbe0;
border-radius: 23px;
background: linear-gradient(145deg, #fff, #f7f0fb);
box-shadow: 0 30px 80px rgba(59, 35, 70, 0.2);
transform: translateX(-50%) rotate(-2deg);
animation: vnd-drop 4.8s cubic-bezier(0.3, 0.75, 0.3, 1) infinite;
}
.vnd-fold {
position: absolute;
top: -1px;
right: -1px;
width: 58px;
height: 58px;
border-bottom: 1px solid #d1c1dc;
border-left: 1px solid #d1c1dc;
border-radius: 0 23px 0 16px;
background: #e7cdf9;
clip-path: polygon(0 0, 100% 100%, 0 100%);
}
.vnd-file > svg {
width: 58px;
margin-bottom: 20px;
color: var(--brand-600);
}
.vnd-file strong {
font-family: var(--font-display);
font-size: 16px;
}
.vnd-file small {
color: var(--ink-muted);
font-size: 10px;
}
.vnd-file code {
margin-top: 18px;
padding: 5px 10px;
border-radius: 7px;
background: var(--brand-100);
color: var(--brand-600);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 800;
}
.file-tray {
position: absolute;
right: 37px;
bottom: 7px;
left: 37px;
height: 73px;
border: 2px solid #cabbd2;
border-radius: 19px;
background: rgba(237, 228, 242, 0.85);
transform: perspective(250px) rotateX(55deg);
}
.file-tray span {
position: absolute;
inset: 14px;
border: 1px dashed #aa8cbc;
border-radius: 10px;
}
.file-art-orbit {
position: absolute;
border: 1px solid rgba(168, 85, 247, 0.2);
border-radius: 50%;
}
.file-art-orbit-one { inset: 14px 32px 44px; animation: scene-orbits 24s linear infinite; }
.file-art-orbit-two { inset: 52px 70px 80px; border-style: dashed; animation: scene-orbits 18s linear infinite reverse; }

View File

@@ -1,153 +0,0 @@
"use client";
import { useState } from "react";
import { Icon, type IconName } from "@/components/icons";
type Method = "qr" | "nfc" | "file";
const methods: Array<{
id: Method;
icon: IconName;
label: string;
eyebrow: string;
title: string;
text: string;
}> = [
{
id: "qr",
icon: "qr",
label: "QR code",
eyebrow: "IN THE SAME ROOM",
title: "Point. Scan. Connected.",
text: "Show the invitation on one screen and scan it from the other. No contact exchange required.",
},
{
id: "nfc",
icon: "nfc",
label: "NFC",
eyebrow: "THE QUICKEST TAP",
title: "Bring devices together.",
text: "Write or open an invitation with an NFC tag for a physical, close-range handoff.",
},
{
id: "file",
icon: "file",
label: ".vnd file",
eyebrow: "SEND IT ANYWHERE",
title: "An invitation that travels.",
text: "Save the small invitation as a .vnd file, then pass it through the channel you already use.",
},
];
function QrArt() {
const filled = new Set([
0, 1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 28, 29, 30, 31, 32,
36, 38, 40, 42, 44, 48, 49, 50, 51, 52, 54, 56, 58, 60, 62, 64, 66, 67, 69,
72, 73, 74, 76, 78, 79, 80,
]);
return (
<div className="invite-qr-art">
<div className="invite-qr-grid">
{Array.from({ length: 81 }, (_, index) => (
<span key={index} className={filled.has(index) ? "is-filled" : ""} />
))}
</div>
<span className="qr-scan-line" />
<span className="qr-corner qr-corner-one" />
<span className="qr-corner qr-corner-two" />
<span className="qr-corner qr-corner-three" />
<span className="qr-corner qr-corner-four" />
</div>
);
}
function NfcArt() {
return (
<div className="invite-nfc-art">
<div className="nfc-device nfc-device-left">
<span className="nfc-screen-dot" />
</div>
<div className="nfc-waves">
<span />
<span />
<span />
<i />
</div>
<div className="nfc-tag">
<Icon name="nfc" />
<small>VniDrop</small>
</div>
</div>
);
}
function FileArt() {
return (
<div className="invite-file-art">
<span className="file-art-orbit file-art-orbit-one" />
<span className="file-art-orbit file-art-orbit-two" />
<div className="vnd-file">
<span className="vnd-fold" />
<Icon name="file" />
<strong>Weekend trip</strong>
<small>VniDrop invitation</small>
<code>.vnd</code>
</div>
<div className="file-tray">
<span />
</div>
</div>
);
}
export function InvitationShowcase() {
const [active, setActive] = useState<Method>("qr");
const current = methods.find((method) => method.id === active) ?? methods[0];
return (
<div className="invite-showcase">
<div className="invite-tabs" role="tablist" aria-label="Invitation methods">
{methods.map((method) => (
<button
key={method.id}
id={`invite-tab-${method.id}`}
type="button"
role="tab"
aria-selected={active === method.id}
aria-controls="invite-panel"
className={active === method.id ? "is-active" : ""}
onClick={() => setActive(method.id)}
>
<Icon name={method.icon} />
{method.label}
</button>
))}
</div>
<div
id="invite-panel"
className="invite-panel"
role="tabpanel"
aria-labelledby={`invite-tab-${active}`}
key={active}
>
<div className="invite-copy">
<span className="mono-label">{current.eyebrow}</span>
<h3>{current.title}</h3>
<p>{current.text}</p>
<div className="invite-detail">
<Icon name="lock" />
<span>
<strong>The invitation is a private access link.</strong>
<small>Only share it with people you intend to receive the transfer.</small>
</span>
</div>
</div>
<div className="invite-art" aria-hidden="true">
{active === "qr" && <QrArt />}
{active === "nfc" && <NfcArt />}
{active === "file" && <FileArt />}
</div>
</div>
</div>
);
}

View File

@@ -1,51 +0,0 @@
import { Icon } from "@/components/icons";
export function NetworkIllustration() {
return (
<div className="network-visual" aria-hidden="true">
<div className="network-grid" />
<div className="network-device network-device-left">
<span className="network-device-top" />
<Icon name="file" />
<small>SENDER</small>
</div>
<div className="network-device network-device-right">
<span className="network-device-top" />
<Icon name="check" />
<small>RECEIVER</small>
</div>
<svg className="network-routes" viewBox="0 0 760 430">
<defs>
<linearGradient id="network-route-gradient" x1="115" y1="210" x2="650" y2="210" gradientUnits="userSpaceOnUse">
<stop stopColor="#D08BFF" />
<stop offset="0.5" stopColor="#A855F7" />
<stop offset="1" stopColor="#7C2AEF" />
</linearGradient>
<filter id="network-route-glow" x="-30%" y="-100%" width="160%" height="300%">
<feGaussianBlur stdDeviation="8" />
</filter>
</defs>
<path className="network-direct-glow" d="M134 227C279 92 491 92 626 227" />
<path className="network-direct" d="M134 227C279 92 491 92 626 227" />
<path className="network-direct-pulse" d="M134 227C279 92 491 92 626 227" />
<path className="network-relay-base" d="M134 247c126 87 371 87 492 0" />
<path className="network-relay-line" d="M134 247c126 87 371 87 492 0" />
</svg>
<div className="network-direct-label">
<span className="live-dot" />
DIRECT WHEN POSSIBLE
</div>
<div className="network-relay-node">
<span className="relay-ring relay-ring-one" />
<span className="relay-ring relay-ring-two" />
<div className="relay-core">
<Icon name="lock" />
</div>
<strong>Encrypted relay</strong>
<small>Forwards packets · stores no transfer copy</small>
</div>
<div className="network-packet network-packet-one"><Icon name="file" /></div>
<div className="network-packet network-packet-two"><Icon name="file" /></div>
</div>
);
}

View File

@@ -1,190 +0,0 @@
/* Direct / relay network scene */
.network-visual {
position: relative;
width: 100%;
min-height: 520px;
overflow: hidden;
border: 1px solid #302737;
border-radius: 27px;
background: radial-gradient(circle at center, rgba(168, 85, 247, 0.1), transparent 47%), #151117;
}
.network-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 30px 30px;
mask-image: radial-gradient(circle at center, black, transparent 79%);
}
.network-device {
position: absolute;
z-index: 4;
top: 178px;
display: grid;
width: 105px;
height: 145px;
place-items: center;
border: 5px solid #3b3340;
border-radius: 20px;
background: linear-gradient(145deg, #29222d, #171419);
box-shadow: 0 22px 60px rgba(0, 0, 0, 0.38);
color: #d89bff;
}
.network-device-left { left: 30px; transform: rotate(-5deg); }
.network-device-right { right: 30px; transform: rotate(5deg); }
.network-device svg { width: 31px; }
.network-device small {
position: absolute;
bottom: 14px;
color: #756b79;
font-family: var(--font-mono);
font-size: 7px;
font-weight: 800;
letter-spacing: 0.09em;
}
.network-device-top {
position: absolute;
top: 8px;
left: 50%;
width: 35px;
height: 5px;
border-radius: 999px;
background: #080609;
transform: translateX(-50%);
}
.network-routes {
position: absolute;
z-index: 2;
inset: 34px 0 auto;
width: 100%;
height: calc(100% - 40px);
overflow: visible;
}
.network-direct-glow,
.network-direct,
.network-direct-pulse,
.network-relay-base,
.network-relay-line {
fill: none;
stroke-linecap: round;
}
.network-direct-glow {
stroke: rgba(168, 85, 247, 0.26);
stroke-width: 20;
filter: url(#network-route-glow);
}
.network-direct {
stroke: #3d3343;
stroke-width: 8;
}
.network-direct-pulse {
stroke: url(#network-route-gradient);
stroke-width: 3;
stroke-dasharray: 4 18;
animation: network-dash 2.2s linear infinite;
}
.network-relay-base {
stroke: #2e2732;
stroke-width: 5;
}
.network-relay-line {
stroke: #776681;
stroke-width: 2;
stroke-dasharray: 4 11;
animation: network-dash 3.4s linear infinite reverse;
}
.network-direct-label {
position: absolute;
z-index: 5;
top: 62px;
left: 50%;
display: flex;
align-items: center;
gap: 9px;
padding: 7px 10px;
border: 1px solid #372c3c;
border-radius: 999px;
background: rgba(25, 20, 28, 0.82);
color: #9a8fa0;
font-family: var(--font-mono);
font-size: 7px;
font-weight: 800;
letter-spacing: 0.08em;
transform: translateX(-50%);
}
.network-relay-node {
position: absolute;
z-index: 5;
bottom: 39px;
left: 50%;
display: flex;
align-items: center;
flex-direction: column;
transform: translateX(-50%);
}
.relay-core {
position: relative;
z-index: 2;
display: grid;
width: 66px;
height: 66px;
margin-bottom: 11px;
place-items: center;
border: 1px solid #5c426a;
border-radius: 20px;
background: linear-gradient(145deg, #312139, #1d1721);
box-shadow: 0 0 45px rgba(168, 85, 247, 0.18);
color: #d294fc;
transform: rotate(45deg);
}
.relay-core svg { width: 24px; transform: rotate(-45deg); }
.network-relay-node strong { color: #d9d1dc; font-size: 10px; }
.network-relay-node small { color: #756b79; font-size: 7px; }
.relay-ring {
position: absolute;
top: -18px;
left: 50%;
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 50%;
transform: translateX(-50%);
animation: relay-ring 3s ease-out infinite;
}
.relay-ring-one { width: 100px; height: 100px; }
.relay-ring-two { width: 136px; height: 136px; animation-delay: 1.2s; }
.network-packet {
position: absolute;
z-index: 3;
display: grid;
width: 26px;
height: 32px;
place-items: center;
border: 1px solid #aa65d2;
border-radius: 6px;
background: #2c2231;
color: #d69bfa;
opacity: 0;
}
.network-packet svg { width: 13px; }
.network-packet-one { animation: network-packet-top 6s ease-in-out infinite; }
.network-packet-two { animation: network-packet-bottom 7s ease-in-out 2.2s infinite; }

View File

@@ -1,29 +0,0 @@
import { BrandMark } from "@/components/brand";
import { PlatformIcon, supportedPlatforms } from "@/components/platform-icon";
export function PlatformOrbit() {
return (
<div
className="platform-orbit"
role="img"
aria-label="VniDrop supports Android, iOS, macOS, Windows, and Linux"
>
<div className="platform-ring platform-ring-outer" aria-hidden="true" />
<div className="platform-ring platform-ring-inner" aria-hidden="true" />
<div className="platform-center" aria-hidden="true">
<span className="platform-center-glow" />
<BrandMark />
</div>
{supportedPlatforms.map((platform, index) => (
<span
key={platform}
className={`platform-chip platform-chip-${index + 1}`}
title={platform}
>
<PlatformIcon className="platform-glyph" platform={platform} />
<span className="sr-only">{platform}</span>
</span>
))}
</div>
);
}

View File

@@ -1,103 +0,0 @@
/* Cross-platform orbit */
.platform-orbit {
position: relative;
width: min(100%, 610px);
aspect-ratio: 1;
margin: 0 auto;
}
.platform-ring {
position: absolute;
top: 50%;
left: 50%;
border: 1px solid #e2d8e7;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.platform-ring::before,
.platform-ring::after {
position: absolute;
border-radius: 50%;
background: var(--brand-400);
box-shadow: 0 0 0 7px rgba(168, 85, 247, 0.08);
content: "";
}
.platform-ring-outer {
width: 88%;
height: 88%;
animation: orbit-spin 35s linear infinite;
}
.platform-ring-outer::before { top: 13%; left: 10%; width: 7px; height: 7px; }
.platform-ring-outer::after { right: 7%; bottom: 23%; width: 5px; height: 5px; }
.platform-ring-inner {
width: 58%;
height: 58%;
border-style: dashed;
animation: orbit-spin 25s linear infinite reverse;
}
.platform-ring-inner::before { top: 48%; left: -4px; width: 7px; height: 7px; }
.platform-ring-inner::after { top: 4%; right: 23%; width: 5px; height: 5px; }
.platform-center {
position: absolute;
z-index: 2;
top: 50%;
left: 50%;
display: grid;
width: 145px;
height: 145px;
place-items: center;
border: 1px solid #e5d8eb;
border-radius: 38px;
background: white;
box-shadow: 0 30px 80px rgba(55, 29, 67, 0.16);
transform: translate(-50%, -50%) rotate(-3deg);
}
.platform-center img { position: relative; z-index: 2; width: 87px; height: 87px; }
.platform-center-glow {
position: absolute;
inset: -45px;
border-radius: 50%;
background: rgba(168, 85, 247, 0.18);
filter: blur(42px);
animation: platform-glow 5s ease-in-out infinite;
}
.platform-chip {
position: absolute;
z-index: 4;
display: inline-flex;
align-items: center;
justify-content: center;
width: 58px;
height: 58px;
border: 1px solid #e2d9e6;
border-radius: 18px;
background: rgba(255, 255, 255, 0.93);
box-shadow: 0 12px 35px rgba(53, 34, 61, 0.1);
color: #4b4350;
animation: platform-chip-float 5s ease-in-out infinite;
}
.platform-chip .platform-glyph {
width: 30px;
height: 30px;
}
.platform-chip-1 { top: 8%; left: 17%; animation-delay: -1s; }
.platform-chip-2 { top: 19%; right: 5%; animation-delay: -2.7s; }
.platform-chip-3 { right: 1%; bottom: 22%; animation-delay: -4s; }
.platform-chip-4 { bottom: 7%; left: 24%; animation-delay: -0.5s; }
.platform-chip-5 { top: 45%; left: 0; animation-delay: -3.2s; }
.platform-glyph[data-platform="android"] { color: #2fbf71; }
.platform-glyph[data-platform="ios"] { color: #201c23; }
.platform-glyph[data-platform="macos"] { color: #1687f8; }
.platform-glyph[data-platform="windows"] { color: #008de5; }

View File

@@ -1,278 +0,0 @@
/* Five-step transfer flow */
.flow-layout {
display: grid;
grid-template-columns: minmax(520px, 1.1fr) minmax(390px, 0.9fr);
align-items: start;
gap: 80px;
}
.flow-visual {
position: sticky;
top: 115px;
height: 660px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 27px;
background:
linear-gradient(rgba(112, 85, 124, 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(112, 85, 124, 0.06) 1px, transparent 1px),
linear-gradient(145deg, #fdfbfe, #f4eff6);
background-size: 28px 28px, 28px 28px, auto;
box-shadow: inset 0 1px white, var(--shadow-sm);
}
.flow-visual::after {
position: absolute;
inset: 0;
background: radial-gradient(circle at center, transparent 20%, rgba(248, 244, 250, 0.65) 78%);
content: "";
pointer-events: none;
}
.flow-visual-glow {
position: absolute;
top: 30%;
left: 32%;
width: 40%;
aspect-ratio: 1;
border-radius: 50%;
background: rgba(168, 85, 247, 0.18);
filter: blur(55px);
animation: flow-glow 7s ease-in-out infinite;
}
.flow-track {
position: absolute;
z-index: 2;
inset: 4% 5% auto;
width: 90%;
height: 92%;
overflow: visible;
}
.flow-track-shadow,
.flow-track-base,
.flow-track-energy {
fill: none;
stroke-linecap: round;
}
.flow-track-shadow {
stroke: rgba(168, 85, 247, 0.22);
stroke-width: 34;
filter: url(#flow-blur);
}
.flow-track-base {
stroke: #e4dae9;
stroke-width: 18;
}
.flow-track-energy {
stroke: url(#flow-gradient);
stroke-width: 6;
stroke-dasharray: 3 17;
animation: conduit-energy 2.1s linear infinite;
}
.flow-moving-drop,
.flow-moving-halo {
offset-path: path("M132 141c216 14 240 88 153 159-89 73-43 157 81 160");
offset-rotate: 0deg;
animation: flow-drop 8s cubic-bezier(0.45, 0.08, 0.35, 0.98) infinite;
}
.flow-moving-halo {
animation-delay: -0.08s;
}
.flow-gate {
transform-origin: 241px 299px;
animation: gate-breathe 3.5s ease-in-out infinite;
}
.flow-device {
position: absolute;
z-index: 4;
display: grid;
width: 168px;
height: 258px;
place-items: center;
border: 7px solid #d7cfdc;
border-radius: 28px;
background: #211d24;
box-shadow: 0 24px 55px rgba(48, 31, 56, 0.18), inset 0 0 0 1px #ada2b3;
}
.flow-device-sender {
top: 46px;
left: 31px;
transform: rotate(-6deg);
}
.flow-device-receiver {
right: 25px;
bottom: 32px;
transform: rotate(5deg);
}
.flow-device-camera {
position: absolute;
top: 8px;
left: 50%;
width: 54px;
height: 7px;
border-radius: 999px;
background: #0c0a0e;
transform: translateX(-50%);
}
.flow-device-content {
display: flex;
width: 100%;
height: 100%;
align-items: center;
flex-direction: column;
justify-content: center;
padding: 25px 15px 15px;
color: white;
text-align: center;
}
.flow-mini-label {
margin-bottom: 20px;
color: #8c7e93;
font-family: var(--font-mono);
font-size: 7px;
font-weight: 800;
letter-spacing: 0.12em;
}
.flow-file-stack {
position: relative;
width: 74px;
height: 82px;
margin-bottom: 16px;
}
.flow-file-stack span {
position: absolute;
display: grid;
width: 50px;
height: 66px;
place-items: center;
border: 1px solid rgba(216, 180, 254, 0.5);
border-radius: 9px;
background: #392e40;
color: #d9a8fb;
}
.flow-file-stack span:nth-child(1) { left: 0; transform: rotate(-10deg); }
.flow-file-stack span:nth-child(2) { right: 0; transform: rotate(10deg); }
.flow-file-stack span:nth-child(3) { left: 12px; background: #f7effc; color: var(--brand-600); }
.flow-file-stack svg { width: 22px; }
.flow-device-content strong {
font-family: var(--font-display);
font-size: 13px;
}
.flow-device-content small {
color: #928798;
font-size: 9px;
}
.flow-complete-ring {
display: grid;
width: 70px;
height: 70px;
margin-bottom: 18px;
place-items: center;
border: 1px solid rgba(216, 180, 254, 0.4);
border-radius: 50%;
background: radial-gradient(circle, rgba(168, 85, 247, 0.26), rgba(168, 85, 247, 0.05));
color: #d59aff;
animation: complete-ring 3.5s ease-in-out infinite;
}
.flow-complete-ring svg {
width: 30px;
stroke-width: 2.4;
}
.flow-annotation {
position: absolute;
z-index: 5;
padding: 5px 8px;
border: 1px solid #ded4e3;
border-radius: 7px;
background: rgba(255, 255, 255, 0.85);
color: var(--brand-600);
font-family: var(--font-mono);
font-size: 7px;
font-weight: 800;
letter-spacing: 0.08em;
}
.flow-annotation-one { top: 35%; right: 12%; transform: rotate(4deg); }
.flow-annotation-two { bottom: 11%; left: 25%; transform: rotate(-4deg); }
.flow-steps {
display: grid;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.flow-step {
display: grid;
grid-template-columns: auto 1fr;
gap: 18px;
padding: 24px;
border: 1px solid var(--line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.8);
transition: transform 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
}
.flow-step:hover {
transform: translateX(6px);
border-color: var(--brand-300);
box-shadow: var(--shadow-sm);
}
.flow-step-icon {
display: grid;
width: 44px;
height: 44px;
place-items: center;
border-radius: 12px;
background: var(--brand-100);
color: var(--brand-600);
}
.flow-step-icon svg { width: 21px; }
.flow-step-number {
display: inline-block;
margin-bottom: 2px;
color: var(--brand-500);
font-family: var(--font-mono);
font-size: 9px;
font-weight: 800;
}
.flow-step h3 {
margin-bottom: 7px;
font-size: 18px;
letter-spacing: -0.025em;
}
.flow-step p {
margin-bottom: 0;
color: var(--ink-muted);
font-size: 13px;
line-height: 1.58;
}

View File

@@ -1,108 +0,0 @@
import { Icon, type IconName } from "@/components/icons";
const steps: Array<{
number: string;
icon: IconName;
title: string;
text: string;
}> = [
{
number: "01",
icon: "folder",
title: "Pick anything",
text: "Choose one file, a whole batch, or a complete folder. Its structure stays intact.",
},
{
number: "02",
icon: "qr",
title: "Pass a tiny invitation",
text: "Share a QR code, tap an NFC tag, or send a portable .vnd invitation file.",
},
{
number: "03",
icon: "shield",
title: "Let them knock",
text: "Every new receiver asks first by default. You choose approve or refuse.",
},
{
number: "04",
icon: "verified",
title: "Stream and verify",
text: "Files move over an authenticated encrypted connection and are verified as they arrive.",
},
{
number: "05",
icon: "stop",
title: "Close the door",
text: "Follow each receiver, cancel in progress, or stop sharing to revoke the invitation.",
},
];
export function TransferFlow() {
return (
<div className="flow-layout">
<div className="flow-visual" aria-hidden="true">
<div className="flow-visual-glow" />
<div className="flow-device flow-device-sender">
<span className="flow-device-camera" />
<div className="flow-device-content">
<span className="flow-mini-label">SELECTED</span>
<div className="flow-file-stack">
<span><Icon name="file" /></span>
<span><Icon name="file" /></span>
<span><Icon name="file" /></span>
</div>
<strong>Project files</strong>
<small>18 items · 248 MB</small>
</div>
</div>
<svg className="flow-track" viewBox="0 0 480 600">
<defs>
<linearGradient id="flow-gradient" x1="90" y1="80" x2="401" y2="520" gradientUnits="userSpaceOnUse">
<stop stopColor="#C778FF" />
<stop offset="0.5" stopColor="#A855F7" />
<stop offset="1" stopColor="#7628EA" />
</linearGradient>
<filter id="flow-blur" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="18" />
</filter>
</defs>
<path className="flow-track-shadow" d="M132 141c216 14 240 88 153 159-89 73-43 157 81 160" />
<path className="flow-track-base" d="M132 141c216 14 240 88 153 159-89 73-43 157 81 160" />
<path className="flow-track-energy" d="M132 141c216 14 240 88 153 159-89 73-43 157 81 160" />
<circle className="flow-moving-drop" cx="0" cy="0" r="11" fill="url(#flow-gradient)" />
<circle className="flow-moving-halo" cx="0" cy="0" r="24" fill="#A855F7" opacity="0.18" filter="url(#flow-blur)" />
<g className="flow-gate">
<rect x="197" y="274" width="88" height="50" rx="17" fill="#FFFCFF" stroke="#D8B4FE" />
<path d="m241 286 12 5v8.5c0 7.2-5.1 12.1-12 14.2-6.9-2.1-12-7-12-14.2V291Z" fill="url(#flow-gradient)" />
<path d="m235.5 299 3.5 3.5 7.5-8" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" />
</g>
</svg>
<div className="flow-device flow-device-receiver">
<span className="flow-device-camera" />
<div className="flow-device-content flow-received-content">
<span className="flow-complete-ring"><Icon name="check" /></span>
<strong>All received</strong>
<small>18 items verified</small>
</div>
</div>
<span className="flow-annotation flow-annotation-one">E2E</span>
<span className="flow-annotation flow-annotation-two">VERIFIED</span>
</div>
<ol className="flow-steps">
{steps.map((step) => (
<li key={step.number} className="flow-step">
<div className="flow-step-icon">
<Icon name={step.icon} />
</div>
<div>
<span className="flow-step-number">{step.number}</span>
<h3>{step.title}</h3>
<p>{step.text}</p>
</div>
</li>
))}
</ol>
</div>
);
}