feat(docs): streamline product and privacy pages

This commit is contained in:
2026-07-16 19:49:17 +02:00
parent 0a185077af
commit 526a5da444
16 changed files with 869 additions and 1755 deletions

View File

@@ -1,72 +1,36 @@
.site-footer {
padding-top: 75px;
padding-top: 42px;
background: var(--paper);
}
.footer-main {
display: grid;
grid-template-columns: 1fr auto;
gap: 60px;
padding-bottom: 70px;
}
.footer-brand {
.footer-inner {
display: flex;
align-items: flex-start;
gap: 15px;
align-items: center;
justify-content: space-between;
gap: 40px;
padding-bottom: 38px;
}
.footer-mark {
display: grid;
width: 48px;
height: 48px;
flex: 0 0 auto;
place-items: center;
border: 1px solid var(--line);
border-radius: 14px;
background: white;
}
.footer-mark img {
width: 32px;
height: 32px;
}
.footer-brand p {
margin-bottom: 0;
color: var(--ink-muted);
font-size: 13px;
}
.footer-brand .footer-name {
margin-bottom: 3px;
.footer-identity {
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--ink);
font-family: var(--font-display);
font-size: 21px;
font-size: 18px;
font-weight: 750;
letter-spacing: -0.04em;
}
.footer-identity img {
width: 38px;
height: 38px;
}
.footer-links {
display: grid;
grid-template-columns: repeat(2, minmax(140px, 1fr));
gap: 70px;
}
.footer-links > div {
display: flex;
flex-direction: column;
gap: 9px;
}
.footer-links .footer-label {
margin-bottom: 7px;
color: var(--ink-muted);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
align-items: center;
gap: 24px;
}
.footer-links a {
@@ -74,8 +38,8 @@
align-items: center;
gap: 6px;
color: var(--ink-soft);
font-size: 13px;
font-weight: 600;
font-size: 12px;
font-weight: 620;
}
.footer-links a:hover {
@@ -83,15 +47,15 @@
}
.footer-links svg {
width: 13px;
width: 14px;
}
.footer-bottom {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 22px;
padding-bottom: 28px;
padding-top: 20px;
padding-bottom: 26px;
border-top: 1px solid var(--line);
color: var(--ink-muted);
font-size: 11px;

View File

@@ -97,7 +97,7 @@
}
.hero-lead {
max-width: 610px;
max-width: 590px;
margin-bottom: 30px;
color: var(--ink-soft);
font-size: 18px;
@@ -138,157 +138,69 @@
}
.hero-platforms li {
padding: 4px 8px;
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: 1px solid var(--line);
border-radius: 7px;
background: rgba(255, 255, 255, 0.65);
border-radius: 10px;
background: rgba(255, 255, 255, 0.78);
box-shadow: 0 7px 18px rgba(55, 35, 63, 0.055);
}
.hero-platforms .platform-glyph {
width: 20px;
height: 20px;
}
.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; }
.hero-art-reveal {
min-width: 0;
}
.trust-section {
.hero-app-preview {
position: relative;
z-index: 3;
margin-top: -32px;
width: min(112%, 850px);
margin: 0 0 0 -3%;
isolation: isolate;
}
.trust-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
overflow: hidden;
border: 1px solid var(--line);
border-radius: 23px;
background: rgba(255, 255, 255, 0.94);
box-shadow: var(--shadow-lg);
backdrop-filter: blur(12px);
.hero-app-preview::before {
position: absolute;
z-index: -1;
inset: 14% 8% 8% 10%;
border-radius: 48%;
background: rgba(168, 85, 247, 0.2);
filter: blur(70px);
content: "";
}
.trust-item {
display: flex;
min-height: 124px;
align-items: center;
gap: 19px;
padding: 24px 28px;
}
.trust-item + .trust-item {
border-left: 1px solid var(--line);
}
.trust-number {
color: var(--brand-500);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 800;
}
.trust-item strong {
.hero-app-preview-image {
display: block;
margin-bottom: 3px;
font-family: var(--font-display);
font-size: 18px;
letter-spacing: -0.025em;
width: 100%;
height: auto;
filter: drop-shadow(0 28px 42px rgba(36, 24, 42, 0.2));
}
.trust-item p {
margin: 0;
color: var(--ink-muted);
font-size: 13px;
line-height: 1.45;
.simple-section {
padding: 124px 0;
}
.word-stream {
overflow: hidden;
padding: 92px 0 28px;
color: #ebe5ee;
font-family: var(--font-display);
font-size: clamp(3.5rem, 7vw, 7.6rem);
font-weight: 800;
letter-spacing: -0.055em;
line-height: 1;
white-space: nowrap;
}
.word-stream > div {
display: flex;
width: max-content;
align-items: center;
gap: 35px;
animation: word-stream 36s linear infinite;
}
.word-stream i {
color: var(--brand-300);
font-size: 0.18em;
font-style: normal;
}
.section {
position: relative;
padding: 132px 0;
}
.section-heading {
margin-bottom: 64px;
}
.section-heading h2,
.privacy-intro h2,
.network-copy h2,
.platforms-copy h2,
.final-cta h2,
.approval-copy h2 {
margin-bottom: 20px;
font-size: clamp(2.7rem, 4.4vw, 4.8rem);
font-weight: 740;
letter-spacing: -0.06em;
line-height: 1.02;
}
.section-heading p,
.section-lead {
color: var(--ink-soft);
font-size: 17px;
line-height: 1.72;
}
.section-heading-centered {
max-width: 810px;
margin-right: auto;
margin-left: auto;
text-align: center;
}
.section-heading-centered p {
max-width: 620px;
margin-right: auto;
margin-bottom: 0;
margin-left: auto;
}
.section-heading-split {
display: grid;
grid-template-columns: 1.1fr 0.72fr;
align-items: end;
gap: 90px;
}
.section-heading-split h2 {
margin-bottom: 0;
}
.section-heading-split p {
margin-bottom: 6px;
.simple-heading {
max-width: 760px;
margin-bottom: 58px;
}
.kicker {
display: inline-block;
margin-bottom: 18px;
margin-bottom: 17px;
color: var(--brand-600);
font-family: var(--font-mono);
font-size: 11px;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.13em;
}
@@ -297,405 +209,150 @@
color: #d89bff;
}
.flow-section {
padding-top: 105px;
.simple-heading h2,
.trust-summary-copy h2 {
margin-bottom: 18px;
font-size: clamp(2.6rem, 4.2vw, 4.6rem);
font-weight: 740;
letter-spacing: -0.06em;
line-height: 1.02;
}
.invitation-section {
overflow: hidden;
background: var(--paper-warm);
}
.approval-section {
padding: 150px 0;
}
.approval-layout {
display: grid;
grid-template-columns: minmax(0, 0.86fr) minmax(480px, 1.14fr);
align-items: center;
gap: 100px;
}
.approval-copy h2 {
max-width: 620px;
}
.approval-copy .section-lead {
max-width: 570px;
margin-bottom: 27px;
}
.check-list {
display: grid;
gap: 13px;
margin: 0 0 25px;
padding: 0;
list-style: none;
}
.check-list li {
display: flex;
align-items: center;
gap: 11px;
color: #3c3540;
font-size: 14px;
font-weight: 600;
}
.check-list svg {
width: 20px;
height: 20px;
padding: 4px;
border-radius: 50%;
background: var(--brand-100);
color: var(--brand-600);
stroke-width: 2.5;
}
.approval-note {
max-width: 555px;
.simple-heading p {
max-width: 650px;
margin: 0;
padding: 15px 17px;
border: 1px solid #eadff0;
border-radius: 12px;
background: #faf7fb;
color: var(--ink-muted);
font-size: 12px;
line-height: 1.55;
color: var(--ink-soft);
font-size: 17px;
line-height: 1.7;
}
.approval-demo-wrap {
position: relative;
}
.demo-hint {
position: absolute;
right: 22px;
bottom: -45px;
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--brand-600);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.07em;
transform: rotate(-2deg);
}
.demo-hint svg {
width: 15px;
}
.privacy-principles-section {
position: relative;
overflow: hidden;
padding: 145px 0 135px;
background:
radial-gradient(circle at 90% 8%, rgba(168, 85, 247, 0.18), transparent 25%),
radial-gradient(circle at 5% 90%, rgba(124, 42, 239, 0.12), transparent 25%),
var(--dark);
color: white;
}
.privacy-principles-section::after {
position: absolute;
inset: 0;
background-image: radial-gradient(rgba(255, 255, 255, 0.12) 0.7px, transparent 0.7px);
background-size: 22px 22px;
content: "";
opacity: 0.13;
pointer-events: none;
}
.privacy-principles-section .page-shell {
position: relative;
z-index: 1;
}
.privacy-intro {
max-width: 890px;
margin-bottom: 65px;
}
.privacy-intro h2 {
max-width: 840px;
}
.privacy-intro > p {
max-width: 700px;
margin-bottom: 0;
color: #bdb4c2;
font-size: 18px;
line-height: 1.72;
}
.privacy-point-grid {
.simple-steps {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
grid-template-columns: repeat(3, 1fr);
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
}
.privacy-point {
min-height: 275px;
padding: 24px;
border: 1px solid var(--dark-line);
border-radius: 20px;
background: rgba(255, 255, 255, 0.035);
transition: border-color 200ms ease, transform 200ms ease, background 200ms ease;
.simple-step {
min-height: 235px;
padding: 30px 30px 36px;
}
.privacy-point:hover {
transform: translateY(-5px);
border-color: rgba(192, 132, 252, 0.45);
background: rgba(255, 255, 255, 0.055);
.simple-step + .simple-step {
border-left: 1px solid var(--line);
}
.privacy-point-top {
.simple-step-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 43px;
margin-bottom: 46px;
}
.privacy-point-top > span {
display: grid;
width: 44px;
height: 44px;
place-items: center;
border: 1px solid rgba(192, 132, 252, 0.28);
border-radius: 12px;
background: rgba(168, 85, 247, 0.1);
color: var(--brand-400);
}
.privacy-point-top svg {
width: 22px;
}
.privacy-point-top small {
color: #706775;
.simple-step-top span {
color: var(--brand-600);
font-family: var(--font-mono);
font-size: 10px;
font-size: 11px;
font-weight: 800;
}
.privacy-point h3 {
.simple-step-top svg {
width: 24px;
height: 24px;
color: var(--brand-500);
}
.simple-step h3 {
margin-bottom: 10px;
font-size: 19px;
letter-spacing: -0.025em;
font-size: 22px;
letter-spacing: -0.035em;
}
.privacy-point p {
margin-bottom: 0;
color: #a99fac;
font-size: 13px;
.simple-step p {
max-width: 310px;
margin: 0;
color: var(--ink-muted);
font-size: 14px;
line-height: 1.65;
}
.privacy-policy-link {
display: flex;
.trust-summary-section {
padding: 112px 0;
background: #171319;
color: white;
}
.trust-summary {
display: grid;
grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
align-items: start;
gap: 100px;
}
.trust-summary-copy {
max-width: 520px;
}
.trust-summary-copy p {
margin-bottom: 28px;
color: #b7adb9;
font-size: 16px;
line-height: 1.72;
}
.simple-text-link {
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 30px;
margin-top: 38px;
padding-top: 27px;
gap: 9px;
color: #ddaaff;
font-size: 14px;
font-weight: 720;
}
.simple-text-link svg {
width: 17px;
}
.simple-text-link:hover {
color: white;
}
.trust-summary-list {
border-top: 1px solid var(--dark-line);
}
.privacy-policy-link p {
.trust-summary-item {
display: grid;
grid-template-columns: 44px 1fr;
gap: 18px;
padding: 24px 0;
border-bottom: 1px solid var(--dark-line);
}
.trust-summary-item > span {
display: grid;
width: 42px;
height: 42px;
place-items: center;
border: 1px solid rgba(192, 132, 252, 0.24);
border-radius: 12px;
color: var(--brand-400);
}
.trust-summary-item svg {
width: 21px;
}
.trust-summary-item h3 {
margin-bottom: 6px;
font-size: 18px;
letter-spacing: -0.025em;
}
.trust-summary-item p {
max-width: 530px;
margin: 0;
color: #a99fac;
font-size: 13px;
line-height: 1.62;
}
.privacy-policy-link a {
display: inline-flex;
align-items: center;
gap: 10px;
color: #dca7ff;
font-size: 14px;
font-weight: 750;
}
.privacy-policy-link svg {
width: 18px;
transition: transform 180ms ease;
}
.privacy-policy-link a:hover svg {
transform: translateX(4px);
}
.network-section {
padding: 150px 0;
background: #0f0c11;
color: white;
}
.network-layout {
display: grid;
grid-template-columns: minmax(0, 0.78fr) minmax(580px, 1.22fr);
align-items: center;
gap: 75px;
}
.network-copy h2 {
font-size: clamp(2.6rem, 4vw, 4.2rem);
}
.network-copy > p {
margin-bottom: 35px;
color: #aaa0ae;
font-size: 16px;
line-height: 1.75;
}
.network-copy blockquote {
position: relative;
margin: 0;
padding: 20px 0 20px 25px;
border-left: 2px solid var(--brand-500);
color: #f0e9f3;
font-family: var(--font-display);
font-size: 20px;
font-weight: 630;
line-height: 1.45;
}
.network-copy blockquote span {
position: absolute;
top: -14px;
left: 12px;
color: var(--brand-500);
font-family: Georgia, serif;
font-size: 44px;
opacity: 0.55;
}
.platforms-section {
overflow: hidden;
padding: 155px 0;
}
.platforms-layout {
display: grid;
grid-template-columns: minmax(0, 0.86fr) minmax(520px, 1.14fr);
align-items: center;
gap: 75px;
}
.platforms-copy .section-lead {
max-width: 570px;
margin-bottom: 29px;
}
.platform-feature-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.platform-feature-list span {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 11px;
border: 1px solid var(--line);
border-radius: 9px;
background: white;
color: var(--ink-soft);
font-size: 12px;
font-weight: 620;
}
.platform-feature-list svg {
width: 16px;
color: var(--brand-600);
}
.orbit-wrap {
min-width: 0;
}
.final-cta-section {
padding: 0 0 40px;
}
.final-cta {
position: relative;
overflow: hidden;
padding: 100px 50px;
border-radius: 31px;
background:
linear-gradient(125deg, rgba(255, 255, 255, 0.06), transparent 40%),
linear-gradient(135deg, #25172f, #171019 55%, #28143a);
color: white;
text-align: center;
}
.final-cta::after {
position: absolute;
inset: 0;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: inherit;
content: "";
pointer-events: none;
}
.final-cta h2 {
position: relative;
z-index: 1;
max-width: 850px;
margin-right: auto;
margin-left: auto;
font-size: clamp(2.8rem, 5vw, 5.4rem);
}
.final-cta > p {
position: relative;
z-index: 1;
max-width: 600px;
margin: 0 auto 31px;
color: #bfb4c4;
font-size: 16px;
}
.final-cta > .kicker {
position: relative;
z-index: 1;
}
.final-cta-actions {
position: relative;
z-index: 1;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.final-cta-orb {
position: absolute;
border-radius: 50%;
background: var(--brand-500);
filter: blur(90px);
opacity: 0.18;
}
.final-cta-orb-one {
top: -140px;
left: -70px;
width: 380px;
height: 380px;
animation: cta-orb 12s ease-in-out infinite alternate;
}
.final-cta-orb-two {
right: -80px;
bottom: -200px;
width: 430px;
height: 430px;
animation: cta-orb 15s ease-in-out -4s infinite alternate-reverse;
}

View File

@@ -7,16 +7,8 @@ import "../components/header.css";
import "./home.css";
import "./footer.css";
import "./privacy/privacy-document.css";
import "../components/hero-scene.css";
import "../components/hero-device.css";
import "../components/transfer-flow.css";
import "../components/invitation-scene.css";
import "../components/approval-scene.css";
import "../components/network-scene.css";
import "../components/platform-scene.css";
import "./responsive.css";
import "./privacy/privacy-responsive.css";
import "../components/illustration-motion.css";
export const metadata: Metadata = {
title: {

View File

@@ -1,48 +1,54 @@
import Image from "next/image";
import Link from "next/link";
import { ApprovalDemo } from "@/components/approval-demo";
import { HeroIllustration } from "@/components/hero-illustration";
import { Icon, type IconName } from "@/components/icons";
import { InvitationShowcase } from "@/components/invitation-showcase";
import { NetworkIllustration } from "@/components/network-illustration";
import { PlatformOrbit } from "@/components/platform-orbit";
import { PlatformIcon, supportedPlatforms } from "@/components/platform-icon";
import { Reveal } from "@/components/reveal";
import { TransferFlow } from "@/components/transfer-flow";
const trustItems = [
{ value: "No account", detail: "Open the app and start sharing" },
{ value: "No hosted copy", detail: "Your transfer is not parked in our cloud" },
{ value: "No silent overwrite", detail: "Existing files stay untouched" },
];
const privacyPoints: Array<{
const steps: Array<{
icon: IconName;
number: string;
title: string;
text: string;
}> = [
{
icon: "lock",
icon: "folder",
number: "01",
title: "Encrypted in transit",
text: "Each connection is authenticated and end-to-end encrypted, including through a relay.",
title: "Choose what to send",
text: "Pick files, a batch, or a complete folder. VniDrop preserves the folder structure.",
},
{
icon: "qr",
number: "02",
title: "Share an invitation",
text: "Introduce the devices with a QR code, NFC tap, or portable .vnd invitation.",
},
{
icon: "shield",
number: "02",
title: "Approval by default",
text: "An invitation helps devices meet. It is not automatic permission to download.",
number: "03",
title: "Approve and transfer",
text: "The receiver asks first. Once approved, the files move over an authenticated encrypted connection.",
},
];
const trustItems: Array<{
icon: IconName;
title: string;
text: string;
}> = [
{
icon: "devices",
title: "Direct when possible",
text: "Devices connect directly when they can. An encrypted relay forwards traffic when they cannot.",
},
{
icon: "lock",
title: "No hosted copy",
text: "A relay is a route, not storage. Your transfer is never turned into a cloud download.",
},
{
icon: "verified",
number: "03",
title: "Verified on arrival",
text: "Content addressing checks that incoming bytes match exactly what was offered.",
},
{
icon: "stop",
number: "04",
title: "Revocable access",
text: "Stop a share at any time and the invitation can no longer be used.",
text: "Content addressing checks that the received bytes match exactly what you sent.",
},
];
@@ -67,224 +73,95 @@ export default function HomePage() {
between devices.
</h1>
<p className="hero-lead">
Send files and folders directly when possible, with an end-to-end encrypted relay
when needed. No account. No hosted transfer copy. You decide who receives them.
Send files and folders across your devicesdirect when possible, private by design,
and always under your control.
</p>
<div className="hero-actions">
<a className="button button-primary" href="#how-it-works">
See how it works
<Icon name="arrow" />
</a>
<a
className="button button-ghost"
href="https://github.com/vnidrop/vnidrop"
target="_blank"
rel="noreferrer"
>
<Icon name="github" />
Explore the project
</a>
</div>
<div className="hero-platforms" aria-label="Supported platforms">
<span>Available across</span>
<ul>
<li>Android</li>
<li>iOS</li>
<li>macOS</li>
<li>Windows</li>
<li>Linux</li>
{supportedPlatforms.map((platform) => (
<li key={platform} aria-label={platform} title={platform}>
<PlatformIcon className="platform-glyph" platform={platform} />
<span className="sr-only">{platform}</span>
</li>
))}
</ul>
</div>
</Reveal>
</div>
<Reveal className="hero-art-reveal" delay={120}>
<HeroIllustration />
<figure className="hero-app-preview">
<Image
className="hero-app-preview-image"
src="/App.png"
width={2338}
height={1873}
sizes="(max-width: 920px) calc(100vw - 32px), 56vw"
alt="VniDrop running on desktop and iPhone, showing transfer review, receiver permissions, and invitation options."
priority
unoptimized
/>
</figure>
</Reveal>
</div>
</section>
<section className="trust-section" aria-label="VniDrop principles">
<div className="page-shell trust-grid">
{trustItems.map((item, index) => (
<Reveal key={item.value} className="trust-item" delay={index * 70}>
<span className="trust-number">0{index + 1}</span>
<div>
<strong>{item.value}</strong>
<p>{item.detail}</p>
</div>
</Reveal>
))}
</div>
</section>
<div className="word-stream" aria-hidden="true">
<div>
<span>FILES</span><i></i><span>FOLDERS</span><i></i><span>PHOTOS</span><i></i>
<span>PROJECTS</span><i></i><span>FILES</span><i></i><span>FOLDERS</span><i></i>
<span>PHOTOS</span><i></i><span>PROJECTS</span><i></i>
</div>
</div>
<section id="how-it-works" className="section flow-section">
<section id="how-it-works" className="simple-section simple-flow-section">
<div className="page-shell">
<Reveal className="section-heading section-heading-centered">
<span className="kicker">ONE DROP, FIVE MOMENTS</span>
<h2>A handoff that stays in your hands.</h2>
<Reveal className="simple-heading">
<span className="kicker">HOW IT WORKS</span>
<h2>Three steps. No account.</h2>
<p>
VniDrop keeps the transfer understandable from first selection to final verified file.
Choose the files, introduce the devices, and approve the handoff. VniDrop handles the
secure route.
</p>
</Reveal>
<Reveal delay={100}>
<TransferFlow />
</Reveal>
</div>
</section>
<section id="invitations" className="section invitation-section">
<div className="page-shell">
<Reveal className="section-heading section-heading-split">
<div>
<span className="kicker">MAKE THE INTRODUCTION</span>
<h2>One transfer. Three ways to meet.</h2>
</div>
<p>
The invitation is small enough to scan, tap, or send. Your files stay where they are
until a receiver connects and you allow the transfer.
</p>
</Reveal>
<Reveal delay={100}>
<InvitationShowcase />
</Reveal>
</div>
</section>
<section className="section approval-section">
<div className="page-shell approval-layout">
<Reveal className="approval-copy">
<span className="kicker">CONTROL IS BUILT IN</span>
<h2>A link is not permission.</h2>
<p className="section-lead">
Every new receiver knocks by default. Approve people you recognize, refuse the rest,
and follow progress for each device.
</p>
<ul className="check-list">
<li><Icon name="check" /> See the receiver before anything moves</li>
<li><Icon name="check" /> Cancel a transfer already in progress</li>
<li><Icon name="check" /> Stop sharing to revoke future access</li>
</ul>
<p className="approval-note">
Need an open handoff? An optional Anyone with this transfer mode is available for
non-sensitive files.
</p>
</Reveal>
<Reveal className="approval-demo-wrap" delay={120}>
<ApprovalDemo />
<span className="demo-hint"><Icon name="spark" /> Try the controls</span>
</Reveal>
</div>
</section>
<section id="privacy" className="privacy-principles-section">
<div className="page-shell">
<Reveal className="privacy-intro">
<span className="kicker kicker-light">PRIVACY BY DESIGN</span>
<h2>The file never becomes our file.</h2>
<p>
VniDrop coordinates a secure handoff. It does not turn your transfer into a hosted
download sitting on someone elses storage.
</p>
</Reveal>
<div className="privacy-point-grid">
{privacyPoints.map((point, index) => (
<Reveal key={point.number} className="privacy-point" delay={index * 70}>
<div className="privacy-point-top">
<span><Icon name={point.icon} /></span>
<small>{point.number}</small>
<div className="simple-steps">
{steps.map((step) => (
<article key={step.number} className="simple-step">
<div className="simple-step-top">
<span>{step.number}</span>
<Icon name={step.icon} />
</div>
<h3>{point.title}</h3>
<p>{point.text}</p>
</Reveal>
<h3>{step.title}</h3>
<p>{step.text}</p>
</article>
))}
</div>
<Reveal className="privacy-policy-link" delay={120}>
<p>Diagnostics are optional and exclude invitations, file paths, and transfer contents.</p>
<Link href="/privacy/">
Read the full privacy policy
</div>
</section>
<section id="privacy" className="trust-summary-section">
<div className="page-shell trust-summary">
<Reveal className="trust-summary-copy">
<span className="kicker kicker-light">PRIVACY BY DESIGN</span>
<h2>What VniDrop doesand doesnt do.</h2>
<p>
The transfer happens between devices. These are the details that matter when you
decide what to share and who can receive it.
</p>
<Link className="simple-text-link" href="/privacy/">
Read the privacy policy
<Icon name="arrow" />
</Link>
</Reveal>
</div>
</section>
<section className="network-section">
<div className="page-shell network-layout">
<Reveal className="network-copy">
<span className="kicker kicker-light">DIRECT WHEN POSSIBLE</span>
<h2>The shortest safe path, chosen automatically.</h2>
<p>
VniDrop tries to connect devices directlyeven across home routers and mobile
networks. If that path is not available, a relay forwards the same encrypted
connection without becoming a file store.
</p>
<blockquote>
<span></span>
The relay can move the envelope. It cannot open it.
</blockquote>
</Reveal>
<Reveal delay={120}>
<NetworkIllustration />
</Reveal>
</div>
</section>
<section id="platforms" className="section platforms-section">
<div className="page-shell platforms-layout">
<Reveal className="platforms-copy">
<span className="kicker">ONE FAMILIAR HANDOFF</span>
<h2>At home on every screen.</h2>
<p className="section-lead">
Android, iOS, macOS, Windows, and Linux share one clear transfer experience while each
platform keeps control of its own file picker and save destination.
</p>
<div className="platform-feature-list">
<span><Icon name="devices" /> Cross-platform by design</span>
<span><Icon name="folder" /> Native file destinations</span>
<span><Icon name="code" /> Open-source core</span>
</div>
</Reveal>
<Reveal className="orbit-wrap" delay={100}>
<PlatformOrbit />
</Reveal>
</div>
</section>
<section className="final-cta-section">
<div className="page-shell">
<Reveal className="final-cta">
<div className="final-cta-orb final-cta-orb-one" aria-hidden="true" />
<div className="final-cta-orb final-cta-orb-two" aria-hidden="true" />
<span className="kicker kicker-light">OPEN SOURCE · APACHE 2.0</span>
<h2>Drop the cloud holding pen. Keep the control.</h2>
<p>
VniDrop is in early development. Explore the code, build the app, and help shape a
more direct way to share.
</p>
<div className="final-cta-actions">
<a
className="button button-light"
href="https://github.com/vnidrop/vnidrop"
target="_blank"
rel="noreferrer"
>
<Icon name="github" />
View the source
</a>
<Link className="button button-dark-ghost" href="/privacy/">
Privacy policy
<Icon name="arrow" />
</Link>
</div>
</Reveal>
<div className="trust-summary-list">
{trustItems.map((item) => (
<article key={item.title} className="trust-summary-item">
<span><Icon name={item.icon} /></span>
<div>
<h3>{item.title}</h3>
<p>{item.text}</p>
</div>
</article>
))}
</div>
</div>
</section>
</main>

View File

@@ -1,8 +1,4 @@
import type { Metadata } from "next";
import Link from "next/link";
import { BrandMark } from "@/components/brand";
import { Icon, type IconName } from "@/components/icons";
import { Reveal } from "@/components/reveal";
export const metadata: Metadata = {
title: "Privacy policy",
@@ -10,13 +6,6 @@ export const metadata: Metadata = {
"How VniDrop handles transfers, local app data, optional diagnostics, bug reports, and website visits.",
};
const summaries: Array<{ icon: IconName; label: string; value: string }> = [
{ icon: "devices", label: "Transfers", value: "Device to device" },
{ icon: "globe", label: "Accounts", value: "None" },
{ icon: "shield", label: "Diagnostics", value: "Optional" },
{ icon: "folder", label: "Hosted file copy", value: "None" },
];
const sections = [
["scope", "Scope"],
["transfers", "Transfers"],
@@ -36,55 +25,13 @@ export default function PrivacyPage() {
return (
<main id="main-content" className="privacy-page">
<section className="privacy-hero">
<div className="privacy-hero-grid" aria-hidden="true" />
<div className="page-shell privacy-hero-layout">
<Reveal className="privacy-hero-copy">
<Link className="privacy-back" href="/">
<Icon name="arrow" />
Back to VniDrop
</Link>
<span className="kicker">PRIVACY POLICY</span>
<h1>Clear by design.<br />Specific by default.</h1>
<p>
This policy explains what moves between devices, what stays local, and what is sent
only when you choose to share diagnostics or a bug report.
</p>
<div className="privacy-meta">
<span>Effective July 16, 2026</span>
<i />
<span>Version 1.0</span>
</div>
</Reveal>
<Reveal className="privacy-hero-art" delay={100}>
<div className="privacy-art-orbit privacy-art-orbit-one" aria-hidden="true" />
<div className="privacy-art-orbit privacy-art-orbit-two" aria-hidden="true" />
<div className="privacy-mark-card" aria-hidden="true">
<span className="privacy-mark-glow" />
<BrandMark />
<span className="privacy-mark-lock"><Icon name="lock" /></span>
</div>
<div className="privacy-data-pill privacy-data-pill-one" aria-hidden="true">
<Icon name="file" /> CONTENT
</div>
<div className="privacy-data-pill privacy-data-pill-two" aria-hidden="true">
<Icon name="shield" /> APPROVAL
</div>
<div className="privacy-data-pill privacy-data-pill-three" aria-hidden="true">
<Icon name="verified" /> VERIFIED
</div>
</Reveal>
</div>
</section>
<section className="privacy-summary" aria-label="Privacy summary">
<div className="page-shell privacy-summary-grid">
{summaries.map((summary, index) => (
<Reveal key={summary.label} className="privacy-summary-card" delay={index * 60}>
<span><Icon name={summary.icon} /></span>
<small>{summary.label}</small>
<strong>{summary.value}</strong>
</Reveal>
))}
<div className="page-shell privacy-hero-inner">
<h1>Privacy Policy</h1>
<p>
This policy explains what moves between devices, what stays local, and what is sent
only when you choose to share diagnostics or a bug report.
</p>
<p className="privacy-meta">Effective July 16, 2026 · Version 1.0</p>
</div>
</section>
@@ -93,30 +40,27 @@ export default function PrivacyPage() {
<aside className="privacy-toc">
<p>On this page</p>
<nav aria-label="Privacy policy sections">
{sections.map(([id, label], index) => (
<a key={id} href={`#${id}`}>
<span>{String(index + 1).padStart(2, "0")}</span>
{label}
</a>
))}
<ol>
{sections.map(([id, label]) => (
<li key={id}>
<a href={`#${id}`}>{label}</a>
</li>
))}
</ol>
</nav>
</aside>
<article className="privacy-document">
<div className="privacy-callout">
<Icon name="spark" />
<div>
<strong>The short version</strong>
<p>
VniDrop has no user accounts and does not upload your transfer to a VniDrop file
store. Files travel over an authenticated, end-to-end encrypted connection.
Product diagnostics are opt-in; a bug report is sent only when you submit one.
</p>
</div>
<strong>The short version</strong>
<p>
VniDrop has no user accounts and does not upload your transfer to a VniDrop file
store. Files travel over an authenticated, end-to-end encrypted connection.
Product diagnostics are opt-in; a bug report is sent only when you submit one.
</p>
</div>
<section id="scope" className="policy-section">
<span className="policy-number">01</span>
<h2>Scope and who VniDrop means</h2>
<p>
This policy covers the official VniDrop website, the VniDrop applications for
@@ -132,7 +76,6 @@ export default function PrivacyPage() {
</section>
<section id="transfers" className="policy-section">
<span className="policy-number">02</span>
<h2>What happens during a transfer</h2>
<h3>File contents</h3>
<p>
@@ -159,7 +102,6 @@ export default function PrivacyPage() {
encrypted transfer contents.
</p>
<div className="policy-note">
<Icon name="shield" />
<p>
Approval is required by default. If the sender selects Anyone with this transfer,
anyone holding the invitation may receive the files until sharing stops.
@@ -168,7 +110,6 @@ export default function PrivacyPage() {
</section>
<section id="local-data" className="policy-section">
<span className="policy-number">03</span>
<h2>Information kept on your device</h2>
<p>VniDrop stores the information needed to operate the app locally, including:</p>
<ul>
@@ -187,7 +128,6 @@ export default function PrivacyPage() {
</section>
<section id="diagnostics" className="policy-section">
<span className="policy-number">04</span>
<h2>Optional diagnostics and bug reports</h2>
<h3>Automatic product diagnostics</h3>
<p>
@@ -217,7 +157,6 @@ export default function PrivacyPage() {
</section>
<section id="website" className="policy-section">
<span className="policy-number">05</span>
<h2>The VniDrop website</h2>
<p>
This website is a static product site. It does not provide an account, contact form,
@@ -235,14 +174,25 @@ export default function PrivacyPage() {
</section>
<section id="permissions" className="policy-section">
<span className="policy-number">06</span>
<h2>Device permissions</h2>
<div className="permission-grid">
<div><Icon name="folder" /><strong>Files & folders</strong><p>Choose what to send and where received files are saved.</p></div>
<div><Icon name="qr" /><strong>Camera / scanner</strong><p>Scan a QR invitation when you choose that receive method.</p></div>
<div><Icon name="nfc" /><strong>NFC</strong><p>Read or write an invitation through a compatible NFC tag.</p></div>
<div><Icon name="devices" /><strong>Network & notifications</strong><p>Connect peers and alert you to background receiver requests.</p></div>
</div>
<dl className="permission-list">
<div>
<dt>Files &amp; folders</dt>
<dd>Choose what to send and where received files are saved.</dd>
</div>
<div>
<dt>Camera / scanner</dt>
<dd>Scan a QR invitation when you choose that receive method.</dd>
</div>
<div>
<dt>NFC</dt>
<dd>Read or write an invitation through a compatible NFC tag.</dd>
</div>
<div>
<dt>Network &amp; notifications</dt>
<dd>Connect peers and alert you to background receiver requests.</dd>
</div>
</dl>
<p>
VniDrop requests a platform permission only for the related feature. On Android, QR
scanning may be provided through Google Play services Code Scanner. Platform-level
@@ -251,54 +201,100 @@ export default function PrivacyPage() {
</section>
<section id="providers" className="policy-section">
<span className="policy-number">07</span>
<h2>Infrastructure and external services</h2>
<div className="provider-list">
<dl className="provider-list">
<div>
<span>I</span>
<div><strong>Iroh / public relay operators</strong><p>Device discovery, connection establishment, and encrypted relay fallback. Relays process connection metadata but cannot decrypt transfer contents.</p></div>
<dt>Iroh / public relay operators</dt>
<dd>
Device discovery, connection establishment, and encrypted relay fallback.
Relays process connection metadata but cannot decrypt transfer contents.
</dd>
</div>
<div>
<span>C</span>
<div><strong>Cloudflare</strong><p>The projects diagnostics design uses Cloudflare Workers, D1, and R2. Cloudflare also processes source IPs for request delivery and abuse controls.</p></div>
<dt>Cloudflare</dt>
<dd>
The projects diagnostics design uses Cloudflare Workers, D1, and R2.
Cloudflare also processes source IPs for request delivery and abuse controls.
</dd>
</div>
<div>
<span>G</span>
<div><strong>Google Play services</strong><p>May provide the QR code scanner on supported Android devices when you choose to scan an invitation.</p></div>
<dt>Google Play services</dt>
<dd>
May provide the QR code scanner on supported Android devices when you choose to
scan an invitation.
</dd>
</div>
<div>
<span>GH</span>
<div><strong>GitHub</strong><p>Hosts the source repository, issue tracker, and external pages linked from this site. GitHubs own privacy terms apply after you follow those links.</p></div>
<dt>GitHub</dt>
<dd>
Hosts the source repository, issue tracker, and external pages linked from this
site. GitHubs own privacy terms apply after you follow those links.
</dd>
</div>
</div>
</dl>
<p className="provider-links">
Provider policies: {" "}
<a href="https://services.iroh.computer/legal/privacy" target="_blank" rel="noreferrer">Iroh</a>, {" "}
<a href="https://www.cloudflare.com/policies/privacy/" target="_blank" rel="noreferrer">Cloudflare</a>, {" "}
<a href="https://policies.google.com/privacy" target="_blank" rel="noreferrer">Google</a>, and {" "}
<a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement" target="_blank" rel="noreferrer">GitHub</a>.
Provider policies:{" "}
<a
href="https://services.iroh.computer/legal/privacy"
target="_blank"
rel="noreferrer"
>
Iroh
</a>
,{" "}
<a
href="https://www.cloudflare.com/policies/privacy/"
target="_blank"
rel="noreferrer"
>
Cloudflare
</a>
,{" "}
<a href="https://policies.google.com/privacy" target="_blank" rel="noreferrer">
Google
</a>
, and{" "}
<a
href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement"
target="_blank"
rel="noreferrer"
>
GitHub
</a>
.
</p>
</section>
<section id="retention" className="policy-section">
<span className="policy-number">08</span>
<h2>Retention and deletion</h2>
<div className="retention-table" role="table" aria-label="Data retention periods">
<div className="retention-row retention-head" role="row">
<span role="columnheader">Data</span><span role="columnheader">Typical retention</span>
</div>
<div className="retention-row" role="row">
<span role="cell">Transfer history and settings</span><span role="cell">Until you delete them, clear app data, or uninstall</span>
</div>
<div className="retention-row" role="row">
<span role="cell">Pending local crash reports</span><span role="cell">Up to 30 days and 20 reports; deleted when diagnostics is disabled</span>
</div>
<div className="retention-row" role="row">
<span role="cell">Server diagnostics and bug reports</span><span role="cell">The current project configuration is 90 days, with scheduled deletion</span>
</div>
<div className="retention-row" role="row">
<span role="cell">Downloaded files</span><span role="cell">Until you delete them through your operating system</span>
</div>
<div className="retention-table-wrap">
<table className="retention-table">
<caption className="sr-only">Data retention periods</caption>
<thead>
<tr>
<th scope="col">Data</th>
<th scope="col">Typical retention</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Transfer history and settings</th>
<td>Until you delete them, clear app data, or uninstall</td>
</tr>
<tr>
<th scope="row">Pending local crash reports</th>
<td>Up to 30 days and 20 reports; deleted when diagnostics is disabled</td>
</tr>
<tr>
<th scope="row">Server diagnostics and bug reports</th>
<td>The current project configuration is 90 days, with scheduled deletion</td>
</tr>
<tr>
<th scope="row">Downloaded files</th>
<td>Until you delete them through your operating system</td>
</tr>
</tbody>
</table>
</div>
<p>
Operational backups, provider logs, and deletion backlogs may persist briefly beyond
@@ -309,14 +305,22 @@ export default function PrivacyPage() {
</section>
<section id="choices" className="policy-section">
<span className="policy-number">09</span>
<h2>Your choices and rights</h2>
<ul>
<li>Enable or disable Share diagnostics in VniDrop settings.</li>
<li>Submit a bug report only when you choose, omit contact information, and exclude logs.</li>
<li>
Submit a bug report only when you choose, omit contact information, and exclude
logs.
</li>
<li>Approve or refuse each receiver, cancel a transfer, or stop sharing.</li>
<li>Delete individual transfer history or clear completed, failed, and cancelled receive history.</li>
<li>Delete downloaded files using your operating system, or clear all app data by uninstalling or resetting the app.</li>
<li>
Delete individual transfer history or clear completed, failed, and cancelled
receive history.
</li>
<li>
Delete downloaded files using your operating system, or clear all app data by
uninstalling or resetting the app.
</li>
</ul>
<p>
Depending on where you live, privacy law may provide rights to access, correct,
@@ -328,7 +332,6 @@ export default function PrivacyPage() {
</section>
<section id="security" className="policy-section">
<span className="policy-number">10</span>
<h2>Security</h2>
<p>
VniDrop uses authenticated end-to-end encrypted connections, content verification,
@@ -338,13 +341,19 @@ export default function PrivacyPage() {
updated, and stop sharing when a transfer is finished.
</p>
<p>
Please report a suspected vulnerability through the private process in the {" "}
<a href="https://github.com/vnidrop/vnidrop/blob/master/SECURITY.md" target="_blank" rel="noreferrer">VniDrop security policy</a>, not in a public issue.
Please report a suspected vulnerability through the private process in the{" "}
<a
href="https://github.com/vnidrop/vnidrop/blob/master/SECURITY.md"
target="_blank"
rel="noreferrer"
>
VniDrop security policy
</a>
, not in a public issue.
</p>
</section>
<section id="changes" className="policy-section">
<span className="policy-number">11</span>
<h2>Changes to this policy</h2>
<p>
VniDrop is in early development. Features and data practices may change. When this
@@ -355,7 +364,6 @@ export default function PrivacyPage() {
</section>
<section id="contact" className="policy-section policy-contact">
<span className="policy-number">12</span>
<h2>Contact</h2>
<p>
VniDrop is currently maintained as an open-source project and does not yet publish a
@@ -364,13 +372,12 @@ export default function PrivacyPage() {
credentials, or other sensitive information in a public issue.
</p>
<a
className="button button-primary"
className="privacy-contact-link"
href="https://github.com/vnidrop/vnidrop/issues/new"
target="_blank"
rel="noreferrer"
>
Contact the maintainers
<Icon name="arrow" />
</a>
</section>
</article>

View File

@@ -1,659 +1,288 @@
.privacy-page {
background: #fcfbfd;
background: var(--surface);
}
.privacy-hero {
position: relative;
overflow: hidden;
padding: 165px 0 105px;
background:
radial-gradient(circle at 80% 20%, rgba(192, 132, 252, 0.2), transparent 30%),
linear-gradient(180deg, #fdfcfe, #f7f3f9);
padding: 158px 0 72px;
border-bottom: 1px solid var(--line);
text-align: center;
}
.privacy-hero-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(114, 84, 128, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(114, 84, 128, 0.055) 1px, transparent 1px);
background-size: 32px 32px;
mask-image: linear-gradient(to bottom, black, transparent 90%);
}
.privacy-hero-layout {
position: relative;
display: grid;
grid-template-columns: minmax(0, 0.92fr) minmax(430px, 0.72fr);
align-items: center;
gap: 95px;
}
.privacy-back {
.privacy-hero-inner {
display: flex;
width: max-content;
flex-direction: column;
align-items: center;
gap: 8px;
margin-bottom: 42px;
color: var(--ink-muted);
font-size: 12px;
font-weight: 650;
}
.privacy-hero-copy > .kicker {
display: block;
width: max-content;
.privacy-hero h1 {
margin-bottom: 22px;
font-size: clamp(3.25rem, 5.2vw, 4.5rem);
font-weight: 700;
letter-spacing: -0.055em;
line-height: 1;
}
.privacy-back svg {
width: 16px;
transform: rotate(180deg);
transition: transform 180ms ease;
}
.privacy-back:hover {
color: var(--brand-600);
}
.privacy-back:hover svg {
transform: rotate(180deg) translateX(4px);
}
.privacy-hero-copy h1 {
margin: 0 0 25px;
font-size: clamp(3.7rem, 6.2vw, 6.5rem);
font-weight: 760;
letter-spacing: -0.075em;
line-height: 0.98;
}
.privacy-hero-copy > p {
max-width: 680px;
margin-bottom: 28px;
.privacy-hero-inner > p:not(.privacy-meta) {
max-width: 640px;
margin-bottom: 24px;
color: var(--ink-soft);
font-size: 18px;
line-height: 1.72;
font-size: 16px;
line-height: 1.7;
}
.privacy-meta {
display: flex;
align-items: center;
gap: 12px;
margin: 0;
color: var(--ink-muted);
font-family: var(--font-mono);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.privacy-meta i {
width: 3px;
height: 3px;
border-radius: 50%;
background: var(--brand-500);
}
.privacy-hero-art {
position: relative;
min-height: 435px;
}
.privacy-art-orbit {
position: absolute;
top: 50%;
left: 50%;
border: 1px solid rgba(168, 85, 247, 0.18);
border-radius: 50%;
transform: translate(-50%, -50%);
}
.privacy-art-orbit::before,
.privacy-art-orbit::after {
position: absolute;
border-radius: 50%;
background: var(--brand-400);
content: "";
}
.privacy-art-orbit-one {
width: 420px;
height: 420px;
animation: privacy-orbit 28s linear infinite;
}
.privacy-art-orbit-one::before {
top: 11%;
left: 16%;
width: 7px;
height: 7px;
box-shadow: 0 0 0 7px rgba(168, 85, 247, 0.09);
}
.privacy-art-orbit-one::after {
right: 8%;
bottom: 25%;
width: 5px;
height: 5px;
}
.privacy-art-orbit-two {
width: 295px;
height: 295px;
border-style: dashed;
animation: privacy-orbit 21s linear infinite reverse;
}
.privacy-art-orbit-two::before {
top: 48%;
left: -4px;
width: 7px;
height: 7px;
}
.privacy-art-orbit-two::after {
top: 4%;
right: 22%;
width: 5px;
height: 5px;
}
.privacy-mark-card {
position: absolute;
z-index: 2;
top: 50%;
left: 50%;
display: grid;
width: 180px;
height: 180px;
place-items: center;
border: 1px solid #dfd2e6;
border-radius: 45px;
background: rgba(255, 255, 255, 0.91);
box-shadow: 0 35px 90px rgba(65, 30, 80, 0.17);
transform: translate(-50%, -50%) rotate(-3deg);
backdrop-filter: blur(14px);
}
.privacy-mark-card > img {
position: relative;
z-index: 2;
width: 112px;
height: 112px;
}
.privacy-mark-glow {
position: absolute;
inset: -35px;
border-radius: 50%;
background: rgba(168, 85, 247, 0.2);
filter: blur(42px);
animation: privacy-glow 5s ease-in-out infinite;
}
.privacy-mark-lock {
position: absolute;
right: -12px;
bottom: -10px;
display: grid;
width: 49px;
height: 49px;
place-items: center;
border: 4px solid #f8f4fa;
border-radius: 15px;
background: var(--ink);
box-shadow: 0 12px 30px rgba(42, 25, 50, 0.2);
color: white;
transform: rotate(3deg);
}
.privacy-mark-lock svg {
width: 21px;
}
.privacy-data-pill {
position: absolute;
z-index: 4;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 11px;
border: 1px solid #dfd3e4;
border-radius: 10px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 12px 35px rgba(58, 33, 69, 0.11);
color: var(--brand-600);
font-family: var(--font-mono);
font-size: 8px;
font-weight: 800;
letter-spacing: 0.07em;
backdrop-filter: blur(10px);
animation: privacy-pill 5s ease-in-out infinite;
}
.privacy-data-pill svg {
width: 15px;
}
.privacy-data-pill-one {
top: 11%;
left: 4%;
transform: rotate(-5deg);
}
.privacy-data-pill-two {
top: 29%;
right: -2%;
transform: rotate(4deg);
animation-delay: -2s;
}
.privacy-data-pill-three {
bottom: 6%;
left: 7%;
transform: rotate(3deg);
animation-delay: -3.5s;
}
.privacy-summary {
position: relative;
z-index: 4;
margin-top: -37px;
}
.privacy-summary-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
overflow: hidden;
border: 1px solid var(--line);
border-radius: 22px;
background: rgba(255, 255, 255, 0.95);
box-shadow: var(--shadow-lg);
backdrop-filter: blur(12px);
}
.privacy-summary-card {
display: grid;
min-height: 151px;
align-content: center;
padding: 25px 27px;
}
.privacy-summary-card + .privacy-summary-card {
border-left: 1px solid var(--line);
}
.privacy-summary-card > span {
display: grid;
width: 37px;
height: 37px;
margin-bottom: 17px;
place-items: center;
border-radius: 10px;
background: var(--brand-100);
color: var(--brand-600);
}
.privacy-summary-card svg {
width: 19px;
}
.privacy-summary-card small {
margin-bottom: 1px;
color: var(--ink-muted);
font-family: var(--font-mono);
font-size: 8px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.privacy-summary-card strong {
font-family: var(--font-display);
font-size: 16px;
letter-spacing: -0.02em;
font-size: 13px;
}
.privacy-document-section {
padding: 135px 0 155px;
padding: 80px 0 144px;
}
.privacy-document-layout {
display: grid;
grid-template-columns: 210px minmax(0, 780px);
grid-template-areas: "article toc";
grid-template-columns: minmax(0, 720px) 220px;
justify-content: center;
gap: 105px;
gap: 92px;
}
.privacy-document {
grid-area: article;
min-width: 0;
}
.privacy-toc {
position: sticky;
top: 112px;
top: 108px;
grid-area: toc;
align-self: start;
padding-left: 24px;
border-left: 1px solid var(--line);
}
.privacy-toc > p {
margin-bottom: 17px;
margin-bottom: 14px;
color: var(--ink);
font-family: var(--font-mono);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.09em;
text-transform: uppercase;
font-size: 13px;
font-weight: 700;
}
.privacy-toc nav {
display: flex;
flex-direction: column;
.privacy-toc ol {
display: grid;
gap: 1px;
margin: 0;
padding: 0;
list-style: none;
}
.privacy-toc a {
display: grid;
grid-template-columns: 26px 1fr;
align-items: center;
gap: 6px;
padding: 7px 0;
color: #79707e;
font-size: 11px;
font-weight: 600;
transition: color 160ms ease, transform 160ms ease;
display: block;
padding: 5px 0;
color: var(--ink-muted);
font-size: 12px;
line-height: 1.45;
transition: color 160ms ease;
}
.privacy-toc a:hover {
color: var(--brand-600);
transform: translateX(3px);
}
.privacy-toc a span {
color: #b5aabb;
font-family: var(--font-mono);
font-size: 8px;
}
.privacy-document {
min-width: 0;
color: var(--ink);
}
.privacy-callout {
display: grid;
grid-template-columns: auto 1fr;
gap: 17px;
margin-bottom: 75px;
padding: 25px;
border: 1px solid #dfd0e7;
border-radius: 17px;
background: linear-gradient(145deg, #fbf6ff, #f7f1fa);
}
.privacy-callout > svg {
width: 24px;
color: var(--brand-600);
margin-bottom: 64px;
padding: 0 0 40px 20px;
border-bottom: 1px solid var(--line);
border-left: 2px solid var(--ink);
}
.privacy-callout strong {
display: block;
margin-bottom: 5px;
margin-bottom: 8px;
font-family: var(--font-display);
font-size: 14px;
font-size: 17px;
}
.privacy-callout p {
margin: 0;
color: var(--ink-soft);
font-size: 12px;
line-height: 1.7;
font-size: 15px;
line-height: 1.75;
}
.policy-section {
position: relative;
scroll-margin-top: 110px;
padding: 0 0 72px;
scroll-margin-top: 104px;
padding-bottom: 56px;
}
.policy-section + .policy-section {
padding-top: 3px;
padding-top: 56px;
border-top: 1px solid var(--line);
}
.policy-number {
display: inline-flex;
margin-top: 0;
margin-bottom: 15px;
color: var(--brand-600);
font-family: var(--font-mono);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.08em;
}
.policy-section + .policy-section .policy-number {
margin-top: 65px;
}
.policy-section h2 {
margin-bottom: 27px;
font-size: clamp(2rem, 3.3vw, 3.15rem);
font-weight: 720;
letter-spacing: -0.052em;
line-height: 1.08;
margin-bottom: 24px;
font-size: clamp(1.75rem, 2.7vw, 2rem);
font-weight: 680;
letter-spacing: -0.035em;
line-height: 1.22;
}
.policy-section h3 {
margin: 34px 0 10px;
font-size: 16px;
font-weight: 720;
margin: 32px 0 10px;
font-size: 19px;
font-weight: 680;
letter-spacing: -0.018em;
line-height: 1.35;
}
.policy-section p,
.policy-section li {
color: #514956;
font-size: 14px;
line-height: 1.82;
.policy-section li,
.permission-list dd,
.provider-list dd {
color: var(--ink-soft);
font-size: 15px;
line-height: 1.75;
}
.policy-section p {
margin-bottom: 19px;
margin-bottom: 18px;
}
.policy-section ul {
display: grid;
gap: 8px;
margin: 17px 0 22px;
gap: 7px;
margin: 18px 0 24px;
padding-left: 22px;
}
.policy-section li::marker {
color: var(--brand-500);
color: var(--ink-muted);
}
.policy-section code {
padding: 2px 6px;
border: 1px solid #e7dceb;
border-radius: 5px;
background: #f8f3fa;
color: var(--brand-700);
padding: 2px 5px;
border: 1px solid var(--line);
border-radius: 4px;
background: var(--paper-warm);
color: var(--ink);
font-family: var(--font-mono);
font-size: 0.86em;
font-size: 0.88em;
}
.policy-section a:not(.button) {
.policy-section a,
.privacy-contact-link {
color: var(--brand-700);
font-weight: 650;
font-weight: 620;
text-decoration: underline;
text-decoration-color: rgba(132, 47, 238, 0.28);
text-decoration-color: rgba(111, 39, 233, 0.34);
text-underline-offset: 3px;
}
.policy-note {
display: grid;
grid-template-columns: auto 1fr;
gap: 13px;
margin-top: 28px;
padding: 18px;
border: 1px solid #e2d6e8;
border-radius: 13px;
background: #faf7fb;
.policy-section a:hover,
.privacy-contact-link:hover {
text-decoration-color: currentColor;
}
.policy-note svg {
width: 21px;
color: var(--brand-600);
.policy-note {
margin-top: 28px;
padding: 18px 20px;
border-left: 2px solid var(--line-strong);
background: var(--paper-warm);
}
.policy-note p {
margin: 0;
color: #5e5264;
font-size: 12px;
line-height: 1.65;
}
.permission-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin: 30px 0;
}
.permission-grid > div {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 5px 12px;
padding: 17px;
border: 1px solid var(--line);
border-radius: 13px;
background: white;
}
.permission-grid svg {
width: 21px;
grid-row: 1 / 3;
color: var(--brand-600);
}
.permission-grid strong {
font-size: 11px;
}
.permission-grid p {
margin: 0;
color: var(--ink-muted);
font-size: 9px;
line-height: 1.5;
font-size: 14px;
line-height: 1.7;
}
.permission-list,
.provider-list {
display: grid;
gap: 9px;
margin: 28px 0;
}
.provider-list > div {
display: grid;
grid-template-columns: auto 1fr;
gap: 15px;
padding: 17px;
border: 1px solid var(--line);
border-radius: 13px;
background: white;
}
.provider-list > div > span {
display: grid;
width: 38px;
height: 38px;
place-items: center;
border-radius: 10px;
background: var(--brand-100);
color: var(--brand-700);
font-family: var(--font-mono);
font-size: 9px;
font-weight: 850;
}
.provider-list strong {
display: block;
margin-bottom: 3px;
font-size: 11px;
}
.provider-list p {
margin: 0;
color: var(--ink-muted);
font-size: 10px;
line-height: 1.55;
}
.provider-links {
font-size: 11px !important;
}
.retention-table {
overflow: hidden;
margin: 28px 0;
border: 1px solid var(--line);
border-radius: 14px;
background: white;
}
.retention-row {
display: grid;
grid-template-columns: minmax(190px, 0.72fr) 1.28fr;
}
.retention-row + .retention-row {
border-top: 1px solid var(--line);
}
.retention-row span {
padding: 14px 16px;
color: var(--ink-soft);
font-size: 10px;
.permission-list > div,
.provider-list > div {
display: grid;
grid-template-columns: minmax(150px, 0.42fr) 1fr;
gap: 24px;
padding: 17px 0;
border-bottom: 1px solid var(--line);
}
.permission-list dt,
.provider-list dt {
color: var(--ink);
font-size: 14px;
font-weight: 680;
line-height: 1.55;
}
.retention-row span + span {
border-left: 1px solid var(--line);
.permission-list dd,
.provider-list dd {
margin: 0;
font-size: 14px;
line-height: 1.65;
}
.provider-links {
font-size: 14px !important;
}
.retention-table-wrap {
overflow-x: auto;
margin: 28px 0;
border-top: 1px solid var(--line);
-webkit-overflow-scrolling: touch;
}
.retention-table {
width: 100%;
min-width: 600px;
border-collapse: collapse;
text-align: left;
}
.retention-table th,
.retention-table td {
padding: 15px 16px 15px 0;
border-bottom: 1px solid var(--line);
color: var(--ink-soft);
font-size: 13px;
font-weight: 400;
line-height: 1.6;
vertical-align: top;
}
.retention-table th:first-child {
width: 38%;
}
.retention-table thead th {
color: var(--ink-muted);
}
.retention-head {
background: #f4eff6;
}
.retention-head span {
color: var(--ink) !important;
font-family: var(--font-mono);
font-size: 8px;
font-weight: 800;
letter-spacing: 0.07em;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.retention-table tbody th {
color: var(--ink);
font-weight: 620;
}
.policy-contact {
overflow: hidden;
padding: 54px !important;
border: 1px solid #dfd0e7 !important;
border-radius: 21px;
background:
radial-gradient(circle at 100% 0%, rgba(168, 85, 247, 0.14), transparent 35%),
linear-gradient(145deg, #fbf7fe, #f6f0f9);
}
.policy-contact .policy-number {
margin-top: 0 !important;
}
.policy-contact h2 {
margin-bottom: 16px;
padding-bottom: 0;
}
.policy-contact p {
max-width: 620px;
margin-bottom: 26px;
margin-bottom: 18px;
}
.policy-contact .button {
color: white;
text-decoration: none;
.privacy-contact-link::after {
margin-left: 5px;
content: "↗";
}

View File

@@ -1,156 +1,89 @@
@keyframes privacy-orbit {
from { transform: translate(-50%, -50%) rotate(0); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes privacy-glow {
0%, 100% { opacity: 0.5; transform: scale(0.84); }
50% { opacity: 1; transform: scale(1.14); }
}
@keyframes privacy-pill {
0%, 100% { margin-top: 0; }
50% { margin-top: -8px; }
}
@media (max-width: 1050px) {
.privacy-hero-layout {
grid-template-columns: minmax(0, 1fr) 390px;
gap: 45px;
}
.privacy-hero-copy h1 {
font-size: clamp(3.6rem, 6.8vw, 5.5rem);
}
@media (max-width: 940px) {
.privacy-document-layout {
grid-template-columns: 180px minmax(0, 720px);
gap: 60px;
}
}
@media (max-width: 820px) {
.privacy-hero {
padding-top: 135px;
}
.privacy-hero-layout {
grid-template-columns: 1fr;
}
.privacy-hero-copy {
max-width: 690px;
}
.privacy-hero-art {
width: min(100%, 520px);
margin: 0 auto;
}
.privacy-summary-grid {
grid-template-columns: 1fr 1fr;
}
.privacy-summary-card:nth-child(3) {
border-top: 1px solid var(--line);
border-left: 0;
}
.privacy-summary-card:nth-child(4) {
border-top: 1px solid var(--line);
}
.privacy-document-layout {
grid-template-columns: 1fr;
grid-template-areas:
"toc"
"article";
grid-template-columns: minmax(0, 720px);
gap: 56px;
}
.privacy-toc {
position: relative;
top: auto;
padding-bottom: 25px;
position: static;
padding: 0 0 28px;
border-bottom: 1px solid var(--line);
border-left: 0;
}
.privacy-toc nav {
display: grid;
grid-template-columns: repeat(3, 1fr);
.privacy-toc ol {
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 24px;
}
}
@media (max-width: 560px) {
@media (max-width: 640px) {
.privacy-hero {
padding: 120px 0 90px;
padding: 122px 0 54px;
}
.privacy-back {
margin-bottom: 32px;
.privacy-hero h1 {
margin-bottom: 18px;
font-size: clamp(2.55rem, 12vw, 3.4rem);
}
.privacy-hero-copy h1 {
font-size: clamp(3rem, 15vw, 4.5rem);
}
.privacy-hero-copy > p {
.privacy-hero-inner > p:not(.privacy-meta) {
font-size: 15px;
line-height: 1.65;
}
.privacy-hero-art {
min-height: 360px;
transform: scale(0.86);
}
.privacy-summary-grid {
grid-template-columns: 1fr;
}
.privacy-summary-card {
min-height: 120px;
}
.privacy-summary-card + .privacy-summary-card {
border-top: 1px solid var(--line);
border-left: 0;
.privacy-meta {
font-size: 12px;
}
.privacy-document-section {
padding: 95px 0 105px;
padding: 56px 0 96px;
}
.privacy-toc nav {
grid-template-columns: 1fr 1fr;
.privacy-document-layout {
gap: 0;
}
.privacy-callout {
padding: 20px;
}
.policy-section h2 {
font-size: 2.25rem;
}
.policy-section p,
.policy-section li {
font-size: 13px;
}
.permission-grid {
grid-template-columns: 1fr;
}
.retention-row {
grid-template-columns: 1fr;
}
.retention-row span + span {
padding-top: 0;
border-left: 0;
}
.retention-head {
.privacy-toc {
display: none;
}
.policy-contact {
padding: 35px 24px !important;
.privacy-callout {
margin-bottom: 48px;
padding: 0 0 32px 16px;
}
.privacy-callout p,
.policy-section p,
.policy-section li,
.permission-list dd,
.provider-list dd {
font-size: 14px;
}
.policy-section {
padding-bottom: 44px;
}
.policy-section + .policy-section {
padding-top: 44px;
}
.policy-section h2 {
font-size: 1.65rem;
}
.policy-section h3 {
margin-top: 28px;
font-size: 17px;
}
.permission-list > div,
.provider-list > div {
grid-template-columns: 1fr;
gap: 5px;
}
}

View File

@@ -3,15 +3,6 @@
to { transform: translate3d(24px, 18px, 0) rotate(8deg); }
}
@keyframes word-stream {
to { transform: translateX(-50%); }
}
@keyframes cta-orb {
from { transform: translate3d(-20px, -10px, 0) scale(0.9); }
to { transform: translate3d(40px, 25px, 0) scale(1.12); }
}
@media (max-width: 1120px) {
.hero-layout {
grid-template-columns: minmax(0, 0.88fr) minmax(470px, 1.12fr);
@@ -21,18 +12,8 @@
font-size: clamp(3.2rem, 5.4vw, 4.5rem);
}
.approval-layout,
.network-layout,
.platforms-layout {
gap: 55px;
}
.privacy-point-grid {
grid-template-columns: repeat(2, 1fr);
}
.privacy-point {
min-height: 230px;
.trust-summary {
gap: 65px;
}
}
@@ -62,46 +43,23 @@
justify-content: center;
}
.trust-grid {
.hero-app-preview {
width: min(100%, 780px);
margin: 12px auto 0;
}
.simple-step {
padding-right: 22px;
padding-left: 22px;
}
.trust-summary {
grid-template-columns: 1fr;
gap: 50px;
}
.trust-item {
min-height: 102px;
}
.trust-item + .trust-item {
border-top: 1px solid var(--line);
border-left: 0;
}
.section-heading-split {
grid-template-columns: 1fr;
gap: 20px;
}
.section-heading-split p {
max-width: 640px;
}
.approval-layout,
.network-layout,
.platforms-layout {
grid-template-columns: 1fr;
}
.approval-copy,
.network-copy,
.platforms-copy {
max-width: 690px;
}
.network-layout {
gap: 75px;
}
.platforms-layout {
gap: 65px;
.trust-summary-copy {
max-width: 680px;
}
}
@@ -141,97 +99,73 @@
justify-content: center;
}
.trust-section {
margin-top: -18px;
.hero-app-preview {
width: calc(100% + 24px);
margin-left: -12px;
}
.trust-grid {
border-radius: 18px;
.simple-section,
.trust-summary-section {
padding: 88px 0;
}
.trust-item {
padding: 20px;
.simple-heading {
margin-bottom: 40px;
}
.word-stream {
padding-top: 70px;
font-size: 3.6rem;
}
.section {
padding: 95px 0;
}
.section-heading {
margin-bottom: 42px;
}
.section-heading h2,
.privacy-intro h2,
.network-copy h2,
.platforms-copy h2,
.final-cta h2,
.approval-copy h2 {
.simple-heading h2,
.trust-summary-copy h2 {
font-size: clamp(2.35rem, 12vw, 3.5rem);
}
.section-heading p,
.section-lead {
.simple-heading p,
.trust-summary-copy p {
font-size: 15px;
}
.approval-section,
.platforms-section,
.network-section,
.privacy-principles-section {
padding: 100px 0;
}
.approval-layout {
gap: 55px;
}
.privacy-point-grid {
.simple-steps {
grid-template-columns: 1fr;
}
.privacy-point {
.simple-step {
min-height: 0;
padding: 28px 0 32px;
}
.privacy-point-top {
margin-bottom: 30px;
.simple-step + .simple-step {
border-top: 1px solid var(--line);
border-left: 0;
}
.privacy-policy-link {
align-items: flex-start;
flex-direction: column;
.simple-step-top {
margin-bottom: 28px;
}
.final-cta-section {
padding-bottom: 24px;
.trust-summary-item {
grid-template-columns: 40px 1fr;
gap: 15px;
}
.final-cta {
padding: 76px 22px;
border-radius: 23px;
}
.final-cta-actions {
display: grid;
}
.footer-main {
grid-template-columns: 1fr;
}
.footer-links {
gap: 35px;
.trust-summary-item > span {
width: 38px;
height: 38px;
}
.footer-inner,
.footer-bottom {
align-items: flex-start;
flex-direction: column;
}
.footer-inner {
gap: 24px;
}
.footer-links {
flex-wrap: wrap;
}
.footer-bottom {
gap: 5px;
}
}

View File

@@ -37,8 +37,11 @@ export function AppIcon({ className, title }: BrandAssetProps) {
export function Brand({ compact = false }: { compact?: boolean }) {
return (
<Link className="brand" href="/" aria-label="VniDrop home">
<BrandMark className="brand-mark-image" />
{!compact && <span className="brand-name">VniDrop</span>}
<span className="brand-primary">
<BrandMark className="brand-mark-image" />
{!compact && <span className="brand-name">VniDrop</span>}
</span>
{!compact && <span className="brand-tagline">Send files directly. Stay in control.</span>}
</Link>
);
}

View File

@@ -20,10 +20,17 @@
}
.brand {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
width: max-content;
}
.brand-primary {
display: inline-flex;
align-items: center;
gap: 3px;
width: max-content;
height: 47px;
}
.brand-mark-image {
@@ -42,6 +49,17 @@
letter-spacing: -0.045em;
}
.brand-tagline {
margin-top: -4px;
margin-left: 4px;
color: #756c79;
font-size: 9px;
font-weight: 580;
line-height: 1;
letter-spacing: -0.01em;
white-space: nowrap;
}
.header-github-button {
display: inline-flex;
min-height: 40px;
@@ -57,15 +75,19 @@
font-size: 12px;
font-weight: 680;
line-height: 1;
transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease,
transform 160ms ease;
animation: none;
transform: none;
}
.header-github-button:hover {
border-color: #403944;
background: #29242d;
box-shadow: 0 10px 26px rgba(23, 20, 25, 0.17), inset 0 1px rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
border-color: #6f6475;
background: #302a34;
box-shadow: 0 11px 28px rgba(23, 20, 25, 0.2), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.header-github-button:focus-visible {
outline: 3px solid rgba(168, 85, 247, 0.28);
outline-offset: 3px;
}
.header-github-button svg {
@@ -85,10 +107,20 @@
height: 44px;
}
.brand-primary {
height: 43px;
}
.brand-name {
font-size: 17px;
}
.brand-tagline {
margin-top: -3px;
margin-left: 3px;
font-size: 8px;
}
.header-github-button {
min-height: 38px;
gap: 7px;
@@ -107,6 +139,10 @@
font-size: 16px;
}
.brand-tagline {
font-size: 7.5px;
}
.header-github-button {
min-height: 36px;
padding-inline: 10px;

View File

@@ -267,8 +267,9 @@
.platform-orbit { width: 112%; margin-left: -6%; }
.platform-center { width: 112px; height: 112px; border-radius: 30px; }
.platform-center svg { width: 68px; height: 68px; }
.platform-chip { padding: 8px 10px; font-size: 9px; }
.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%; }

View File

@@ -0,0 +1,90 @@
import type { SVGProps } from "react";
export const supportedPlatforms = ["Android", "iOS", "macOS", "Windows", "Linux"] as const;
export type PlatformName = (typeof supportedPlatforms)[number];
type PlatformIconProps = Omit<SVGProps<SVGSVGElement>, "children"> & {
platform: PlatformName;
};
function AndroidIcon() {
return (
<>
<path
d="M7.25 8.15h9.5a1.5 1.5 0 0 1 1.5 1.5v6.1a1.5 1.5 0 0 1-1.5 1.5h-9.5a1.5 1.5 0 0 1-1.5-1.5v-6.1a1.5 1.5 0 0 1 1.5-1.5Z"
fill="currentColor"
/>
<path d="M8.15 8.1a3.95 3.95 0 0 1 7.7 0" fill="currentColor" />
<path d="m8.35 4.45-1.2-1.7M15.65 4.45l1.2-1.7" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" />
<path d="M4.2 10.05v5.15M19.8 10.05v5.15M8.5 17v3.05M15.5 17v3.05" stroke="currentColor" strokeWidth="2.1" strokeLinecap="round" />
<circle cx="9.5" cy="6.65" r="0.62" fill="white" />
<circle cx="14.5" cy="6.65" r="0.62" fill="white" />
</>
);
}
function AppleIcon() {
return (
<path
d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.32.07 2.24.76 3.01.82 1.15-.23 2.25-.89 3.44-.8 1.43.12 2.51.68 3.22 1.7-2.96 1.78-2.26 5.68.46 6.77-.54 1.42-1.24 2.83-2.13 4.48ZM12.03 7.25c-.15-2.13 1.59-3.89 3.58-4.06.27 2.46-2.23 4.3-3.58 4.06Z"
fill="currentColor"
/>
);
}
function MacIcon() {
return (
<>
<rect x="2.5" y="2.5" width="19" height="19" rx="5.5" fill="currentColor" />
<path d="M12 2.5v19" stroke="white" strokeOpacity="0.72" />
<path d="M8 9.15h.01M16 9.15h.01" stroke="white" strokeWidth="1.9" strokeLinecap="round" />
<path d="M7.1 15.2c2.7 1.45 7.1 1.45 9.8 0" stroke="white" strokeWidth="1.35" strokeLinecap="round" />
<path d="m13.55 6.25-2.45 8.3" stroke="white" strokeWidth="1.1" strokeLinecap="round" strokeOpacity="0.82" />
</>
);
}
function WindowsIcon() {
return (
<path
d="M3 5.15 11 4v7H3V5.15Zm9-1.3L21 2.5V11h-9V3.85ZM3 12h8v7l-8-1.15V12Zm9 0h9v8.5l-9-1.35V12Z"
fill="currentColor"
/>
);
}
function LinuxIcon() {
return (
<>
<ellipse cx="12" cy="12.6" rx="5.25" ry="7.55" fill="#242027" />
<ellipse cx="12" cy="14.55" rx="3.45" ry="4.7" fill="#f8f7f9" />
<ellipse cx="9.95" cy="8.45" rx="1.35" ry="1.8" fill="white" />
<ellipse cx="14.05" cy="8.45" rx="1.35" ry="1.8" fill="white" />
<circle cx="10.35" cy="8.65" r="0.55" fill="#242027" />
<circle cx="13.65" cy="8.65" r="0.55" fill="#242027" />
<path d="m9.8 10.15 2.2-1 2.2 1-2.2 2.1-2.2-2.1Z" fill="#f5b82e" />
<path d="M6.15 18.45c1.2-1 2.45-1.3 3.75-.75-.7 1.6-2.35 2.45-4.9 2.2.2-.6.58-1.08 1.15-1.45ZM17.85 18.45c-1.2-1-2.45-1.3-3.75-.75.7 1.6 2.35 2.45 4.9 2.2-.2-.6-.58-1.08-1.15-1.45Z" fill="#f5b82e" />
</>
);
}
export function PlatformIcon({ platform, className, ...props }: PlatformIconProps) {
return (
<svg
className={className}
data-platform={platform.toLowerCase()}
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
focusable="false"
{...props}
>
{platform === "Android" && <AndroidIcon />}
{platform === "iOS" && <AppleIcon />}
{platform === "macOS" && <MacIcon />}
{platform === "Windows" && <WindowsIcon />}
{platform === "Linux" && <LinuxIcon />}
</svg>
);
}

View File

@@ -1,23 +1,27 @@
import { BrandMark } from "@/components/brand";
const platforms = ["Android", "iOS", "macOS", "Windows", "Linux"];
import { PlatformIcon, supportedPlatforms } from "@/components/platform-icon";
export function PlatformOrbit() {
return (
<div className="platform-orbit" aria-label="VniDrop supports Android, iOS, macOS, Windows, and Linux">
<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>
{platforms.map((platform, index) => (
{supportedPlatforms.map((platform, index) => (
<span
key={platform}
className={`platform-chip platform-chip-${index + 1}`}
title={platform}
>
<i />
{platform}
<PlatformIcon className="platform-glyph" platform={platform} />
<span className="sr-only">{platform}</span>
</span>
))}
</div>

View File

@@ -75,24 +75,20 @@
z-index: 4;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
justify-content: center;
width: 58px;
height: 58px;
border: 1px solid #e2d9e6;
border-radius: 999px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.93);
box-shadow: 0 12px 35px rgba(53, 34, 61, 0.1);
color: #4b4350;
font-size: 11px;
font-weight: 700;
animation: platform-chip-float 5s ease-in-out infinite;
}
.platform-chip i {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--brand-500);
box-shadow: 0 0 0 4px var(--brand-100);
.platform-chip .platform-glyph {
width: 30px;
height: 30px;
}
.platform-chip-1 { top: 8%; left: 17%; animation-delay: -1s; }
@@ -100,3 +96,8 @@
.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

@@ -5,38 +5,24 @@ import { Icon } from "@/components/icons";
export function SiteFooter() {
return (
<footer className="site-footer">
<div className="footer-main page-shell">
<div className="footer-brand">
<span className="footer-mark">
<BrandMark />
</span>
<div>
<p className="footer-name">VniDrop</p>
<p>Send files directly. Stay in control.</p>
</div>
</div>
<div className="footer-links">
<div>
<p className="footer-label">Explore</p>
<Link href="/#how-it-works">How it works</Link>
<Link href="/#privacy">Privacy by design</Link>
<Link href="/#platforms">Platforms</Link>
</div>
<div>
<p className="footer-label">Project</p>
<a href="https://github.com/vnidrop/vnidrop" target="_blank" rel="noreferrer">
GitHub <Icon name="github" />
</a>
<a
href="https://github.com/vnidrop/vnidrop/blob/master/LICENSE"
target="_blank"
rel="noreferrer"
>
Apache 2.0 <Icon name="arrow" />
</a>
<Link href="/privacy/">Privacy policy</Link>
</div>
<div className="footer-inner page-shell">
<div className="footer-identity">
<BrandMark />
<span>VniDrop</span>
</div>
<nav className="footer-links" aria-label="Footer navigation">
<a href="https://github.com/vnidrop/vnidrop" target="_blank" rel="noreferrer">
<Icon name="github" /> GitHub
</a>
<Link href="/privacy/">Privacy</Link>
<a
href="https://github.com/vnidrop/vnidrop/blob/master/LICENSE"
target="_blank"
rel="noreferrer"
>
Apache 2.0
</a>
</nav>
</div>
<div className="footer-bottom page-shell">
<p>© 2026 VniDrop contributors.</p>

BIN
docs/public/App.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB