mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(docs): add animated product website
This commit is contained in:
10
docs/.gitignore
vendored
Normal file
10
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.next/
|
||||
out/
|
||||
.vercel/
|
||||
*.tsbuildinfo
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.env*
|
||||
!.env.example
|
||||
22
docs/README.md
Normal file
22
docs/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# VniDrop website
|
||||
|
||||
The product website for VniDrop, built with Next.js and exported as a static site.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Checks
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run typecheck
|
||||
npm run build
|
||||
```
|
||||
|
||||
The production build is written to `out/` and can be hosted by any static web server.
|
||||
325
docs/app/base.css
Normal file
325
docs/app/base.css
Normal file
@@ -0,0 +1,325 @@
|
||||
:root {
|
||||
--brand-700: #6f27e9;
|
||||
--brand-600: #842fee;
|
||||
--brand-500: #a855f7;
|
||||
--brand-400: #c084fc;
|
||||
--brand-300: #d8b4fe;
|
||||
--brand-200: #ead4ff;
|
||||
--brand-100: #f5eaff;
|
||||
--ink: #171419;
|
||||
--ink-soft: #4f4854;
|
||||
--ink-muted: #786f7d;
|
||||
--paper: #fbfafc;
|
||||
--paper-warm: #f7f4f8;
|
||||
--surface: #ffffff;
|
||||
--surface-soft: #f3eff5;
|
||||
--line: #e5dfe8;
|
||||
--line-strong: #d4cad8;
|
||||
--dark: #17131a;
|
||||
--dark-raised: #211a25;
|
||||
--dark-line: #362c3c;
|
||||
--success: #24a66b;
|
||||
--warning: #f59e0b;
|
||||
--font-sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-display: "Avenir Next", Avenir, "Segoe UI", ui-sans-serif, sans-serif;
|
||||
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
--shadow-sm: 0 8px 30px rgba(41, 25, 48, 0.07);
|
||||
--shadow-lg: 0 30px 90px rgba(49, 25, 60, 0.14);
|
||||
--shell: 1220px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 92px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--brand-300);
|
||||
color: #28132f;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: var(--font-display);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 3px solid rgba(168, 85, 247, 0.55);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
width: min(calc(100% - 48px), var(--shell));
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
transform: translateY(-140%);
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
background: var(--ink);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.site-header {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 16px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: min(calc(100% - 32px), 1260px);
|
||||
margin-inline: auto;
|
||||
border: 1px solid rgba(220, 212, 224, 0.82);
|
||||
border-radius: 18px;
|
||||
background: rgba(251, 250, 252, 0.82);
|
||||
box-shadow: 0 12px 40px rgba(42, 26, 49, 0.08);
|
||||
backdrop-filter: blur(20px) saturate(1.4);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.4);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
padding: 0 10px 0 17px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: max-content;
|
||||
font-family: var(--font-display);
|
||||
font-size: 19px;
|
||||
font-weight: 750;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.brand-mark-shell {
|
||||
display: grid;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border: 1px solid #eadcf1;
|
||||
border-radius: 11px;
|
||||
background: white;
|
||||
box-shadow: 0 5px 15px rgba(105, 42, 145, 0.08);
|
||||
}
|
||||
|
||||
.brand-mark-shell svg {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 4px;
|
||||
border-radius: 13px;
|
||||
background: rgba(239, 235, 241, 0.7);
|
||||
}
|
||||
|
||||
.desktop-nav a {
|
||||
padding: 7px 14px;
|
||||
border-radius: 10px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
transition: background 160ms ease, color 160ms ease;
|
||||
}
|
||||
|
||||
.desktop-nav a:hover {
|
||||
background: white;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.header-github {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
display: none;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 11px;
|
||||
background: var(--surface-soft);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-button svg {
|
||||
width: 21px;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
min-height: 52px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 0 21px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 730;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease,
|
||||
border-color 180ms ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.button svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.button-small {
|
||||
min-height: 44px;
|
||||
padding-inline: 16px;
|
||||
border-radius: 11px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background: linear-gradient(135deg, var(--brand-500), var(--brand-700));
|
||||
box-shadow: 0 12px 28px rgba(132, 47, 238, 0.24), inset 0 1px rgba(255, 255, 255, 0.35);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
box-shadow: 0 16px 34px rgba(132, 47, 238, 0.3), inset 0 1px rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.button-ghost {
|
||||
border-color: var(--line);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.button-ghost:hover {
|
||||
border-color: var(--line-strong);
|
||||
background: white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.button-dark {
|
||||
background: var(--ink);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-light {
|
||||
background: white;
|
||||
color: #211925;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.button-dark-ghost {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.motion-ready .reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(28px);
|
||||
transition: opacity 700ms cubic-bezier(0.2, 0.75, 0.25, 1),
|
||||
transform 700ms cubic-bezier(0.2, 0.75, 0.25, 1);
|
||||
transition-delay: var(--reveal-delay, 0ms);
|
||||
}
|
||||
|
||||
.motion-ready .reveal.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
103
docs/app/footer.css
Normal file
103
docs/app/footer.css
Normal file
@@ -0,0 +1,103 @@
|
||||
.site-footer {
|
||||
padding-top: 75px;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.footer-main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 60px;
|
||||
padding-bottom: 70px;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.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 svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.footer-brand p {
|
||||
margin-bottom: 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer-brand .footer-name {
|
||||
margin-bottom: 3px;
|
||||
color: var(--ink);
|
||||
font-family: var(--font-display);
|
||||
font-size: 21px;
|
||||
font-weight: 750;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.footer-links svg {
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 22px;
|
||||
padding-bottom: 28px;
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--ink-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.footer-bottom p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
701
docs/app/home.css
Normal file
701
docs/app/home.css
Normal file
@@ -0,0 +1,701 @@
|
||||
.hero-section {
|
||||
position: relative;
|
||||
min-height: 850px;
|
||||
overflow: hidden;
|
||||
padding: 150px 0 86px;
|
||||
background:
|
||||
radial-gradient(circle at 82% 18%, rgba(211, 165, 255, 0.24), transparent 28%),
|
||||
radial-gradient(circle at 12% 76%, rgba(184, 108, 255, 0.12), transparent 25%),
|
||||
linear-gradient(180deg, #fdfcfe 0%, #faf7fb 100%);
|
||||
}
|
||||
|
||||
.hero-section::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(rgba(93, 71, 102, 0.1) 0.7px, transparent 0.7px);
|
||||
background-size: 19px 19px;
|
||||
content: "";
|
||||
mask-image: linear-gradient(to bottom, black, transparent 82%);
|
||||
opacity: 0.38;
|
||||
}
|
||||
|
||||
.hero-ambient {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(168, 85, 247, 0.18);
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-ambient-one {
|
||||
top: 120px;
|
||||
right: -80px;
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
animation: ambient-drift 15s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.hero-ambient-two {
|
||||
bottom: -190px;
|
||||
left: -120px;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
animation: ambient-drift 18s ease-in-out -5s infinite alternate-reverse;
|
||||
}
|
||||
|
||||
.hero-layout {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.88fr) minmax(540px, 1.12fr);
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.eyebrow-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin-bottom: 25px;
|
||||
padding: 7px 11px;
|
||||
border: 1px solid #dfd4e4;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: #665c6b;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.eyebrow-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--warning);
|
||||
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.13);
|
||||
}
|
||||
|
||||
.hero-copy h1 {
|
||||
margin-bottom: 28px;
|
||||
font-size: clamp(3.5rem, 5.1vw, 5.4rem);
|
||||
font-weight: 760;
|
||||
letter-spacing: -0.072em;
|
||||
line-height: 0.98;
|
||||
}
|
||||
|
||||
.hero-copy h1 span {
|
||||
background: linear-gradient(100deg, var(--brand-600), #ba62f8 58%, #7c2aef);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
max-width: 610px;
|
||||
margin-bottom: 30px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 18px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.hero-platforms {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-top: 35px;
|
||||
color: var(--ink-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.hero-platforms > span {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero-platforms ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.hero-platforms li {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 7px;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.hero-art-reveal {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.trust-section {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
margin-top: -32px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.trust-item p {
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 18px;
|
||||
color: var(--brand-600);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.13em;
|
||||
}
|
||||
|
||||
.kicker-light {
|
||||
color: #d89bff;
|
||||
}
|
||||
|
||||
.flow-section {
|
||||
padding-top: 105px;
|
||||
}
|
||||
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.privacy-point:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: rgba(192, 132, 252, 0.45);
|
||||
background: rgba(255, 255, 255, 0.055);
|
||||
}
|
||||
|
||||
.privacy-point-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 43px;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.privacy-point h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 19px;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.privacy-point p {
|
||||
margin-bottom: 0;
|
||||
color: #a99fac;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.privacy-policy-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 30px;
|
||||
margin-top: 38px;
|
||||
padding-top: 27px;
|
||||
border-top: 1px solid var(--dark-line);
|
||||
}
|
||||
|
||||
.privacy-policy-link p {
|
||||
margin: 0;
|
||||
color: #a99fac;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
10
docs/app/icon.svg
Normal file
10
docs/app/icon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" rx="16" fill="#FBF7FF"/>
|
||||
<path d="M16 12h8.2c2.1 0 3.8 1.7 3.8 3.8v20.4c0 7 4.8 12.2 11.5 12.2S51 43.2 51 36.2V15.8c0-2.1 1.7-3.8 3.8-3.8H58v24.4C58 47.7 50 56 39.5 56S21 47.7 21 36.4V24h-3v-5h-2v-7Z" fill="url(#a)"/>
|
||||
<path d="M39.5 26.5c-2.5 3.3-6.5 8.6-6.5 12.4 0 4 2.8 7.1 6.5 7.1s6.5-3.1 6.5-7.1c0-3.8-4-9.1-6.5-12.4Z" fill="url(#b)"/>
|
||||
<path d="M51 12h7v7l-7-7Z" fill="#E7CBFF"/>
|
||||
<defs>
|
||||
<linearGradient id="a" x1="16" y1="12" x2="59" y2="51" gradientUnits="userSpaceOnUse"><stop stop-color="#B45CFA"/><stop offset="1" stop-color="#7828ED"/></linearGradient>
|
||||
<linearGradient id="b" x1="33" y1="27" x2="47" y2="43" gradientUnits="userSpaceOnUse"><stop stop-color="#B45CFA"/><stop offset="1" stop-color="#7828ED"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 893 B |
71
docs/app/layout.tsx
Normal file
71
docs/app/layout.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { SiteHeader } from "@/components/site-header";
|
||||
import "./base.css";
|
||||
import "./home.css";
|
||||
import "./footer.css";
|
||||
import "./privacy/privacy-document.css";
|
||||
import "../components/hero-scene.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: {
|
||||
default: "VniDrop — Direct file transfer, on your terms",
|
||||
template: "%s · VniDrop",
|
||||
},
|
||||
description:
|
||||
"Send files and folders directly across Android, iOS, macOS, Windows, and Linux—with approval by default and no hosted transfer copy.",
|
||||
applicationName: "VniDrop",
|
||||
manifest: "/site.webmanifest",
|
||||
keywords: [
|
||||
"peer-to-peer file transfer",
|
||||
"encrypted file sharing",
|
||||
"cross-platform file transfer",
|
||||
"open source",
|
||||
],
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName: "VniDrop",
|
||||
title: "VniDrop — Direct file transfer, on your terms",
|
||||
description:
|
||||
"Move files from your device to theirs, with no account and no hosted transfer copy.",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title: "VniDrop — Direct file transfer, on your terms",
|
||||
description:
|
||||
"Move files from your device to theirs, with no account and no hosted transfer copy.",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#fbfafc" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#17131a" },
|
||||
],
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<a className="skip-link" href="#main-content">
|
||||
Skip to content
|
||||
</a>
|
||||
<SiteHeader />
|
||||
{children}
|
||||
<SiteFooter />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
292
docs/app/page.tsx
Normal file
292
docs/app/page.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
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 { 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<{
|
||||
icon: IconName;
|
||||
number: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}> = [
|
||||
{
|
||||
icon: "lock",
|
||||
number: "01",
|
||||
title: "Encrypted in transit",
|
||||
text: "Each connection is authenticated and end-to-end encrypted, including through a relay.",
|
||||
},
|
||||
{
|
||||
icon: "shield",
|
||||
number: "02",
|
||||
title: "Approval by default",
|
||||
text: "An invitation helps devices meet. It is not automatic permission to 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.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main id="main-content">
|
||||
<section className="hero-section">
|
||||
<div className="hero-ambient hero-ambient-one" aria-hidden="true" />
|
||||
<div className="hero-ambient hero-ambient-two" aria-hidden="true" />
|
||||
<div className="page-shell hero-layout">
|
||||
<div className="hero-copy">
|
||||
<Reveal>
|
||||
<div className="eyebrow-badge">
|
||||
<span className="eyebrow-dot" />
|
||||
OPEN SOURCE · EARLY DEVELOPMENT
|
||||
</div>
|
||||
<h1>
|
||||
Your files.
|
||||
<br />
|
||||
<span>A straight line</span>
|
||||
<br />
|
||||
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.
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<Reveal className="hero-art-reveal" delay={120}>
|
||||
<HeroIllustration />
|
||||
</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">
|
||||
<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>
|
||||
<p>
|
||||
VniDrop keeps the transfer understandable from first selection to final verified file.
|
||||
</p>
|
||||
</Reveal>
|
||||
<Reveal delay={100}>
|
||||
<TransferFlow />
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section 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 else’s 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>
|
||||
<h3>{point.title}</h3>
|
||||
<p>{point.text}</p>
|
||||
</Reveal>
|
||||
))}
|
||||
</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
|
||||
<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 directly—even 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>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
381
docs/app/privacy/page.tsx
Normal file
381
docs/app/privacy/page.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
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",
|
||||
description:
|
||||
"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"],
|
||||
["local-data", "Local data"],
|
||||
["diagnostics", "Diagnostics"],
|
||||
["website", "Website"],
|
||||
["permissions", "Permissions"],
|
||||
["providers", "Service providers"],
|
||||
["retention", "Retention"],
|
||||
["choices", "Your choices"],
|
||||
["security", "Security"],
|
||||
["changes", "Changes"],
|
||||
["contact", "Contact"],
|
||||
];
|
||||
|
||||
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>
|
||||
</section>
|
||||
|
||||
<section className="privacy-document-section">
|
||||
<div className="page-shell privacy-document-layout">
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
</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
|
||||
Android, iOS, macOS, Windows, and Linux, and the diagnostics service configured by
|
||||
the official project. In this policy, “VniDrop,” “we,” and “us” refer to the
|
||||
maintainers of the official VniDrop project and the official builds they distribute.
|
||||
</p>
|
||||
<p>
|
||||
VniDrop is open-source software. A build distributed or operated by someone else
|
||||
may use different networking infrastructure, diagnostics settings, or website
|
||||
hosting. That distributor is responsible for explaining its own practices.
|
||||
</p>
|
||||
</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>
|
||||
The sender chooses files or folders on their device. VniDrop streams those bytes to
|
||||
an approved receiver and does not first upload them to a VniDrop-hosted storage
|
||||
bucket. The receiver saves the files to a destination they choose. Relayed traffic
|
||||
remains end-to-end encrypted.
|
||||
</p>
|
||||
<h3>Invitations and transfer metadata</h3>
|
||||
<p>
|
||||
A QR code, NFC tag, or <code>.vnd</code> file contains a transfer invitation. The
|
||||
invitation includes connection and content identifiers plus transfer metadata such
|
||||
as the transfer name, optional sender name, creation time, file count, and total
|
||||
size. It is a capability: anyone who receives it may be able to request the transfer
|
||||
while the share is active. Treat it like a private access link.
|
||||
</p>
|
||||
<h3>What peers and relays can see</h3>
|
||||
<p>
|
||||
A receive request can disclose the receiver’s chosen display or device name,
|
||||
application version, and a technical endpoint identifier to the sender. A direct
|
||||
connection exposes the peers’ IP addresses to one another. When a public relay is
|
||||
used, its operator can observe connection metadata such as source and destination IP
|
||||
addresses, connection time, and the amount of relayed data, but cannot read the
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
<li>device identity and networking keys used to establish secure connections;</li>
|
||||
<li>active shares, transfer history, receiver requests, progress, and status;</li>
|
||||
<li>app preferences, including access and diagnostics choices;</li>
|
||||
<li>download destinations and locally managed transfer data; and</li>
|
||||
<li>an anonymous installation identifier used only for diagnostics correlation.</li>
|
||||
</ul>
|
||||
<p>
|
||||
This information remains until you remove the relevant history, stop or delete a
|
||||
share, clear the app’s data, or uninstall the app, subject to operating-system file
|
||||
behavior. Removing VniDrop history does not delete a file you already downloaded;
|
||||
delete that file through your operating system if you no longer want it.
|
||||
</p>
|
||||
</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>
|
||||
When an official build includes diagnostics, automatic usage events and crash
|
||||
reports are disabled until you enable “Share diagnostics.” If enabled, VniDrop may
|
||||
send an anonymous installation ID, app version, platform, sparse event names and
|
||||
properties, crash type and message, a redacted stack trace, timestamps, and recent
|
||||
in-app breadcrumbs. You can turn this off at any time; doing so also removes pending
|
||||
local crash reports.
|
||||
</p>
|
||||
<h3>User-submitted bug reports</h3>
|
||||
<p>
|
||||
A bug report is separate from the diagnostics toggle and is sent only when you press
|
||||
submit. It can contain what you say happened, what you expected, reproduction steps,
|
||||
an optional contact email, app and platform versions, an anonymous installation ID,
|
||||
device name and model, operating system, network and battery information, recent
|
||||
breadcrumbs, and optional recent logs. You can exclude logs before submitting.
|
||||
</p>
|
||||
<h3>Data deliberately excluded</h3>
|
||||
<p>
|
||||
Automatic diagnostics are designed to exclude transfer contents, invitations, and
|
||||
file paths. Before diagnostic text or optional logs are sent, VniDrop applies rules
|
||||
intended to redact invitation tokens, endpoint identifiers, absolute paths, file and
|
||||
content URIs, and platform document identifiers. No redaction system is perfect, so
|
||||
review anything you type into a bug report and avoid including secrets.
|
||||
</p>
|
||||
</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,
|
||||
advertising, behavioral analytics, marketing pixels, or non-essential cookies. It
|
||||
does not ask the browser for access to your files, camera, contacts, location, or
|
||||
nearby devices.
|
||||
</p>
|
||||
<p>
|
||||
The hosting and security infrastructure may process routine request information—such
|
||||
as IP address, time, requested page, referrer, and browser user agent—to deliver the
|
||||
site, maintain reliability, and prevent abuse. The live hosting provider must be
|
||||
identified in this policy before public deployment if it differs from the providers
|
||||
described below.
|
||||
</p>
|
||||
</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>
|
||||
<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
|
||||
permission prompts and service-provider terms also apply.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="providers" className="policy-section">
|
||||
<span className="policy-number">07</span>
|
||||
<h2>Infrastructure and external services</h2>
|
||||
<div 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>
|
||||
</div>
|
||||
<div>
|
||||
<span>C</span>
|
||||
<div><strong>Cloudflare</strong><p>The project’s diagnostics design uses Cloudflare Workers, D1, and R2. Cloudflare also processes source IPs for request delivery and abuse controls.</p></div>
|
||||
</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>
|
||||
</div>
|
||||
<div>
|
||||
<span>GH</span>
|
||||
<div><strong>GitHub</strong><p>Hosts the source repository, issue tracker, and external pages linked from this site. GitHub’s own privacy terms apply after you follow those links.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<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>.
|
||||
</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>
|
||||
<p>
|
||||
Operational backups, provider logs, and deletion backlogs may persist briefly beyond
|
||||
the stated period where necessary for security, integrity, or legal obligations. If
|
||||
the production diagnostics retention configuration changes, this policy should be
|
||||
updated to match it.
|
||||
</p>
|
||||
</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>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>
|
||||
</ul>
|
||||
<p>
|
||||
Depending on where you live, privacy law may provide rights to access, correct,
|
||||
delete, restrict, or object to processing of personal information. Because VniDrop
|
||||
has no account and automatic diagnostics use an anonymous installation ID, we may
|
||||
not be able to connect a server record to you without additional information. Use
|
||||
the contact method below and provide only what is needed to locate your submission.
|
||||
</p>
|
||||
</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,
|
||||
deny-by-default share access, bounded diagnostics payloads, redaction, and safe file
|
||||
publishing that avoids silently replacing an existing file. No system can guarantee
|
||||
absolute security. Keep invitations private, verify receiver names, keep your device
|
||||
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.
|
||||
</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
|
||||
policy changes, we will update the effective date and version at the top of the page
|
||||
and publish the revised text with the project. Material changes should be called out
|
||||
in release notes or the application where practical.
|
||||
</p>
|
||||
</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
|
||||
dedicated privacy email or postal address. For a privacy question or request, open a
|
||||
request in the project issue tracker. Do not put an invitation, file content,
|
||||
credentials, or other sensitive information in a public issue.
|
||||
</p>
|
||||
<a
|
||||
className="button button-primary"
|
||||
href="https://github.com/vnidrop/vnidrop/issues/new"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Contact the maintainers
|
||||
<Icon name="arrow" />
|
||||
</a>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
660
docs/app/privacy/privacy-document.css
Normal file
660
docs/app/privacy/privacy-document.css
Normal file
@@ -0,0 +1,660 @@
|
||||
.privacy-page {
|
||||
background: #fcfbfd;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
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-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;
|
||||
color: var(--ink-soft);
|
||||
font-size: 18px;
|
||||
line-height: 1.72;
|
||||
}
|
||||
|
||||
.privacy-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
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 > svg {
|
||||
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;
|
||||
}
|
||||
|
||||
.privacy-document-section {
|
||||
padding: 135px 0 155px;
|
||||
}
|
||||
|
||||
.privacy-document-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 210px minmax(0, 780px);
|
||||
justify-content: center;
|
||||
gap: 105px;
|
||||
}
|
||||
|
||||
.privacy-toc {
|
||||
position: sticky;
|
||||
top: 112px;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.privacy-toc > p {
|
||||
margin-bottom: 17px;
|
||||
color: var(--ink);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.privacy-toc nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.privacy-callout strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.privacy-callout p {
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.policy-section {
|
||||
position: relative;
|
||||
scroll-margin-top: 110px;
|
||||
padding: 0 0 72px;
|
||||
}
|
||||
|
||||
.policy-section + .policy-section {
|
||||
padding-top: 3px;
|
||||
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;
|
||||
}
|
||||
|
||||
.policy-section h3 {
|
||||
margin: 34px 0 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 720;
|
||||
letter-spacing: -0.018em;
|
||||
}
|
||||
|
||||
.policy-section p,
|
||||
.policy-section li {
|
||||
color: #514956;
|
||||
font-size: 14px;
|
||||
line-height: 1.82;
|
||||
}
|
||||
|
||||
.policy-section p {
|
||||
margin-bottom: 19px;
|
||||
}
|
||||
|
||||
.policy-section ul {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 17px 0 22px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.policy-section li::marker {
|
||||
color: var(--brand-500);
|
||||
}
|
||||
|
||||
.policy-section code {
|
||||
padding: 2px 6px;
|
||||
border: 1px solid #e7dceb;
|
||||
border-radius: 5px;
|
||||
background: #f8f3fa;
|
||||
color: var(--brand-700);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.86em;
|
||||
}
|
||||
|
||||
.policy-section a:not(.button) {
|
||||
color: var(--brand-700);
|
||||
font-weight: 650;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(132, 47, 238, 0.28);
|
||||
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-note svg {
|
||||
width: 21px;
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.retention-row span + span {
|
||||
border-left: 1px solid var(--line);
|
||||
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;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.policy-contact p {
|
||||
max-width: 620px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.policy-contact .button {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
156
docs/app/privacy/privacy-responsive.css
Normal file
156
docs/app/privacy/privacy-responsive.css
Normal file
@@ -0,0 +1,156 @@
|
||||
@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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.privacy-toc {
|
||||
position: relative;
|
||||
top: auto;
|
||||
padding-bottom: 25px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.privacy-toc nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.privacy-hero {
|
||||
padding: 120px 0 90px;
|
||||
}
|
||||
|
||||
.privacy-back {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.privacy-hero-copy h1 {
|
||||
font-size: clamp(3rem, 15vw, 4.5rem);
|
||||
}
|
||||
|
||||
.privacy-hero-copy > p {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.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-document-section {
|
||||
padding: 95px 0 105px;
|
||||
}
|
||||
|
||||
.privacy-toc nav {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.policy-contact {
|
||||
padding: 35px 24px !important;
|
||||
}
|
||||
}
|
||||
322
docs/app/responsive.css
Normal file
322
docs/app/responsive.css
Normal file
@@ -0,0 +1,322 @@
|
||||
@keyframes ambient-drift {
|
||||
from { transform: translate3d(-10px, -8px, 0) rotate(0deg); }
|
||||
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);
|
||||
}
|
||||
|
||||
.hero-copy h1 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.desktop-nav,
|
||||
.header-github {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
display: grid;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 12px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: max-height 250ms ease, padding 250ms ease, opacity 180ms ease, visibility 250ms;
|
||||
}
|
||||
|
||||
.mobile-nav.is-open {
|
||||
max-height: 280px;
|
||||
padding: 4px 12px 12px;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.mobile-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 10px;
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--ink-soft);
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.mobile-nav svg {
|
||||
width: 17px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding-top: 128px;
|
||||
}
|
||||
|
||||
.hero-layout {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 35px;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.hero-actions,
|
||||
.hero-platforms {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.trust-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.page-shell {
|
||||
width: min(calc(100% - 32px), var(--shell));
|
||||
}
|
||||
|
||||
.site-header {
|
||||
top: 10px;
|
||||
width: calc(100% - 20px);
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
height: 58px;
|
||||
padding-left: 11px;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.brand-mark-shell {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
min-height: auto;
|
||||
padding: 115px 0 72px;
|
||||
}
|
||||
|
||||
.hero-copy h1 {
|
||||
font-size: clamp(2.75rem, 14vw, 4.25rem);
|
||||
letter-spacing: -0.065em;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.hero-actions .button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-platforms {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.hero-platforms ul {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.trust-section {
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
.trust-grid {
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.trust-item {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: clamp(2.35rem, 12vw, 3.5rem);
|
||||
}
|
||||
|
||||
.section-heading p,
|
||||
.section-lead {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.approval-section,
|
||||
.platforms-section,
|
||||
.network-section,
|
||||
.privacy-principles-section {
|
||||
padding: 100px 0;
|
||||
}
|
||||
|
||||
.approval-layout {
|
||||
gap: 55px;
|
||||
}
|
||||
|
||||
.privacy-point-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.privacy-point {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.privacy-point-top {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.privacy-policy-link {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.final-cta-section {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.final-cta {
|
||||
padding: 76px 22px;
|
||||
border-radius: 23px;
|
||||
}
|
||||
|
||||
.final-cta-actions {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.footer-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
gap: 35px;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.motion-ready .reveal {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
86
docs/components/approval-demo.tsx
Normal file
86
docs/components/approval-demo.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"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" && "Maya’s 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>
|
||||
);
|
||||
}
|
||||
267
docs/components/approval-scene.css
Normal file
267
docs/components/approval-scene.css
Normal file
@@ -0,0 +1,267 @@
|
||||
/* 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); }
|
||||
|
||||
65
docs/components/brand.tsx
Normal file
65
docs/components/brand.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import Link from "next/link";
|
||||
|
||||
type BrandMarkProps = {
|
||||
className?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export function BrandMark({ className, title }: BrandMarkProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
role={title ? "img" : undefined}
|
||||
aria-hidden={title ? undefined : true}
|
||||
aria-label={title}
|
||||
>
|
||||
<path
|
||||
d="M11 8h10.6a4.8 4.8 0 0 1 4.8 4.8v25.4c0 8.7 6.1 15.2 14.5 15.2 8.3 0 14.1-6.5 14.1-15.2V12.8A4.8 4.8 0 0 1 59.8 8H62v30.4C62 52.5 53.3 61 40.9 61 28.3 61 19 52.5 19 38.4V23h-3.5v-6.4H11V8Z"
|
||||
fill="url(#brand-mark-gradient)"
|
||||
/>
|
||||
<path
|
||||
d="M40.8 25.9c-3.1 4.1-8.2 10.8-8.2 15.5 0 5.1 3.6 8.9 8.2 8.9 4.8 0 8.3-3.8 8.3-8.9 0-4.7-5.2-11.4-8.3-15.5Z"
|
||||
fill="url(#brand-drop-gradient)"
|
||||
/>
|
||||
<path d="M54.9 8h7v7l-7-7Z" fill="#E8CDFF" />
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="brand-mark-gradient"
|
||||
x1="11"
|
||||
y1="8"
|
||||
x2="64"
|
||||
y2="55"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#B45CFA" />
|
||||
<stop offset="0.5" stopColor="#9D4DF4" />
|
||||
<stop offset="1" stopColor="#7828ED" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="brand-drop-gradient"
|
||||
x1="32"
|
||||
y1="26"
|
||||
x2="51"
|
||||
y2="47"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#C06CFF" />
|
||||
<stop offset="1" stopColor="#7828ED" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Brand({ compact = false }: { compact?: boolean }) {
|
||||
return (
|
||||
<Link className="brand" href="/" aria-label="VniDrop home">
|
||||
<span className="brand-mark-shell">
|
||||
<BrandMark />
|
||||
</span>
|
||||
{!compact && <span className="brand-name">VniDrop</span>}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
216
docs/components/hero-illustration.tsx
Normal file
216
docs/components/hero-illustration.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties, PointerEvent } from "react";
|
||||
import { useRef } from "react";
|
||||
import { Icon } from "@/components/icons";
|
||||
|
||||
type StageStyle = CSSProperties & {
|
||||
"--pointer-x": string;
|
||||
"--pointer-y": string;
|
||||
};
|
||||
|
||||
export function HeroIllustration() {
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updateParallax = (event: PointerEvent<HTMLDivElement>) => {
|
||||
const stage = stageRef.current;
|
||||
if (!stage || window.matchMedia("(prefers-reduced-motion: reduce)").matches) 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");
|
||||
};
|
||||
|
||||
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-grid" />
|
||||
<div className="hero-glow hero-glow-one" />
|
||||
<div className="hero-glow hero-glow-two" />
|
||||
<div className="hero-status hero-status-secure">
|
||||
<span className="status-icon">
|
||||
<Icon name="lock" />
|
||||
</span>
|
||||
<span>
|
||||
<strong>End-to-end encrypted</strong>
|
||||
<small>Authenticated connection</small>
|
||||
</span>
|
||||
</div>
|
||||
<div className="hero-status hero-status-route">
|
||||
<span className="status-pulse" />
|
||||
Direct route found
|
||||
</div>
|
||||
<svg className="hero-scene" viewBox="0 0 760 620">
|
||||
<defs>
|
||||
<linearGradient id="hero-purple" x1="240" y1="90" x2="620" y2="520">
|
||||
<stop stopColor="#C16BFF" />
|
||||
<stop offset="0.48" stopColor="#A855F7" />
|
||||
<stop offset="1" stopColor="#6F27E9" />
|
||||
</linearGradient>
|
||||
<linearGradient id="hero-screen" x1="88" y1="118" x2="342" y2="345">
|
||||
<stop stopColor="#25202C" />
|
||||
<stop offset="1" stopColor="#151319" />
|
||||
</linearGradient>
|
||||
<linearGradient id="hero-phone" x1="540" y1="160" x2="692" y2="465">
|
||||
<stop stopColor="#2A2530" />
|
||||
<stop offset="1" stopColor="#141217" />
|
||||
</linearGradient>
|
||||
<radialGradient id="hero-orb">
|
||||
<stop stopColor="#E3BDFF" stopOpacity="0.9" />
|
||||
<stop offset="0.45" stopColor="#A855F7" stopOpacity="0.45" />
|
||||
<stop offset="1" stopColor="#A855F7" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<filter id="hero-shadow" x="-30%" y="-30%" width="160%" height="180%">
|
||||
<feDropShadow dx="0" dy="20" stdDeviation="18" floodColor="#3F2454" floodOpacity="0.16" />
|
||||
</filter>
|
||||
<filter id="hero-glow-filter" x="-100%" y="-100%" width="300%" height="300%">
|
||||
<feGaussianBlur stdDeviation="12" />
|
||||
</filter>
|
||||
<clipPath id="laptop-screen-clip">
|
||||
<rect x="82" y="132" width="264" height="176" rx="12" />
|
||||
</clipPath>
|
||||
<clipPath id="phone-screen-clip">
|
||||
<rect x="548" y="171" width="136" height="279" rx="25" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<g className="scene-orbits">
|
||||
<ellipse cx="385" cy="325" rx="315" ry="216" fill="none" stroke="#B969F8" strokeOpacity="0.14" />
|
||||
<ellipse cx="385" cy="325" rx="245" ry="169" fill="none" stroke="#B969F8" strokeOpacity="0.12" strokeDasharray="8 14" />
|
||||
<circle cx="670" cy="215" r="5" fill="#A855F7" />
|
||||
<circle cx="117" cy="455" r="4" fill="#C98AFD" />
|
||||
<path d="m704 345 4 10 10 4-10 4-4 10-4-10-10-4 10-4Z" fill="#D9AEFF" />
|
||||
</g>
|
||||
|
||||
<g className="scene-laptop" filter="url(#hero-shadow)">
|
||||
<rect x="69" y="117" width="290" height="207" rx="19" fill="#E8E4EB" stroke="#D6CFDA" strokeWidth="2" />
|
||||
<rect x="80" y="129" width="268" height="183" rx="13" fill="url(#hero-screen)" />
|
||||
<g clipPath="url(#laptop-screen-clip)">
|
||||
<circle cx="208" cy="235" r="108" fill="#A855F7" opacity="0.07" />
|
||||
<path d="M80 278c62-45 99-24 141-60 39-34 70-39 127-31v125H80Z" fill="#A855F7" opacity="0.09" />
|
||||
<rect x="101" y="151" width="101" height="13" rx="6.5" fill="#3E3745" />
|
||||
<rect x="101" y="175" width="67" height="7" rx="3.5" fill="#5E5366" />
|
||||
<g className="laptop-files">
|
||||
<rect x="118" y="205" width="76" height="79" rx="10" fill="#F8F2FC" stroke="#C98AFD" />
|
||||
<path d="M173 205h21v21Z" fill="#E3C3FA" />
|
||||
<rect x="129" y="219" width="24" height="24" rx="6" fill="#E8D0FB" />
|
||||
<path d="m135 238 7-8 5 5 4-4 6 7Z" fill="#A855F7" opacity="0.75" />
|
||||
<rect x="129" y="253" width="46" height="6" rx="3" fill="#C3B6CB" />
|
||||
<rect x="129" y="265" width="34" height="5" rx="2.5" fill="#DDD4E2" />
|
||||
<rect x="181" y="223" width="82" height="68" rx="10" fill="#2F2935" stroke="#574D5E" />
|
||||
<path d="M242 223h21v21Z" fill="#4F4557" />
|
||||
<circle cx="205" cy="245" r="9" fill="#A855F7" opacity="0.75" />
|
||||
<rect x="195" y="264" width="49" height="6" rx="3" fill="#63586A" />
|
||||
<rect x="195" y="276" width="34" height="5" rx="2.5" fill="#4D4453" />
|
||||
</g>
|
||||
</g>
|
||||
<path d="M46 324h336l-20 23a20 20 0 0 1-15 6H81a20 20 0 0 1-15-7Z" fill="#D9D3DD" stroke="#CAC1CE" strokeWidth="2" />
|
||||
<path d="M183 324h62l-7 12h-48Z" fill="#C1B8C6" />
|
||||
</g>
|
||||
|
||||
<g className="transfer-conduit">
|
||||
<path
|
||||
className="conduit-glow"
|
||||
d="M284 358c23 134 225 151 279 35"
|
||||
fill="none"
|
||||
stroke="#B65DF9"
|
||||
strokeOpacity="0.2"
|
||||
strokeWidth="34"
|
||||
strokeLinecap="round"
|
||||
filter="url(#hero-glow-filter)"
|
||||
/>
|
||||
<path
|
||||
d="M284 358c23 134 225 151 279 35"
|
||||
fill="none"
|
||||
stroke="#ECE3F1"
|
||||
strokeWidth="19"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
className="conduit-energy"
|
||||
d="M284 358c23 134 225 151 279 35"
|
||||
fill="none"
|
||||
stroke="url(#hero-purple)"
|
||||
strokeWidth="7"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="2 17"
|
||||
/>
|
||||
<circle cx="284" cy="358" r="25" fill="#F8F2FC" stroke="#C98AFD" strokeWidth="2" />
|
||||
<circle cx="284" cy="358" r="13" fill="#A855F7" opacity="0.15" />
|
||||
<path d="m276 358 7 7 11-14" fill="none" stroke="#9333EA" strokeWidth="3" strokeLinecap="round" />
|
||||
</g>
|
||||
|
||||
<g className="approval-gate" filter="url(#hero-shadow)">
|
||||
<rect x="365" y="414" width="151" height="58" rx="18" fill="#FEFCFF" stroke="#DFCDEB" />
|
||||
<circle cx="393" cy="443" r="15" fill="#F0DDFE" />
|
||||
<path d="M393 433.5 385.5 437v5.3c0 4.5 3.2 7.6 7.5 8.9 4.3-1.3 7.5-4.4 7.5-8.9V437Z" fill="#A855F7" />
|
||||
<path d="m389.5 442 2.2 2.2 4.5-4.6" fill="none" stroke="white" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<text x="418" y="439" fill="#201B24" fontSize="12" fontWeight="700">Approved</text>
|
||||
<text x="418" y="455" fill="#74697B" fontSize="10">Maya’s phone</text>
|
||||
</g>
|
||||
|
||||
<g className="scene-phone" filter="url(#hero-shadow)">
|
||||
<rect x="536" y="157" width="160" height="309" rx="36" fill="#E4DFE7" stroke="#CFC6D3" strokeWidth="2" />
|
||||
<rect x="546" y="168" width="140" height="285" rx="28" fill="url(#hero-phone)" />
|
||||
<rect x="587" y="177" width="58" height="8" rx="4" fill="#0B090D" />
|
||||
<g clipPath="url(#phone-screen-clip)">
|
||||
<circle cx="616" cy="271" r="76" fill="#A855F7" opacity="0.1" />
|
||||
<circle cx="616" cy="275" r="49" fill="url(#hero-orb)" opacity="0.55" />
|
||||
<g className="received-file">
|
||||
<rect x="579" y="224" width="74" height="92" rx="12" fill="#FCF9FD" stroke="#C98AFD" strokeWidth="1.5" />
|
||||
<path d="M630 224h23v23Z" fill="#DEC0F6" />
|
||||
<rect x="590" y="239" width="26" height="26" rx="7" fill="#E9D2FA" />
|
||||
<path d="m595 259 8-9 5 5 5-5 6 9Z" fill="#9E47EF" />
|
||||
<rect x="590" y="278" width="46" height="6" rx="3" fill="#C6BACD" />
|
||||
<rect x="590" y="291" width="34" height="5" rx="2.5" fill="#DFD6E4" />
|
||||
</g>
|
||||
<rect x="564" y="337" width="104" height="42" rx="12" fill="#2F2935" stroke="#4B4251" />
|
||||
<circle cx="581" cy="358" r="8" fill="#A855F7" />
|
||||
<path d="m577.5 358 2.2 2.2 4.5-4.6" fill="none" stroke="white" strokeWidth="1.5" />
|
||||
<text x="595" y="355" fill="#FCF9FD" fontSize="10" fontWeight="700">Received</text>
|
||||
<text x="595" y="368" fill="#A89EAF" fontSize="8">Verified safely</text>
|
||||
<rect x="564" y="393" width="104" height="5" rx="2.5" fill="#3F3745" />
|
||||
<rect x="564" y="393" width="104" height="5" rx="2.5" fill="#A855F7" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g className="transfer-packets">
|
||||
<g className="packet packet-one">
|
||||
<rect x="0" y="0" width="28" height="34" rx="6" fill="#FCF9FE" stroke="#A855F7" />
|
||||
<path d="M19 0h9v9Z" fill="#DDBCF7" />
|
||||
</g>
|
||||
<g className="packet packet-two">
|
||||
<rect x="0" y="0" width="22" height="27" rx="5" fill="#EEE0F9" stroke="#B869F4" />
|
||||
<path d="M14 0h8v8Z" fill="#CAA0E9" />
|
||||
</g>
|
||||
<circle className="packet-spark packet-spark-one" cx="0" cy="0" r="5" fill="#D795FF" />
|
||||
<circle className="packet-spark packet-spark-two" cx="0" cy="0" r="3" fill="#A855F7" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="hero-file-label">
|
||||
<span className="file-type-icon">JPG</span>
|
||||
<span>
|
||||
<strong>summer-photos</strong>
|
||||
<small>42 files · structure preserved</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<figcaption className="sr-only">
|
||||
An illustration of encrypted files moving from a laptop to a phone after receiver approval.
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
247
docs/components/hero-scene.css
Normal file
247
docs/components/hero-scene.css
Normal file
@@ -0,0 +1,247 @@
|
||||
/* Hero transfer scene */
|
||||
.hero-visual {
|
||||
position: relative;
|
||||
width: min(100%, 730px);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-stage {
|
||||
--pointer-x: 0;
|
||||
--pointer-y: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1.13;
|
||||
isolation: isolate;
|
||||
perspective: 1200px;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
position: absolute;
|
||||
z-index: -3;
|
||||
inset: 9% 2% 4% 7%;
|
||||
border: 1px solid rgba(168, 85, 247, 0.14);
|
||||
border-radius: 50%;
|
||||
background-image:
|
||||
linear-gradient(rgba(121, 82, 138, 0.07) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(121, 82, 138, 0.07) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
transform: rotateX(64deg) rotateZ(-8deg) translateZ(-70px);
|
||||
mask-image: radial-gradient(circle, black 10%, transparent 70%);
|
||||
}
|
||||
|
||||
.hero-glow {
|
||||
position: absolute;
|
||||
z-index: -2;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-glow-one {
|
||||
top: 22%;
|
||||
right: 8%;
|
||||
width: 37%;
|
||||
aspect-ratio: 1;
|
||||
background: rgba(168, 85, 247, 0.23);
|
||||
transform: translate(calc(var(--pointer-x) * 12px), calc(var(--pointer-y) * 12px));
|
||||
}
|
||||
|
||||
.hero-glow-two {
|
||||
bottom: 8%;
|
||||
left: 28%;
|
||||
width: 30%;
|
||||
aspect-ratio: 1;
|
||||
background: rgba(126, 58, 236, 0.16);
|
||||
transform: translate(calc(var(--pointer-x) * -9px), calc(var(--pointer-y) * -9px));
|
||||
}
|
||||
|
||||
.hero-scene {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 1% -3% auto -1%;
|
||||
width: 105%;
|
||||
overflow: visible;
|
||||
transform: rotateY(calc(var(--pointer-x) * 1.7deg)) rotateX(calc(var(--pointer-y) * -1.7deg));
|
||||
transform-origin: center;
|
||||
transition: transform 180ms ease-out;
|
||||
}
|
||||
|
||||
.scene-laptop {
|
||||
transform: translate(calc(var(--pointer-x) * -4px), calc(var(--pointer-y) * -4px));
|
||||
transition: transform 200ms ease-out;
|
||||
}
|
||||
|
||||
.scene-phone {
|
||||
transform: translate(calc(var(--pointer-x) * 7px), calc(var(--pointer-y) * 5px));
|
||||
transition: transform 200ms ease-out;
|
||||
}
|
||||
|
||||
.scene-orbits {
|
||||
transform-origin: 385px 325px;
|
||||
animation: scene-orbits 30s linear infinite;
|
||||
}
|
||||
|
||||
.laptop-files {
|
||||
transform-origin: 190px 245px;
|
||||
animation: laptop-files 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.conduit-energy {
|
||||
animation: conduit-energy 2s linear infinite;
|
||||
}
|
||||
|
||||
.approval-gate {
|
||||
transform-origin: 440px 443px;
|
||||
animation: gate-hover 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.received-file {
|
||||
transform-origin: 616px 270px;
|
||||
animation: received-file 7s cubic-bezier(0.34, 1.2, 0.55, 1) infinite;
|
||||
}
|
||||
|
||||
.packet,
|
||||
.packet-spark {
|
||||
opacity: 0;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.packet-one {
|
||||
animation: hero-packet-one 7s cubic-bezier(0.45, 0.05, 0.35, 1) 0.6s infinite;
|
||||
}
|
||||
|
||||
.packet-two {
|
||||
animation: hero-packet-two 7s cubic-bezier(0.45, 0.05, 0.35, 1) 3s infinite;
|
||||
}
|
||||
|
||||
.packet-spark-one {
|
||||
animation: hero-spark-one 7s ease 0.6s infinite;
|
||||
}
|
||||
|
||||
.packet-spark-two {
|
||||
animation: hero-spark-two 7s ease 3s infinite;
|
||||
}
|
||||
|
||||
.hero-status {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(222, 211, 228, 0.95);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 15px 45px rgba(49, 27, 61, 0.11);
|
||||
backdrop-filter: blur(12px);
|
||||
transition: transform 180ms ease-out;
|
||||
}
|
||||
|
||||
.hero-status-secure {
|
||||
top: 2%;
|
||||
right: 5%;
|
||||
gap: 10px;
|
||||
padding: 10px 13px 10px 10px;
|
||||
border-radius: 14px;
|
||||
transform: translate(calc(var(--pointer-x) * 12px), calc(var(--pointer-y) * 8px)) rotate(2deg);
|
||||
animation: status-hover 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
display: grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
background: var(--brand-100);
|
||||
color: var(--brand-600);
|
||||
}
|
||||
|
||||
.status-icon svg {
|
||||
width: 17px;
|
||||
}
|
||||
|
||||
.hero-status strong,
|
||||
.hero-status small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-status strong {
|
||||
color: #2b2530;
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hero-status small {
|
||||
color: #897f8e;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.hero-status-route {
|
||||
top: 18%;
|
||||
left: 9%;
|
||||
gap: 8px;
|
||||
padding: 7px 11px;
|
||||
border-radius: 999px;
|
||||
color: #534b57;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
transform: translate(calc(var(--pointer-x) * -8px), calc(var(--pointer-y) * -6px)) rotate(-3deg);
|
||||
}
|
||||
|
||||
.status-pulse,
|
||||
.live-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #2bc477;
|
||||
box-shadow: 0 0 0 4px rgba(43, 196, 119, 0.13);
|
||||
animation: status-pulse 2.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hero-file-label {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
bottom: 7%;
|
||||
left: 7%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 10px 13px;
|
||||
border: 1px solid rgba(222, 211, 228, 0.95);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 15px 45px rgba(49, 27, 61, 0.11);
|
||||
backdrop-filter: blur(12px);
|
||||
transform: translate(calc(var(--pointer-x) * -10px), calc(var(--pointer-y) * -7px)) rotate(-2deg);
|
||||
transition: transform 180ms ease-out;
|
||||
}
|
||||
|
||||
.file-type-icon {
|
||||
display: grid;
|
||||
width: 37px;
|
||||
height: 37px;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--brand-400), var(--brand-600));
|
||||
color: white;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.hero-file-label strong,
|
||||
.hero-file-label small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-file-label strong {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.hero-file-label small {
|
||||
color: var(--ink-muted);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
140
docs/components/icons.tsx
Normal file
140
docs/components/icons.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
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";
|
||||
|
||||
type IconProps = SVGProps<SVGSVGElement> & {
|
||||
name: IconName;
|
||||
};
|
||||
|
||||
export function Icon({ name, ...props }: IconProps) {
|
||||
const paths: Record<IconName, React.ReactNode> = {
|
||||
arrow: (
|
||||
<>
|
||||
<path d="M5 12h14" />
|
||||
<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" />
|
||||
<path d="M7 19h5M9.5 15v4" />
|
||||
<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" />
|
||||
),
|
||||
github: (
|
||||
<path d="M12 2a10 10 0 0 0-3.2 19.5c.5.1.7-.2.7-.5v-1.9c-2.8.6-3.4-1.2-3.4-1.2-.5-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 0 1.5 1 1.5 1 .9 1.5 2.3 1.1 2.9.8.1-.6.3-1.1.6-1.3-2.2-.3-4.6-1.1-4.6-5a3.9 3.9 0 0 1 1-2.7c-.1-.3-.4-1.3.1-2.7 0 0 .9-.3 2.8 1a9.6 9.6 0 0 1 5.1 0c2-1.3 2.8-1 2.8-1 .6 1.4.2 2.4.1 2.7a3.9 3.9 0 0 1 1 2.7c0 3.9-2.4 4.7-4.6 5 .4.3.7.9.7 1.8V21c0 .3.2.6.7.5A10 10 0 0 0 12 2Z" />
|
||||
),
|
||||
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" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1" />
|
||||
<path d="M14 14h3v3h-3zM18 18h3v3h-3zM18 13h3M13 19h3v2" />
|
||||
</>
|
||||
),
|
||||
shield: (
|
||||
<>
|
||||
<path d="M12 3 4.5 6v5.5c0 4.8 3.2 8.1 7.5 9.5 4.3-1.4 7.5-4.7 7.5-9.5V6z" />
|
||||
<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 (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
{paths[name]}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
276
docs/components/illustration-motion.css
Normal file
276
docs/components/illustration-motion.css
Normal file
@@ -0,0 +1,276 @@
|
||||
@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 svg { width: 68px; height: 68px; }
|
||||
.platform-chip { padding: 8px 10px; font-size: 9px; }
|
||||
.platform-chip-1 { left: 10%; }
|
||||
.platform-chip-2 { right: 0; }
|
||||
.platform-chip-3 { right: -2%; }
|
||||
.platform-chip-4 { left: 17%; }
|
||||
}
|
||||
405
docs/components/invitation-scene.css
Normal file
405
docs/components/invitation-scene.css
Normal file
@@ -0,0 +1,405 @@
|
||||
/* 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; }
|
||||
|
||||
153
docs/components/invitation-showcase.tsx
Normal file
153
docs/components/invitation-showcase.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
51
docs/components/network-illustration.tsx
Normal file
51
docs/components/network-illustration.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
190
docs/components/network-scene.css
Normal file
190
docs/components/network-scene.css
Normal file
@@ -0,0 +1,190 @@
|
||||
/* 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; }
|
||||
|
||||
25
docs/components/platform-orbit.tsx
Normal file
25
docs/components/platform-orbit.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { BrandMark } from "@/components/brand";
|
||||
|
||||
const platforms = ["Android", "iOS", "macOS", "Windows", "Linux"];
|
||||
|
||||
export function PlatformOrbit() {
|
||||
return (
|
||||
<div className="platform-orbit" 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) => (
|
||||
<span
|
||||
key={platform}
|
||||
className={`platform-chip platform-chip-${index + 1}`}
|
||||
>
|
||||
<i />
|
||||
{platform}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
docs/components/platform-scene.css
Normal file
103
docs/components/platform-scene.css
Normal file
@@ -0,0 +1,103 @@
|
||||
/* 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 svg { 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;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #e2d9e6;
|
||||
border-radius: 999px;
|
||||
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-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; }
|
||||
|
||||
52
docs/components/reveal.tsx
Normal file
52
docs/components/reveal.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
type RevealProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
export function Reveal({ children, className = "", delay = 0 }: RevealProps) {
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.add("motion-ready");
|
||||
const target = targetRef.current;
|
||||
if (!target) return;
|
||||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
if (reduceMotion || !("IntersectionObserver" in window)) {
|
||||
target.classList.add("is-visible");
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
entry.target.classList.add("is-visible");
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
},
|
||||
{ rootMargin: "0px 0px -10%", threshold: 0.08 },
|
||||
);
|
||||
|
||||
observer.observe(target);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={targetRef}
|
||||
className={`reveal ${className}`.trim()}
|
||||
data-reveal
|
||||
style={{ "--reveal-delay": `${delay}ms` } as CSSProperties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
docs/components/site-footer.tsx
Normal file
47
docs/components/site-footer.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import Link from "next/link";
|
||||
import { BrandMark } from "@/components/brand";
|
||||
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="arrow" />
|
||||
</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>
|
||||
</div>
|
||||
<div className="footer-bottom page-shell">
|
||||
<p>© 2026 VniDrop contributors.</p>
|
||||
<p>Open source · Early development</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
71
docs/components/site-header.tsx
Normal file
71
docs/components/site-header.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Brand } from "@/components/brand";
|
||||
import { Icon } from "@/components/icons";
|
||||
|
||||
const navigation = [
|
||||
{ href: "/#how-it-works", label: "How it works" },
|
||||
{ href: "/#privacy", label: "Privacy" },
|
||||
{ href: "/#platforms", label: "Platforms" },
|
||||
];
|
||||
|
||||
export function SiteHeader() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="site-header">
|
||||
<div className="header-inner">
|
||||
<Brand />
|
||||
<nav className="desktop-nav" aria-label="Main navigation">
|
||||
{navigation.map((item) => (
|
||||
<Link key={item.href} href={item.href}>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<a
|
||||
className="button button-small button-dark header-github"
|
||||
href="https://github.com/vnidrop/vnidrop"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Icon name="github" />
|
||||
GitHub
|
||||
</a>
|
||||
<button
|
||||
className="menu-button"
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-navigation"
|
||||
aria-label={open ? "Close navigation" : "Open navigation"}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<Icon name={open ? "close" : "menu"} />
|
||||
</button>
|
||||
</div>
|
||||
<nav
|
||||
id="mobile-navigation"
|
||||
className={`mobile-nav${open ? " is-open" : ""}`}
|
||||
aria-label="Mobile navigation"
|
||||
>
|
||||
{navigation.map((item) => (
|
||||
<Link key={item.href} href={item.href} onClick={() => setOpen(false)}>
|
||||
{item.label}
|
||||
<Icon name="arrow" />
|
||||
</Link>
|
||||
))}
|
||||
<a
|
||||
href="https://github.com/vnidrop/vnidrop"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Explore on GitHub
|
||||
<Icon name="github" />
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
278
docs/components/transfer-flow.css
Normal file
278
docs/components/transfer-flow.css
Normal file
@@ -0,0 +1,278 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
108
docs/components/transfer-flow.tsx
Normal file
108
docs/components/transfer-flow.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
9
docs/eslint.config.mjs
Normal file
9
docs/eslint.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTypeScript from "eslint-config-next/typescript";
|
||||
|
||||
export default defineConfig([
|
||||
...nextVitals,
|
||||
...nextTypeScript,
|
||||
globalIgnores([".next/**", "out/**", "next-env.d.ts"]),
|
||||
]);
|
||||
6
docs/next-env.d.ts
vendored
Normal file
6
docs/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
8
docs/next.config.ts
Normal file
8
docs/next.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
trailingSlash: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6082
docs/package-lock.json
generated
Normal file
6082
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
docs/package.json
Normal file
30
docs/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "vnidrop-site",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=22.12"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.2.10",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "26.1.1",
|
||||
"@types/react": "19.2.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"eslint": "9.39.5",
|
||||
"eslint-config-next": "16.2.10",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"overrides": {
|
||||
"postcss": "8.5.19"
|
||||
}
|
||||
}
|
||||
21
docs/public/brand-mark.svg
Normal file
21
docs/public/brand-mark.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="url(#paint0)"/>
|
||||
<path d="M520.4 431.72C495.8 464.52 443.32 530.12 420.36 577.68C390.84 636.72 403.96 699.04 446.6 731.84C487.6 758.08 549.92 758.08 592.56 730.2C633.56 700.68 646.68 636.72 620.44 577.68C597.48 530.12 546.64 464.52 520.4 431.72Z" fill="url(#paint1)"/>
|
||||
<mask id="fold" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="176" y="148" width="671" height="732">
|
||||
<path d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#fold)">
|
||||
<path d="M688 148H782C832 148 842 171.8 847.48 210V303.68L688 148Z" fill="url(#paint2)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0" x1="176" y1="148" x2="904.706" y2="816.252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A855F7"/><stop offset="0.48" stop-color="#9D4DF4"/><stop offset="1" stop-color="#7C2AEF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1" x1="404.439" y1="431.72" x2="707.314" y2="649.286" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A855F7"/><stop offset="0.48" stop-color="#9D4DF4"/><stop offset="1" stop-color="#7C2AEF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2" x1="683.48" y1="144.6" x2="793.636" y2="306.833" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2DDFF"/><stop offset="1" stop-color="#C084FC"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
16
docs/public/site.webmanifest
Normal file
16
docs/public/site.webmanifest
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "VniDrop",
|
||||
"short_name": "VniDrop",
|
||||
"description": "Direct, private file transfer across your devices.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#fbfafc",
|
||||
"theme_color": "#a855f7",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/brand-mark.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
docs/tsconfig.json
Normal file
41
docs/tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user